fix(typert): close remote gateway review gaps
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-02-typert-remote-method-calls.md
|
||||
2026-08-02-typert-remote-method-calls.md: 4268539ecf0d40a9e8080e0571992cc2c5d724af
|
||||
2026-08-02-typert-remote-method-calls.zh.md: f9f426f2fb80c74cb9ebaef15e801ccfcf67e027
|
||||
2026-08-02-typert-remote-method-calls.md: 552e910b403312c7c7a1cec3a14c0dc1f9cc4380
|
||||
2026-08-02-typert-remote-method-calls.zh.md: 18b8c1687d2c01aa23bb7cb9402fccf85fec333d
|
||||
@@ -452,7 +452,7 @@ The Gateway registers only its ownership matcher and RPC handler with Connection
|
||||
|
||||
The shipped vertical path is `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`. The same direct descriptor with an Agent lookup supports both `ctx.api.goals.create(agentId, request)` and `agentCtx.goals.create(request)`. `@RemoteContext('agent')` remains the distinct scoped-receiver mode.
|
||||
|
||||
Connection supplies the shared-channel interceptor and current HTTP carrier mapping. WebSocket migration, the TUI runtime and carrier, TUI Agent Scope wiring, Permission/Approval state machines, Session event streams, call authorization, cancellation, retries, idempotency, and cross-version protocol compatibility remain outside this decision.
|
||||
Connection supplies the shared-channel interceptor and current HTTP carrier mapping. WebSocket migration, the TUI runtime and carrier, TUI Agent Scope wiring, Permission/Approval state machines, Session event streams, call authorization, retries, idempotency, and cross-version protocol compatibility remain outside this decision.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ Gateway 只向 Connection 注册 ownership matcher 和 RPC handler,不注册 H
|
||||
|
||||
已交付的纵向链路是 `@deepseek-ai/dsh-goal/remote → Browser Client API → Connection RPC /api → Host Gateway → GoalService.remoteExportCreate()`。同一个带 Agent lookup 的 direct descriptor 同时支持 `ctx.api.goals.create(agentId, request)` 与 `agentCtx.goals.create(request)`。`@RemoteContext('agent')` 仍是独立的 scoped receiver 模式。
|
||||
|
||||
Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebSocket 迁移、TUI runtime 与 carrier、TUI Agent Scope 接线、Permission/Approval 状态机、Session 事件流、调用授权、取消、重试、幂等及跨版本协议兼容均不属于本决策。
|
||||
Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebSocket 迁移、TUI runtime 与 carrier、TUI Agent Scope 接线、Permission/Approval 状态机、Session 事件流、调用授权、重试、幂等及跨版本协议兼容均不属于本决策。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -245,6 +245,22 @@ describe('GoalService creation and replay', () => {
|
||||
})
|
||||
|
||||
describe('GoalService mutations', () => {
|
||||
it('exposes the supported mutation sequence through Remote wrappers', async () => {
|
||||
const { ctx, agent } = await harness()
|
||||
const created = ctx.goals.remoteExportCreate(agent, { objective: 'remote lifecycle' })
|
||||
const edited = ctx.goals.remoteExportEdit(agent, created.ref, { objective: 'edited remotely' })
|
||||
const paused = ctx.goals.remoteExportPause(agent, edited)
|
||||
const resumed = ctx.goals.remoteExportResume(agent, paused)
|
||||
const completed = ctx.goals.remoteExportComplete(agent, resumed)
|
||||
const cleared = ctx.goals.remoteExportClear(agent, completed)
|
||||
|
||||
expect(edited).toMatchObject({ objective: 'edited remotely', revision: 2 })
|
||||
expect(paused).toMatchObject({ phase: 'paused', revision: 3 })
|
||||
expect(resumed).toMatchObject({ phase: 'active', revision: 4 })
|
||||
expect(completed).toMatchObject({ phase: 'complete', revision: 5 })
|
||||
expect(cleared).toEqual({ id: created.ref.id, revision: 6 })
|
||||
})
|
||||
|
||||
it('edits with compare-and-set revisions and rejects empty edits', async () => {
|
||||
const { ctx, agent } = await harness()
|
||||
const created = ctx.goals.create(agent, { objective: 'old', maxGoalRounds: 4 })
|
||||
|
||||
@@ -998,14 +998,16 @@ describe('TypertGatewayService', () => {
|
||||
}),
|
||||
})
|
||||
expect(invalid.status).toBe(200)
|
||||
await expect(invalid.json()).resolves.toMatchObject({
|
||||
const invalidBody = await invalid.json() as unknown
|
||||
expect(invalidBody).toMatchObject({
|
||||
type: 'server-response',
|
||||
rpcId: 'rpc-invalid',
|
||||
result: {
|
||||
ok: false,
|
||||
error: { code: 'internal', message: expect.stringContaining('plain-object args field') },
|
||||
error: { code: 'internal' },
|
||||
},
|
||||
})
|
||||
expect(JSON.stringify(invalidBody)).toContain('plain-object args field')
|
||||
|
||||
await removeStrict()
|
||||
strictActive = false
|
||||
@@ -1020,14 +1022,16 @@ describe('TypertGatewayService', () => {
|
||||
}),
|
||||
})
|
||||
expect(withdrawn.status).toBe(200)
|
||||
await expect(withdrawn.json()).resolves.toMatchObject({
|
||||
const withdrawnBody = await withdrawn.json() as unknown
|
||||
expect(withdrawnBody).toMatchObject({
|
||||
type: 'server-response',
|
||||
rpcId: 'rpc-withdrawn',
|
||||
result: {
|
||||
ok: false,
|
||||
error: { code: 'internal', message: expect.stringContaining('strict definition was withdrawn') },
|
||||
error: { code: 'internal' },
|
||||
},
|
||||
})
|
||||
expect(JSON.stringify(withdrawnBody)).toContain('strict definition was withdrawn')
|
||||
|
||||
const unclaimed = await fetch(`${server.origin}/api/legacy/list`, { method: 'POST' })
|
||||
expect(unclaimed.status).toBe(404)
|
||||
|
||||
@@ -562,7 +562,8 @@ function validateInvocation(descriptor: InvocationDescriptor): void {
|
||||
}
|
||||
validateCodec(parameter.codec, `${descriptor.id} parameter ${parameter.name}`)
|
||||
}
|
||||
if (descriptor.cancellation !== undefined && descriptor.cancellation.parameter !== 'signal') {
|
||||
const cancellation = descriptor.cancellation as { readonly parameter: string } | undefined
|
||||
if (cancellation !== undefined && cancellation.parameter !== 'signal') {
|
||||
throw new Error(`typert: invocation "${descriptor.id}" cancellation parameter must be "signal"`)
|
||||
}
|
||||
if (descriptor.scope !== undefined) {
|
||||
|
||||
@@ -601,6 +601,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
|
||||
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
|
||||
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
|
||||
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
|
||||
'packages/client/remotes/tests/built-lib.e2e.ts',
|
||||
// The worker-entry packages' built bundles: the only automated proof
|
||||
// that lib/index.js resolves its sibling lib/worker.cjs under plain node
|
||||
// (the e2e lane runs unbuilt, so these files self-skip there).
|
||||
|
||||
+5
-25
@@ -3,8 +3,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { resolvePwshPath } from './packages/bash/pwsh-local/src/resolve.ts'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import ts from 'typescript'
|
||||
import { vitestExecArgv } from './vitest.shared.ts'
|
||||
import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts'
|
||||
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './scripts/coverage-exempt.ts'
|
||||
|
||||
// Prints exact `path:line:col` records for every uncovered statement, branch
|
||||
@@ -18,29 +17,6 @@ const uncoveredLocationsReporter = fileURLToPath(new URL('./scripts/coverage-unc
|
||||
// map applies to every test file. paths must win over package exports so built
|
||||
// lib/ never loads a second module-singleton copy.
|
||||
const pathsPlugin = (): ReturnType<typeof tsconfigPaths> => tsconfigPaths({ projects: ['./tsconfig.base.json'] })
|
||||
const decoratorSyntax = /^\s*@[A-Za-z_$][\w$]*/m
|
||||
|
||||
const standardDecoratorPlugin = () => ({
|
||||
name: 'dsh-standard-decorators',
|
||||
enforce: 'pre' as const,
|
||||
transform(code: string, id: string) {
|
||||
const file = id.split('?', 1)[0]!
|
||||
if (!/\.[cm]?tsx?$/.test(file) || !decoratorSyntax.test(code)) return
|
||||
const result = ts.transpileModule(code, {
|
||||
fileName: file,
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2024,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
jsx: file.endsWith('x') ? ts.JsxEmit.ReactJSX : undefined,
|
||||
sourceMap: true,
|
||||
},
|
||||
})
|
||||
return {
|
||||
code: result.outputText.replace(/\n?\/\/# sourceMappingURL=.*$/u, '\n'),
|
||||
map: result.sourceMapText,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const windowsUnsupportedPackages = process.platform === 'win32'
|
||||
? [
|
||||
@@ -203,6 +179,10 @@ export default defineConfig({
|
||||
'packages/client/hmr/src/invariant.ts',
|
||||
'packages/client/connection/src/index.ts',
|
||||
'packages/client/connection/src/http-bridge.ts',
|
||||
// This assembly imports generated Host-for-Client code that exists
|
||||
// only in lib; the post-build built-bin smoke executes both entries.
|
||||
'packages/client/remotes/src/index.ts',
|
||||
'packages/client/remotes/src/client/index.ts',
|
||||
// Slash/command/input round: per-file gaps deferred with the same
|
||||
// client-lane debt. TODO(gui): cover and remove with the lane above.
|
||||
'packages/client/connection/src/client/fixture.ts',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { vitestExecArgv } from './vitest.shared.ts'
|
||||
import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts'
|
||||
|
||||
// Real-API suite, separate because it spends tokens. Each test self-skips without
|
||||
// its provider credential for keyless CI; credentialed workflows preflight the
|
||||
@@ -36,7 +36,7 @@ export default defineConfig({
|
||||
// Built-artifact e2e suites are unaffected: their built-ness lives in
|
||||
// subprocesses and createRequire lookups, which bypass vite resolution
|
||||
// entirely.
|
||||
plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })],
|
||||
plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] }), standardDecoratorPlugin()],
|
||||
test: {
|
||||
execArgv: vitestExecArgv,
|
||||
setupFiles: ['./scripts/test-invariants.ts'],
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
import ts from 'typescript'
|
||||
|
||||
const decoratorSyntax = /^\s*@[A-Za-z_$][\w$]*/m
|
||||
|
||||
/**
|
||||
* Worker arguments that keep process-wide Web Storage from shadowing jsdom storage.
|
||||
* Node lists the positive spelling in `allowedNodeEnvironmentFlags` for this negatable flag.
|
||||
*/
|
||||
export const vitestExecArgv = process.allowedNodeEnvironmentFlags.has('--webstorage') ? ['--no-webstorage'] : []
|
||||
|
||||
/**
|
||||
* Transform standard TypeScript decorators before Vite's default parser sees source files.
|
||||
* @returns a pre-transform Vite plugin shared by source-mode test configurations.
|
||||
*/
|
||||
export function standardDecoratorPlugin() {
|
||||
return {
|
||||
name: 'dsh-standard-decorators',
|
||||
enforce: 'pre' as const,
|
||||
transform(code: string, id: string) {
|
||||
const file = id.split('?', 1)[0]!
|
||||
if (!/\.[cm]?tsx?$/.test(file) || !decoratorSyntax.test(code)) return
|
||||
const result = ts.transpileModule(code, {
|
||||
fileName: file,
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ES2024,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
jsx: file.endsWith('x') ? ts.JsxEmit.ReactJSX : undefined,
|
||||
sourceMap: true,
|
||||
},
|
||||
})
|
||||
return {
|
||||
code: result.outputText
|
||||
.replace(
|
||||
/^(\s*)(__esDecorate\()/gmu,
|
||||
'$1/* v8 ignore next -- compiler-synthetic decorator accessors have no source behavior */ $2',
|
||||
)
|
||||
.replace(/\n?\/\/# sourceMappingURL=.*$/u, '\n'),
|
||||
map: result.sourceMapText,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { availableParallelism } from 'node:os'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { vitestExecArgv } from './vitest.shared.ts'
|
||||
import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts'
|
||||
|
||||
const DEFAULT_SNAPSHOT_MAX_CONCURRENCY = 5
|
||||
|
||||
@@ -40,7 +40,7 @@ export default defineConfig({
|
||||
// Same resolution note as vitest.config.ts: bare workspace names resolve
|
||||
// through the tsconfig.base.json paths facade; the native option cannot do
|
||||
// this (the root tsconfig is a solution file with no paths).
|
||||
plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })],
|
||||
plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] }), standardDecoratorPlugin()],
|
||||
test: {
|
||||
execArgv: vitestExecArgv,
|
||||
setupFiles: ['./scripts/test-invariants.ts'],
|
||||
|
||||
Reference in New Issue
Block a user