fix(typert): satisfy workspace static gates

This commit is contained in:
imccyu
2026-08-05 23:18:35 +08:00
parent 64a963da0b
commit 9a0a9350c4
29 changed files with 986 additions and 74 deletions

View File

@@ -149,8 +149,10 @@ class DescriptorStore {
for (const descriptor of descriptors) {
const endpoint = typertEndpoint(descriptor)
const entry = this.entries.get(endpoint)
/* v8 ignore next -- duplicate registration is rejected, so no later owner can replace this entry before its effect disposes. */
if (entry?.owner !== owner) continue
this.entries.delete(endpoint)
/* v8 ignore next -- ids and endpoints are committed and withdrawn together under the same unique owner. */
if (this.ids.get(descriptor.id) === entry) this.ids.delete(descriptor.id)
removed.push(endpoint)
}
@@ -200,6 +202,7 @@ class RemoteStore {
packages.set(contribution.package, owner)
descriptors.commit(owner, contribution.descriptors)
yield () => {
/* v8 ignore else -- duplicate package registration is rejected, so this effect remains the package's unique owner. */
if (packages.get(contribution.package) === owner) packages.delete(contribution.package)
descriptors.withdraw(owner, contribution.descriptors)
}
@@ -244,6 +247,7 @@ class LookupStore {
providers.set(key, entry)
changes.emit({ kind: 'lookup', key })
yield () => {
/* v8 ignore next -- duplicate registration is rejected, so this effect remains the key's unique owner. */
if (providers.get(key) !== entry) return
providers.delete(key)
changes.emit({ kind: 'lookup', key })
@@ -303,6 +307,7 @@ class ContextStore {
table.set(key, entry)
changes.emit({ kind, key })
yield () => {
/* v8 ignore next -- duplicate registration is rejected, so this effect remains the key's unique owner. */
if (table.get(key) !== entry) return
table.delete(key)
changes.emit({ kind, key })
@@ -381,8 +386,10 @@ export class TypertRegistry extends Service implements TypeRTService {
for (const record of schemaRecords) schemas.set(record.key, record)
localStore.commit(owner, invocations)
yield () => {
/* v8 ignore else -- duplicate package-face registration is rejected, so this effect remains its unique owner. */
if (packages.get(packageRecord.key) === packageRecord) packages.delete(packageRecord.key)
for (const record of schemaRecords) {
/* v8 ignore else -- duplicate schema registration is rejected, so this contribution remains each record's unique owner. */
if (schemas.get(record.key) === record) schemas.delete(record.key)
}
localStore.withdraw(owner, invocations)

View File

@@ -13,6 +13,7 @@ import type {
TypeRTLookup,
TypeRTRemoteContribution,
} from '@deepseek-ai/dsh-type-meta'
import { apply as applyClientRegistry, inject as clientRegistryInject } from '../src/client/index.ts'
declare module '@deepseek-ai/dsh-type-meta' {
interface TypeRTLookupMap {
@@ -222,6 +223,29 @@ describe('TypertRegistry', () => {
expect(changes).toEqual(['local:goals/create', 'local:goals/create'])
})
it('rejects duplicate invocation endpoints and ids atomically', async () => {
const ctx = await makeCtx()
const first = invocation()
ctx.typert.register({ ...toolsContribution(), invocations: [first] })
expect(() => ctx.typert.remotes.register({
package: '@fixture/duplicate-endpoint',
descriptors: [invocation('@fixture/remote#first'), invocation('@fixture/remote#second')],
})).toThrow('endpoint "goals/create" is already registered')
expect(() => ctx.typert.remotes.register({
package: '@fixture/duplicate-id',
descriptors: [
invocation('@fixture/remote#same'),
{ ...invocation('@fixture/remote#same'), method: 'rename' },
],
})).toThrow('invocation id "@fixture/remote#same" is already registered')
expect(() => ctx.typert.register({
...toolsContribution(),
package: '@fixture/existing-endpoint',
invocations: [{ ...first, id: '@fixture/local#other' }],
})).toThrow('endpoint "goals/create" is already registered')
})
it('mounts Remote contributions in the calling fiber and withdraws them exactly', async () => {
const ctx = await makeCtx()
const descriptor = invocation()
@@ -314,6 +338,131 @@ describe('TypertRegistry', () => {
expect(ctx.typert.contexts.getClient('registryFixture')).toBeUndefined()
})
it('publishes provider changes, rejects duplicate providers, and disposes subscriptions', async () => {
const ctx = await makeCtx()
const changes: string[] = []
const disposeLookupSubscription = ctx.typert.lookups.subscribe((change) => {
changes.push(`${change.kind}:${change.key}`)
})
const disposeContextSubscription = ctx.typert.contexts.subscribe((change) => {
changes.push(`${change.kind}:${change.key}`)
})
const lookup = {
parameter: 'agent',
wire: 'agentId',
hostTypeSymbol: '@fixture#Agent',
wireTypeSymbol: '@fixture#AgentId',
resolve: () => undefined,
}
const host = {
wire: 'agentId',
wireTypeSymbol: '@fixture#AgentId',
resolve: () => undefined,
}
const client = { identity: () => undefined }
const disposeLookup = ctx.typert.lookups.register('fixture', lookup)
const disposeHost = ctx.typert.contexts.registerHost('registryFixture', host)
const disposeClient = ctx.typert.contexts.registerClient('registryFixture', client)
expect(() => ctx.typert.lookups.register('fixture', lookup)).toThrow('already registered')
expect(() => ctx.typert.contexts.registerHost('registryFixture', host)).toThrow('already registered')
expect(() => ctx.typert.contexts.registerClient('registryFixture', client)).toThrow('already registered')
await Promise.all([disposeLookup(), disposeHost(), disposeClient()])
expect(changes).toEqual([
'lookup:fixture',
'host-context:registryFixture',
'client-context:registryFixture',
'lookup:fixture',
'host-context:registryFixture',
'client-context:registryFixture',
])
await Promise.all([disposeLookupSubscription(), disposeContextSubscription()])
ctx.typert.lookups.register('fixture', lookup)
expect(changes).toHaveLength(6)
})
it('validates every invocation and provider boundary', async () => {
const ctx = await makeCtx()
const strict = {
mode: 'strict' as const,
typeSymbol: '@fixture#Value',
schema: z.string(),
}
const strictInvocation: InvocationDescriptor = {
...invocation('@fixture/remote#strict'),
implementation: 'remoteExportCreate',
parameters: [{ name: 'request', wire: 'request', source: 'json', codec: strict }],
result: strict,
}
const dispose = ctx.typert.remotes.register({ package: '@fixture/strict', descriptors: [strictInvocation] })
await dispose()
const malformed: readonly [InvocationDescriptor, string][] = [
[{ ...invocation(), id: '' }, 'invocation id'],
[{ ...invocation(), namespace: 'bad/name' }, 'namespace'],
[{ ...invocation(), implementation: 'bad/name' }, 'implementation method'],
[{
...invocation(),
parameters: [
...invocation().parameters,
{ name: 'other', wire: 'request', source: 'json', codec: { mode: 'src-json' } },
],
}, 'repeats wire field'],
[{
...invocation(),
parameters: [{ name: 'agent', wire: 'agentId', source: 'lookup', codec: { mode: 'src-json' } }],
}, 'has no lookup key'],
[{
...invocation(),
parameters: [{
name: 'request', wire: 'request', source: 'json', lookup: 'fixture', codec: { mode: 'src-json' },
}],
}, 'JSON parameter'],
[{
...invocation(),
invocation: {
kind: 'context', context: 'registryFixture', wire: 'request', codec: { mode: 'src-json' },
},
}, 'repeats wire field'],
[{
...invocation(),
result: { mode: 'strict', typeSymbol: '', schema: z.string() },
}, 'type symbol'],
[{
...invocation(),
result: { mode: 'strict', typeSymbol: '@fixture#Broken', schema: {} as z.ZodType },
}, 'has no parse'],
]
for (const [index, [descriptor, message]] of malformed.entries()) {
expect(() => ctx.typert.remotes.register({
package: `@fixture/malformed-${String(index)}`,
descriptors: [descriptor],
})).toThrow(message)
}
expect(() => ctx.typert.lookups.register('bad#key' as 'fixture', {
parameter: 'agent',
wire: 'agent/id',
hostTypeSymbol: '',
wireTypeSymbol: '',
resolve: () => undefined,
})).toThrow('lookup key')
expect(() => ctx.typert.lookups.register('fixture', {
parameter: 'agent',
wire: 'agent/id',
hostTypeSymbol: '@fixture#Agent',
wireTypeSymbol: '@fixture#AgentId',
resolve: () => undefined,
})).toThrow('lookup wire field')
})
it('installs the registry through the Client entry without importing the Host entry', async () => {
const ctx = new Context()
await ctx.plugin({ inject: clientRegistryInject, apply: applyClientRegistry })
expect(ctx.typert.list()).toEqual([])
})
it('contains change-listener failures and still notifies later listeners', async () => {
const ctx = await makeCtx()
const warnings: unknown[] = []