Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/core-data-structures/core.zh.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/llm-streaming.md
#	docs/core-data-structures/llm-streaming.zh.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	docs/subsystems/attachment.i18n.yaml
#	docs/subsystems/attachment.md
#	docs/subsystems/attachment.zh.md
#	docs/subsystems/core.i18n.yaml
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/README.i18n.yaml
#	packages/README.md
#	packages/README.zh.md
#	packages/client/runtime/package.json
#	packages/client/ui-conversation/package.json
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/README.zh.md
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/index.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/src/index.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/self-modification/tool-cordis/src/api-catalog.ts
#	pnpm-lock.yaml
#	scripts/type-equiv.manifest.json
This commit is contained in:
Yichen Jiang
2026-08-09 23:33:35 +08:00
3271 changed files with 69578 additions and 24953 deletions

View File

@@ -548,6 +548,40 @@ describe('DeepSeekAdapter against a mock server', () => {
fetchSpy.mockRestore()
}
})
it('keeps an idle provider read alive through SSE comments', async () => {
vi.useFakeTimers()
const encoder = new TextEncoder()
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(() => {
const body = new ReadableStream<Uint8Array>({
start(controller) {
setTimeout(() => { controller.enqueue(encoder.encode(': keep-alive\n\n')) }, 75)
setTimeout(() => { controller.enqueue(encoder.encode(': keep-alive\n\n')) }, 150)
setTimeout(() => {
controller.enqueue(encoder.encode(textEvents.map(event => `data: ${event}\n\n`).join('')))
controller.close()
}, 225)
},
})
return Promise.resolve(new Response(body, { status: 200 }))
})
const adapter = adapterOf({ baseURL: 'https://example.invalid', streamIdleTimeoutMs: 100 })
try {
const chunks: string[] = []
const drain = (async () => {
for await (const chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) {
chunks.push(chunk.type)
}
})()
await vi.advanceTimersByTimeAsync(75)
await vi.advanceTimersByTimeAsync(75)
await vi.advanceTimersByTimeAsync(75)
await expect(drain).resolves.toBeUndefined()
expect(chunks).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish'])
} finally {
fetchSpy.mockRestore()
}
})
})
describe('plugin registration and config', () => {

View File

@@ -132,7 +132,7 @@ describe('llm-deepseek real dynamic composition', () => {
})
it('keeps a stored key writable and rotatable across a real restart', async () => {
// No ambient DEEPSEEK_API_KEY: the shipped surfaces no longer hoist
// No ambient DEEPSEEK_API_KEY: the shipped surfaces do not hoist
// the credentials document into process.env, so a stored key must stay file-sourced.
vi.stubEnv('DEEPSEEK_API_KEY', '')
const first = await mockServer([{ kind: 'sse', events: textEvents }])

View File

@@ -31,6 +31,16 @@ describe('parseSse', () => {
expect(events).toEqual(['{"a":1}', DONE])
})
it('reports comments out of band without yielding them', async () => {
const comments: string[] = []
const events = await collect(parseSse(
bytes(': keep-alive\n\ndata: {"a":1}\n\ndata: [DONE]\n\n'),
(comment) => { comments.push(comment) },
))
expect(comments).toEqual(['keep-alive'])
expect(events).toEqual(['{"a":1}', DONE])
})
it('stops yielding after DONE even when more data follows', async () => {
const events = await collect(parseSse(bytes('data: [DONE]\n\ndata: {"late":1}\n\n')))
expect(events).toEqual([DONE])