Merge remote-tracking branch 'origin/master' into worktree/custom-deepseek-models

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
#	.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
#	apps/web/tests/snapshots/message-actions/ui.expected.md
#	apps/web/tests/snapshots/seeded-history/ui.expected.md
#	packages/client/ui-models/README.i18n.yaml
#	packages/client/ui-models/README.md
#	packages/client/ui-models/README.zh.md
#	packages/client/ui-models/src/client/ModelsSection.module.css
#	packages/client/ui-models/src/client/ModelsSection.tsx
#	packages/client/ui-models/tests/components.spec.tsx
#	packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-31 14:36:00 +08:00
161 changed files with 4441 additions and 654 deletions

View File

@@ -160,8 +160,8 @@ async function harness(options?: {
await ctx.plugin(LlmService)
if (options?.settings !== false) await ctx.plugin(MemorySettings, options?.settings)
if (options?.credentials !== false) await ctx.plugin(MemoryCredentials, options?.credentials)
// The proxy serves only namespaces a configurable provider addresses, which
// is what the real LLM plugins declare at load; the tests mirror that.
// Model-provider namespaces and the explicit Web preference allowlist are
// the proxy's complete settings surface.
if (options?.configurableProviders !== false) {
ctx.llm.registerConfigurableProviders([
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
@@ -222,19 +222,29 @@ describe('settings domain', () => {
expect(JSON.stringify(value)).not.toContain('user-secret')
})
it('serves only namespaces a registered model provider addresses', async () => {
it('serves model-provider and explicitly allowlisted Web namespaces only', async () => {
// The settings seam is general: any plugin may register a namespace for
// its own configuration. The Web configuration plane is not — it is the
// model-provider surface, and a namespace nothing in the provider
// directory addresses must be invisible and unwritable here, so a future
// plugin cannot become remotely configurable just by registering.
// its own configuration. The Web configuration plane remains opt-in, so a
// future internal plugin cannot become remotely configurable just by
// registering; permission is the one non-model namespace intentionally
// admitted by this surface.
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)
ctx.settings.register(settingsNamespace('some-other-plugin'), z.object({ secretPath: z.string() }))
ctx.settings.register(settingsNamespace('permission'), z.object({
defaultPreset: z.union(['read-only', 'workspace-write']).required(),
}), {
base: { defaultPreset: 'read-only' },
})
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.settings.describe(request({})))
expect(value.namespaces.map(view => view.ns)).toEqual(['llm-deepseek'])
expect(value.namespaces.map(view => view.ns)).toEqual(['llm-deepseek', 'permission'])
const permission = expectOk(await api.settings.mutate(request({
ns: 'permission',
ops: [{ op: 'set', path: ['defaultPreset'], value: 'workspace-write' }],
})))
expect(permission.value).toEqual({ defaultPreset: 'workspace-write' })
for (const response of [
await api.settings.update(request({ ns: 'some-other-plugin', patch: { secretPath: '/etc/shadow' } })),
@@ -277,6 +287,20 @@ describe('settings domain', () => {
.toEqual({ apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://base' })
})
it('broadcasts a permission change without invalidating the model catalog', async () => {
const ctx = await harness()
const permission = ctx.settings.register(settingsNamespace('permission'), z.object({
defaultPreset: z.union(['read-only', 'workspace-write']).required(),
}), {
base: { defaultPreset: 'read-only' },
})
const api = createApiProxy(ctx, DEFAULTS)
const frames = await collectHost(api, ['host/settings-changed', 'host/models-changed'], 1, async () => {
await permission.update({ defaultPreset: 'workspace-write' })
})
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'permission' }])
})
it('maps a stale expectedRevision to settings-conflict carrying both revisions', async () => {
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)

View File

@@ -349,6 +349,68 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
expect(skills.result).toEqual({ ok: true, value: { skills: [{ name: 'commit-helper', description: 'Git commits' }] } })
})
it('lets command.execute finish after the 30-second default unary deadline', async () => {
vi.useFakeTimers()
const timeoutSpy = vi.spyOn(AbortSignal, 'timeout').mockImplementation((milliseconds) => {
const controller = new AbortController()
setTimeout(() => {
controller.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
}, milliseconds)
return controller.signal
})
try {
const api = fakeApi()
api.commands.execute = async (request) => {
await new Promise(resolve => setTimeout(resolve, 30_001))
return {
rpcId: request.rpcId,
result: { ok: true, value: { matched: true, commandId: CommandId('cmd-slow') } },
}
}
const execution = client(api).commands.execute({ sessionId: 's' as never, line: '/slow' })
const assertion = expect(execution).resolves.toMatchObject({
result: { ok: true, value: { matched: true, commandId: 'cmd-slow' } },
})
await Promise.all([
vi.advanceTimersByTimeAsync(30_001),
assertion,
])
expect(timeoutSpy).not.toHaveBeenCalled()
} finally {
timeoutSpy.mockRestore()
vi.useRealTimers()
}
})
it('keeps caller and connection aborts on command.execute', async () => {
const api = fakeApi()
const started = Promise.withResolvers<AbortSignal>()
api.commands.execute = async (request, signal) => {
started.resolve(signal)
if (!signal.aborted) {
await new Promise<void>((resolve) => {
signal.addEventListener('abort', () => { resolve() }, { once: true })
})
}
return {
rpcId: request.rpcId,
result: { ok: false, error: { code: 'cancelled', message: 'aborted', details: {} } },
}
}
const controller = new AbortController()
const execution = client(api).commands.execute(
{ sessionId: 's' as never, line: '/hang' },
controller.signal,
)
const handlerSignal = await started.promise
controller.abort(new Error('connection closed'))
await expect(execution).rejects.toThrow('connection closed')
expect(handlerSignal.aborted).toBe(true)
})
it('propagates the carrier Request signal into command.execute', async () => {
const handler = toFetchHandler(fakeApi())
const controller = new AbortController()