Merge branch 'worktree-llm-dynamic-config' into worktree-llm-web-config
# Conflicts: # apps/cli/cordis.yml # apps/web/tests/snapshots/code-mode-round/session.jsonl # apps/web/tests/snapshots/cordis-tool-round/session.jsonl # apps/web/tests/snapshots/fresh-round-trip/session.jsonl # apps/web/tests/snapshots/lifecycle-chrome/session.jsonl # apps/web/tests/snapshots/live-interactions/session.jsonl # apps/web/tests/snapshots/navigation-panes/seed.jsonl # apps/web/tests/snapshots/question-composer/session.jsonl # apps/web/tests/snapshots/seeded-history/seed.jsonl # apps/web/tests/snapshots/steering/session.jsonl # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/settings.i18n.yaml # docs/event-producer-consumer.md # docs/module-graph.md # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # packages/client/connection/README.i18n.yaml # packages/client/connection/src/index.ts # packages/client/connection/tests/node-half.spec.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/index.ts # packages/client/runtime/tests/fake-api.ts # packages/client/ui-models/README.i18n.yaml # packages/examples/tui-demo/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/package.json # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/rpc.schema.ts # packages/host/apiproxy/src/api/rpc.ts # packages/llm/llm-deepseek/README.i18n.yaml # packages/llm/llm-deepseek/README.zh.md # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm/README.i18n.yaml # packages/llm/llm/README.zh.md # packages/sdk/sdk-client/README.i18n.yaml # packages/settings/settings/README.i18n.yaml # packages/settings/settings/README.md # packages/settings/settings/README.zh.md # packages/subagent/subagent-dsh-sdk/README.i18n.yaml # packages/subagent/subagent-dsh-sdk/README.zh.md # packages/support/llm-replay/README.i18n.yaml # packages/ui/jsonrpc/README.i18n.yaml # packages/ui/jsonrpc/README.zh.md # packages/ui/tui/tests/snapshots/model-selector.expected.txt # packages/ui/tui/tests/snapshots/model-switching.expected.txt # packages/ui/tui/tests/snapshots/resume-sessions.expected.txt # packages/ui/tui/tests/snapshots/status-diagnostics-narrow.expected.txt # packages/ui/tui/tests/snapshots/status-diagnostics.expected.txt # packages/ui/tui/tests/tui.snapshot.ts # pnpm-lock.yaml # python/sdk/README.i18n.yaml # scripts/snapshots/translation-prompt-v4/request-response.expected.json
This commit is contained in:
@@ -67,9 +67,15 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
|
||||
},
|
||||
}
|
||||
},
|
||||
async rename(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { title: request.payload.title, seq: 0 } } }
|
||||
},
|
||||
async prompt(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { accepted: true as const } } }
|
||||
},
|
||||
async updateQueue(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { accepted: true as const } } }
|
||||
},
|
||||
async cancel(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { accepted: true as const } } }
|
||||
},
|
||||
@@ -81,6 +87,12 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
|
||||
async pickDirectory(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { path: null } } }
|
||||
},
|
||||
async listDirectory(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { path: '/w', home: '/w', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false } } }
|
||||
},
|
||||
async createDirectory(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { path: '/w/new' } } }
|
||||
},
|
||||
async openPath(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { opened: true as const } } }
|
||||
},
|
||||
@@ -228,7 +240,7 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
if (!response.result.ok) expect(response.result.error.code).toBe('session-not-found')
|
||||
})
|
||||
|
||||
it('covers create/prompt/cancel/describe passthrough', async () => {
|
||||
it('covers create/prompt/updateQueue/cancel/describe passthrough', async () => {
|
||||
const c = client()
|
||||
expect((await c.sessions.create({})).result.ok).toBe(true)
|
||||
expect((await c.sessions.models({ sessionId: 's' as never })).result.ok).toBe(true)
|
||||
@@ -248,7 +260,14 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
const renamed = await c.sessions.rename({ sessionId: 's' as never, title: 'named' })
|
||||
expect(renamed.result).toMatchObject({ ok: true, value: { title: 'named', seq: 0 } })
|
||||
expect((await c.sessions.prompt({ sessionId: 's' as never, mode: 'queue', content: [{ type: 'text', text: 'x' }] })).result.ok).toBe(true)
|
||||
expect((await c.sessions.updateQueue({
|
||||
sessionId: 's' as never,
|
||||
itemId: 'item-1' as never,
|
||||
action: { kind: 'remove' },
|
||||
})).result.ok).toBe(true)
|
||||
expect((await c.sessions.cancel({ sessionId: 's' as never })).result.ok).toBe(true)
|
||||
expect((await c.host.describe({})).result.ok).toBe(true)
|
||||
})
|
||||
@@ -263,6 +282,19 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
expect(response.result).toEqual({ ok: true, value: { path: '/tmp/project' } })
|
||||
})
|
||||
|
||||
it('round-trips the browse listing and creation calls through the wire form', async () => {
|
||||
const c = client()
|
||||
const listed = await c.host.listDirectory({ path: '/w' })
|
||||
expect(listed.result).toEqual({
|
||||
ok: true,
|
||||
value: { path: '/w', home: '/w', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [], truncated: false },
|
||||
})
|
||||
const home = await c.host.listDirectory({})
|
||||
expect(home.result).toMatchObject({ ok: true, value: { home: '/w' } })
|
||||
const created = await c.host.createDirectory({ path: '/w', name: 'fresh' })
|
||||
expect(created.result).toEqual({ ok: true, value: { path: '/w/new' } })
|
||||
})
|
||||
|
||||
it('round-trips host.openPath through the wire form', async () => {
|
||||
const api = fakeApi()
|
||||
let opened: string | undefined
|
||||
@@ -293,7 +325,7 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-sig', method: 'command.execute', payload: { sessionId: 's', line: '/hang' } })
|
||||
// The fake's /hang settles only when the invoke-level signal aborts: a
|
||||
// completed response with the cancelled error proves req.signal reached it.
|
||||
const pending = handler.fetch(new Request('http://x/api/command.execute', { method: 'POST', body, signal: controller.signal }))
|
||||
const pending = handler.fetch(new Request('http://x/api/command.execute', { method: 'POST', headers: { 'content-type': 'application/json' }, body, signal: controller.signal }))
|
||||
controller.abort()
|
||||
const response = await pending
|
||||
const parsed = await response.json() as { rpcId: string; result: { ok: boolean; error?: { code: string } } }
|
||||
@@ -318,7 +350,7 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
const controller = new AbortController()
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-picker', method: 'host.pickDirectory', payload: {} })
|
||||
const pending = handler.fetch(new Request('http://x/api/host.pickDirectory', {
|
||||
method: 'POST', body, signal: controller.signal,
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body, signal: controller.signal,
|
||||
}))
|
||||
controller.abort()
|
||||
const parsed = await (await pending).json() as { result: { error?: { code: string } } }
|
||||
@@ -330,18 +362,18 @@ describe('handler carrier-layer statuses', () => {
|
||||
const handler = toFetchHandler(fakeApi())
|
||||
|
||||
it('404s unknown paths and non-POST non-stream methods', async () => {
|
||||
expect((await handler.fetch(new Request('http://x/other', { method: 'POST', body: '{}' }))).status).toBe(404)
|
||||
expect((await handler.fetch(new Request('http://x/other', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }))).status).toBe(404)
|
||||
expect((await handler.fetch(new Request('http://x/api/session.list', { method: 'GET' }))).status).toBe(404)
|
||||
expect((await handler.fetch(new Request('http://x/api/no.such', { method: 'POST', body: JSON.stringify({ type: 'client-request', rpcId: 'r', method: 'no.such', payload: {} }) }))).status).toBe(404)
|
||||
expect((await handler.fetch(new Request('http://x/api/no.such', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ type: 'client-request', rpcId: 'r', method: 'no.such', payload: {} }) }))).status).toBe(404)
|
||||
})
|
||||
|
||||
it('400s a non-JSON body', async () => {
|
||||
const response = await handler.fetch(new Request('http://x/api/session.list', { method: 'POST', body: 'not json' }))
|
||||
const response = await handler.fetch(new Request('http://x/api/session.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body: 'not json' }))
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('rejects a malformed envelope with bad-request and the invalid-request sentinel rpcId', async () => {
|
||||
const response = await handler.fetch(new Request('http://x/api/session.list', { method: 'POST', body: JSON.stringify({ nope: true }) }))
|
||||
const response = await handler.fetch(new Request('http://x/api/session.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ nope: true }) }))
|
||||
expect(response.status).toBe(200)
|
||||
const body = await response.json() as { rpcId: string; result: { ok: boolean; error?: { code: string } } }
|
||||
expect(body.rpcId).toBe('invalid-request')
|
||||
@@ -350,7 +382,7 @@ describe('handler carrier-layer statuses', () => {
|
||||
|
||||
it('rejects a method/path mismatch echoing the envelope rpcId', async () => {
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-9', method: 'session.cancel', payload: {} })
|
||||
const response = await handler.fetch(new Request('http://x/api/session.list', { method: 'POST', body }))
|
||||
const response = await handler.fetch(new Request('http://x/api/session.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body }))
|
||||
const parsed = await response.json() as { rpcId: string; result: { error?: { message: string } } }
|
||||
expect(parsed.rpcId).toBe('r-9')
|
||||
expect(parsed.result.error?.message).toContain('does not match path')
|
||||
@@ -358,7 +390,7 @@ describe('handler carrier-layer statuses', () => {
|
||||
|
||||
it('rejects an invalid payload with the zod issues attached', async () => {
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-10', method: 'session.cancel', payload: {} })
|
||||
const response = await handler.fetch(new Request('http://x/api/session.cancel', { method: 'POST', body }))
|
||||
const response = await handler.fetch(new Request('http://x/api/session.cancel', { method: 'POST', headers: { 'content-type': 'application/json' }, body }))
|
||||
const parsed = await response.json() as { result: { error?: { code: string; details: { issues: unknown[] } } } }
|
||||
expect(parsed.result.error?.code).toBe('bad-request')
|
||||
expect(parsed.result.error?.details.issues.length).toBeGreaterThan(0)
|
||||
@@ -367,23 +399,23 @@ describe('handler carrier-layer statuses', () => {
|
||||
it('500s when the impl itself throws', async () => {
|
||||
const crashing = toFetchHandler(fakeApi({ crashOn: 'session.list' }))
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-11', method: 'session.list', payload: {} })
|
||||
const response = await crashing.fetch(new Request('http://x/api/session.list', { method: 'POST', body }))
|
||||
const response = await crashing.fetch(new Request('http://x/api/session.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body }))
|
||||
expect(response.status).toBe(500)
|
||||
expect(await response.text()).toContain('impl crashed')
|
||||
})
|
||||
|
||||
it('routes /api/respond, rejecting malformed client-responses as a receipt', async () => {
|
||||
const good = JSON.stringify({ type: 'client-response', rpcId: 'known', result: { ok: true, value: null } })
|
||||
const goodReceipt: unknown = await (await handler.fetch(new Request('http://x/api/respond', { method: 'POST', body: good }))).json()
|
||||
const goodReceipt: unknown = await (await handler.fetch(new Request('http://x/api/respond', { method: 'POST', headers: { 'content-type': 'application/json' }, body: good }))).json()
|
||||
expect(goodReceipt).toEqual({ accepted: true })
|
||||
const bad = JSON.stringify({ type: 'client-request', rpcId: 'r', method: 'x', payload: {} })
|
||||
const badReceipt: unknown = await (await handler.fetch(new Request('http://x/api/respond', { method: 'POST', body: bad }))).json()
|
||||
const badReceipt: unknown = await (await handler.fetch(new Request('http://x/api/respond', { method: 'POST', headers: { 'content-type': 'application/json' }, body: bad }))).json()
|
||||
expect(badReceipt).toEqual({ accepted: false, reason: 'bad-response' })
|
||||
})
|
||||
|
||||
it('accepts (url, init) form fetch invocation', async () => {
|
||||
const body = JSON.stringify({ type: 'client-request', rpcId: 'r-12', method: 'session.list', payload: {} })
|
||||
const response = await handler.fetch('http://x/api/session.list', { method: 'POST', body })
|
||||
const response = await handler.fetch('http://x/api/session.list', { method: 'POST', headers: { 'content-type': 'application/json' }, body })
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user