|
|
@@ -9,8 +9,8 @@ import type { WebServer, WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
|
import {
|
|
|
bindTypertRemote,
|
|
|
Remote,
|
|
|
+ RemoteError,
|
|
|
RemoteScope,
|
|
|
- TypertLookupFailure,
|
|
|
type InvocationDescriptor,
|
|
|
type TypertContext,
|
|
|
type TypertLookup,
|
|
|
@@ -37,6 +37,10 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
interface TypertContextMap {
|
|
|
gatewayFixture: TypertContext<string>
|
|
|
}
|
|
|
+
|
|
|
+ interface RemoteErrorDetailsMap {
|
|
|
+ 'session/agent-busy': { readonly reason: string }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const emptyModel: TypertContribution['model'] = {
|
|
|
@@ -452,7 +456,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'lookup-unavailable')
|
|
|
+ }), 'gateway/lookup-unavailable')
|
|
|
expect(service.calls).toEqual([])
|
|
|
})
|
|
|
|
|
|
@@ -485,7 +489,7 @@ describe('TypertGatewayService', () => {
|
|
|
})).resolves.toBe('land')
|
|
|
await expectCode(ctx.typertGateway.invoke({
|
|
|
namespace: 'other', method: 'absent', args: {},
|
|
|
- }), 'invocation-unavailable')
|
|
|
+ }), 'gateway/invocation-unavailable')
|
|
|
})
|
|
|
|
|
|
it('rejects SRC wire collisions and unavailable Context providers', async () => {
|
|
|
@@ -496,14 +500,14 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'colliding-wire',
|
|
|
method: 'run',
|
|
|
args: { agentId: 'agent-1' },
|
|
|
- }), 'signature-invalid')
|
|
|
+ }), 'gateway/signature-invalid')
|
|
|
|
|
|
const missing = await setup()
|
|
|
await expectCode(missing.ctx.typertGateway.invoke({
|
|
|
namespace: 'goals',
|
|
|
method: 'rename',
|
|
|
args: { agentId: 'agent-1', request: { title: 'land' } },
|
|
|
- }), 'context-unavailable')
|
|
|
+ }), 'gateway/context-unavailable')
|
|
|
|
|
|
const contextCollision = await setupGateway()
|
|
|
await contextCollision.plugin(ContextWireService)
|
|
|
@@ -512,7 +516,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'context-wire',
|
|
|
method: 'run',
|
|
|
args: { agentId: 'agent-1' },
|
|
|
- }), 'signature-invalid')
|
|
|
+ }), 'gateway/signature-invalid')
|
|
|
})
|
|
|
|
|
|
it('re-reads Service and providers on every strict invocation', async () => {
|
|
|
@@ -526,7 +530,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'lookup-unavailable')
|
|
|
+ }), 'gateway/lookup-unavailable')
|
|
|
|
|
|
registerAgentLookup(ctx, agent)
|
|
|
await serviceFiber.dispose()
|
|
|
@@ -534,7 +538,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'service-unavailable')
|
|
|
+ }), 'gateway/service-unavailable')
|
|
|
})
|
|
|
|
|
|
it('re-reads and contains Context providers', async () => {
|
|
|
@@ -548,7 +552,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'rename',
|
|
|
args: { agentId: 'agent-1', request: { title: 'land' } },
|
|
|
- }), 'context-unavailable')
|
|
|
+ }), 'gateway/context-unavailable')
|
|
|
|
|
|
ctx.typert.contexts.registerHost('gatewayFixture', {
|
|
|
...contextProvider(scoped),
|
|
|
@@ -558,13 +562,13 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'rename',
|
|
|
args: { agentId: 'agent-1', request: { title: 'land' } },
|
|
|
- }), 'context-failed')
|
|
|
+ }), 'gateway/context-failed')
|
|
|
expect(error.cause).toEqual(new Error('provider failed'))
|
|
|
})
|
|
|
|
|
|
it('preserves a Host Context policy rejection for the active RPC adapter', async () => {
|
|
|
const { ctx } = await setup()
|
|
|
- const rejection = new TypertLookupFailure({ code: 'agent-busy', message: 'owned', details: { reason: 'subagent' } })
|
|
|
+ const rejection = new RemoteError('session/agent-busy', 'owned', { reason: 'subagent' })
|
|
|
ctx.typert.contexts.registerHost('gatewayFixture', {
|
|
|
...contextProvider(ctx.extend()),
|
|
|
resolve: async () => { throw rejection },
|
|
|
@@ -590,7 +594,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'rename',
|
|
|
args: { agentId: 'agent-1', request: { title: 'land' } },
|
|
|
- }), 'provider-mismatch')
|
|
|
+ }), 'gateway/provider-mismatch')
|
|
|
await mismatch()
|
|
|
|
|
|
ctx.typert.contexts.registerHost('gatewayFixture', {
|
|
|
@@ -601,7 +605,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'rename',
|
|
|
args: { agentId: 'agent-1', request: { title: 'land' } },
|
|
|
- }), 'context-not-found')
|
|
|
+ }), 'gateway/context-not-found')
|
|
|
})
|
|
|
|
|
|
it('contains lookup provider failures and missing identities', async () => {
|
|
|
@@ -615,7 +619,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'lookup-failed')
|
|
|
+ }), 'gateway/lookup-failed')
|
|
|
expect(failure.cause).toEqual(new Error('lookup failed'))
|
|
|
await throwing()
|
|
|
|
|
|
@@ -627,7 +631,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'lookup-not-found')
|
|
|
+ }), 'gateway/lookup-not-found')
|
|
|
await missing()
|
|
|
|
|
|
ctx.typert.lookups.register('gatewayFixture', {
|
|
|
@@ -650,7 +654,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'passthrough',
|
|
|
args: { value: 'would pass through SRC' },
|
|
|
- }), 'definition-unavailable')
|
|
|
+ }), 'gateway/definition-unavailable')
|
|
|
})
|
|
|
|
|
|
it('seeds the no-downgrade guard from definitions present before Gateway startup', async () => {
|
|
|
@@ -665,7 +669,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'passthrough',
|
|
|
args: { value: 'would pass through SRC' },
|
|
|
- }), 'definition-unavailable')
|
|
|
+ }), 'gateway/definition-unavailable')
|
|
|
})
|
|
|
|
|
|
it('retains the no-downgrade guard across Gateway Service reloads', async () => {
|
|
|
@@ -684,7 +688,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'passthrough',
|
|
|
args: { value: 'would pass through SRC' },
|
|
|
- }), 'definition-unavailable')
|
|
|
+ }), 'gateway/definition-unavailable')
|
|
|
})
|
|
|
|
|
|
it('rejects ambiguous SRC endpoints independently of reflection order', async () => {
|
|
|
@@ -696,7 +700,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'shared',
|
|
|
method: 'run',
|
|
|
args: { value: 'ship' },
|
|
|
- }), 'ambiguous-endpoint')
|
|
|
+ }), 'gateway/ambiguous-endpoint')
|
|
|
expect(error.message).toContain('firstShared, secondShared')
|
|
|
})
|
|
|
|
|
|
@@ -714,7 +718,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: testCase.namespace,
|
|
|
method: 'run',
|
|
|
args: testCase.args,
|
|
|
- }), 'signature-invalid')
|
|
|
+ }), 'gateway/signature-invalid')
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -728,7 +732,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'signature-invalid')
|
|
|
+ }), 'gateway/signature-invalid')
|
|
|
})
|
|
|
|
|
|
it('requires exact wire fields before invoking business code', async () => {
|
|
|
@@ -739,17 +743,17 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { request: { title: 'ship' } },
|
|
|
- }), 'arguments-invalid')
|
|
|
+ }), 'gateway/arguments-invalid')
|
|
|
await expectCode(ctx.typertGateway.invoke({
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' }, optional: true },
|
|
|
- }), 'arguments-invalid')
|
|
|
+ }), 'gateway/arguments-invalid')
|
|
|
await expectCode(ctx.typertGateway.invoke({
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: [] as unknown as Record<string, unknown>,
|
|
|
- }), 'arguments-invalid')
|
|
|
+ }), 'gateway/arguments-invalid')
|
|
|
expect(service.calls).toEqual([])
|
|
|
})
|
|
|
|
|
|
@@ -761,7 +765,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'strictOnly',
|
|
|
args: { request: { title: 1 } },
|
|
|
- }), 'input-invalid')
|
|
|
+ }), 'gateway/input-invalid')
|
|
|
|
|
|
service.nextResult = { title: 1 }
|
|
|
await expect(ctx.typertGateway.invoke({
|
|
|
@@ -799,7 +803,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'passthrough',
|
|
|
args: { value },
|
|
|
- }), 'input-invalid')
|
|
|
+ }), 'gateway/input-invalid')
|
|
|
})
|
|
|
|
|
|
it('admits an omitted SRC field and hands the Host method undefined', async () => {
|
|
|
@@ -823,7 +827,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'passthrough',
|
|
|
args: { value: cyclic },
|
|
|
- }), 'input-invalid')
|
|
|
+ }), 'gateway/input-invalid')
|
|
|
|
|
|
const result = new Date(0)
|
|
|
service.nextResult = result
|
|
|
@@ -855,7 +859,7 @@ describe('TypertGatewayService', () => {
|
|
|
for (const value of [sparseWithExtra, symbolArray, symbolObject, hidden, accessor]) {
|
|
|
await expectCode(ctx.typertGateway.invoke({
|
|
|
namespace: 'goals', method: 'passthrough', args: { value },
|
|
|
- }), 'input-invalid')
|
|
|
+ }), 'gateway/input-invalid')
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -871,7 +875,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'create',
|
|
|
args: { agentId: 'agent-1', request: { title: 'ship' } },
|
|
|
- }), 'provider-mismatch')
|
|
|
+ }), 'gateway/provider-mismatch')
|
|
|
})
|
|
|
|
|
|
it('validates binding identity and active method availability', async () => {
|
|
|
@@ -881,7 +885,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'wrong-binding',
|
|
|
method: 'run',
|
|
|
args: { value: 'ship' },
|
|
|
- }), 'binding-invalid')
|
|
|
+ }), 'gateway/binding-invalid')
|
|
|
|
|
|
await ctx.plugin(GoalService)
|
|
|
registerStrict(ctx, [{ ...passthroughDescriptor(), method: 'missing' }])
|
|
|
@@ -889,7 +893,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'missing',
|
|
|
args: { value: 'ship' },
|
|
|
- }), 'method-unavailable')
|
|
|
+ }), 'gateway/method-unavailable')
|
|
|
})
|
|
|
|
|
|
it('requires a visible binding and supports explicitly provided plain Services', async () => {
|
|
|
@@ -904,7 +908,7 @@ describe('TypertGatewayService', () => {
|
|
|
}])
|
|
|
await expectCode(ctx.typertGateway.invoke({
|
|
|
namespace: 'no-binding', method: 'run', args: { value: 'ship' },
|
|
|
- }), 'binding-invalid')
|
|
|
+ }), 'gateway/binding-invalid')
|
|
|
|
|
|
const plain: {
|
|
|
typertRemote?: ReturnType<typeof bindTypertRemote>
|
|
|
@@ -941,7 +945,7 @@ describe('TypertGatewayService', () => {
|
|
|
try {
|
|
|
await expectCode(ctx.typertGateway.invoke({
|
|
|
namespace: 'missing-method', method: 'run', args: { value: 'ship' },
|
|
|
- }), 'method-unavailable')
|
|
|
+ }), 'gateway/method-unavailable')
|
|
|
} finally {
|
|
|
Object.defineProperty(MissingMethodService.prototype, 'run', descriptor)
|
|
|
}
|
|
|
@@ -965,7 +969,7 @@ describe('TypertGatewayService', () => {
|
|
|
namespace: 'goals',
|
|
|
method: 'absent',
|
|
|
args: {},
|
|
|
- }), 'invocation-unavailable')
|
|
|
+ }), 'gateway/invocation-unavailable')
|
|
|
})
|
|
|
|
|
|
it('mounts a shared /api interceptor through an optional Connection and returns existing RPC results', async () => {
|
|
|
@@ -1002,7 +1006,7 @@ describe('TypertGatewayService', () => {
|
|
|
const invalid = await handler('goals/create', { invalid: true }, signal)
|
|
|
expect(invalid).toMatchObject({
|
|
|
ok: false,
|
|
|
- error: { code: 'internal' },
|
|
|
+ error: { code: 'gateway/internal' },
|
|
|
})
|
|
|
if (invalid.ok) throw new Error('invalid Remote payload unexpectedly succeeded')
|
|
|
expect(invalid.error.message).toMatch(/exactly one plain-object args field/)
|
|
|
@@ -1018,13 +1022,13 @@ describe('TypertGatewayService', () => {
|
|
|
|
|
|
for (const endpoint of ['goals', '/create', 'goals/', 'goals/create/extra']) {
|
|
|
const result = await handler(endpoint, { args: {} }, signal)
|
|
|
- expect(result).toMatchObject({ ok: false, error: { code: 'internal' } })
|
|
|
+ expect(result).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
|
|
if (result.ok) throw new Error('invalid Remote endpoint unexpectedly succeeded')
|
|
|
expect(result.error.message).toContain('invalid Remote endpoint')
|
|
|
}
|
|
|
for (const payload of [null, [], { args: {}, extra: true }, { only: true }, { args: null }, { args: [] }]) {
|
|
|
const result = await handler('goals/create', payload, signal)
|
|
|
- expect(result).toMatchObject({ ok: false, error: { code: 'internal' } })
|
|
|
+ expect(result).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
|
|
if (result.ok) throw new Error('invalid Remote payload unexpectedly succeeded')
|
|
|
expect(result.error.message).toContain('plain-object args field')
|
|
|
}
|
|
|
@@ -1036,7 +1040,7 @@ describe('TypertGatewayService', () => {
|
|
|
new AbortController().signal,
|
|
|
)).resolves.toEqual({
|
|
|
ok: false,
|
|
|
- error: { code: 'internal', message: 'non-error failure', details: {} },
|
|
|
+ error: { code: 'gateway/internal', message: 'non-error failure', details: {} },
|
|
|
})
|
|
|
|
|
|
// A business rejection observed while the carrier signal is already aborted
|
|
|
@@ -1051,7 +1055,7 @@ describe('TypertGatewayService', () => {
|
|
|
)).resolves.toEqual({
|
|
|
ok: false,
|
|
|
error: {
|
|
|
- code: 'cancelled',
|
|
|
+ code: 'gateway/cancelled',
|
|
|
message: 'Remote invocation "goals/fail" was aborted',
|
|
|
details: {},
|
|
|
},
|
|
|
@@ -1075,7 +1079,7 @@ describe('TypertGatewayService', () => {
|
|
|
args: { clientId: 'missing-client', eventId: 'missing', outcome: { kind: 'next' } },
|
|
|
}
|
|
|
const inactive = await handler('$events/result', result, new AbortController().signal)
|
|
|
- expect(inactive).toMatchObject({ ok: false, error: { code: 'internal' } })
|
|
|
+ expect(inactive).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
|
|
if (inactive.ok) throw new Error('inactive Remote event result unexpectedly succeeded')
|
|
|
expect(inactive.error.message).toContain('identifies no active event stream')
|
|
|
|
|
|
@@ -1098,7 +1102,7 @@ describe('TypertGatewayService', () => {
|
|
|
|
|
|
for (const payload of [null, [], {}, { other: {} }]) {
|
|
|
const invalid = await handler('$events/result', payload, carrier.signal)
|
|
|
- expect(invalid).toMatchObject({ ok: false, error: { code: 'internal' } })
|
|
|
+ expect(invalid).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
|
|
|
if (invalid.ok) throw new Error('invalid Remote event result payload unexpectedly succeeded')
|
|
|
expect(invalid.error.message).toContain('requires exactly one plain-object args field')
|
|
|
}
|
|
|
@@ -1122,13 +1126,13 @@ describe('TypertGatewayService', () => {
|
|
|
await ctx.plugin(GoalService)
|
|
|
registerStrict(ctx, [createDescriptor()])
|
|
|
const failure = {
|
|
|
- code: 'agent-busy',
|
|
|
+ code: 'session/agent-busy',
|
|
|
message: 'session is owned by subagent routing',
|
|
|
details: { reason: 'use subagent delivery for this child session' },
|
|
|
}
|
|
|
ctx.typert.lookups.register('gatewayFixture', {
|
|
|
...agentLookup({ id: 'agent-1' }),
|
|
|
- resolve: () => { throw new TypertLookupFailure(failure) },
|
|
|
+ resolve: () => { throw new RemoteError('session/agent-busy', failure.message, failure.details) },
|
|
|
})
|
|
|
const handler = rawConnection(ctx).handler
|
|
|
if (handler === undefined) throw new Error('fixture Connection did not retain the /api interceptor')
|
|
|
@@ -1225,7 +1229,7 @@ describe('TypertGatewayService', () => {
|
|
|
rpcId: 'rpc-invalid',
|
|
|
result: {
|
|
|
ok: false,
|
|
|
- error: { code: 'internal' },
|
|
|
+ error: { code: 'gateway/internal' },
|
|
|
},
|
|
|
})
|
|
|
expect(JSON.stringify(invalidBody)).toContain('plain-object args field')
|
|
|
@@ -1249,7 +1253,7 @@ describe('TypertGatewayService', () => {
|
|
|
rpcId: 'rpc-withdrawn',
|
|
|
result: {
|
|
|
ok: false,
|
|
|
- error: { code: 'internal' },
|
|
|
+ error: { code: 'gateway/definition-unavailable' },
|
|
|
},
|
|
|
})
|
|
|
expect(JSON.stringify(withdrawnBody)).toContain('strict definition was withdrawn')
|