diff --git a/apps/web/package.json b/apps/web/package.json index 456b264094..6d4575aaf5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,7 @@ "@deepseek-ai/cordis-plugin-group": "workspace:^", "@deepseek-ai/dsh-client-modules": "workspace:^", "@deepseek-ai/dsh-client-store": "workspace:^", + "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-dockkit": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", @@ -41,6 +42,7 @@ "@deepseek-ai/dsh-pwsh-local": "workspace:^", "@deepseek-ai/dsh-experimental-webworker-packer": "workspace:^", "@deepseek-ai/dsh-experimental-webworker-runtime": "workspace:^", + "@deepseek-ai/dsh-remote-mock": "workspace:^", "@types/node": "^22.0.0", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts index a18893d82b..90acb73bec 100644 --- a/apps/web/tests/assembled-boot.ts +++ b/apps/web/tests/assembled-boot.ts @@ -1,6 +1,6 @@ // Shared scaffolding for the assembled-jsdom snapshots: the real built // workspace `lib/client.js` artifacts booted through AppWebEntry's -// ModuleLoader path (loadBundle) against the keyless fixture Connection RPC +// ModuleLoader path (loadBundle) against a test-owned RemoteMock carrier // transport. Every file that mounts this graph needs the same boot entry list, // the same bundle map, the same jsdom globals, and the same mount call, and // differs only in what it asserts afterwards, so the scaffolding lives here. @@ -15,7 +15,11 @@ import { act, cleanup } from '@testing-library/react' import { afterEach, beforeEach, vi } from 'vitest' import { bootInjections, orderByModuleGraph } from '@deepseek-ai/dsh-client-modules' import type { ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '@deepseek-ai/dsh-client-modules/client' +import type { RemoteMock } from '@deepseek-ai/dsh-remote-mock' import { AppWebEntry } from '@deepseek-ai/dsh-client-web' +import { + createAssembledRemote, type AssembledRemote, type AssembledRemoteOptions, +} from './assembled-remote.ts' interface AssembledPlugin extends WebBootEntry { /** Absolute path to the built client artifact declared by this package. */ @@ -25,6 +29,8 @@ interface AssembledPlugin extends WebBootEntry { interface AssembledBootOptions { /** Package ids omitted from this mounted composition. */ readonly exclude?: readonly string[] + /** Remote answers owned by this assembled case. */ + readonly remote?: AssembledRemoteOptions } interface ClientPackageManifest { @@ -174,6 +180,7 @@ function bundleTable(graph: WebBootGraph, plugins: readonly AssembledPlugin[]): interface FixtureWindow extends Window { __DSH_BOOT__?: WebBootGraph __ModuleLoader__?: ClientModuleLoaderTarget + __DSH_TRANSPORT__?: { readonly rpc: RemoteMock['rpc'] } } class ResizeObserverStub { @@ -189,6 +196,7 @@ class EventSourceStub { const win = window as FixtureWindow let unmount: (() => Promise) | undefined +let mountedRemote: RemoteMock | undefined /** * Register the per-test jsdom setup and teardown the assembled boot needs: @@ -214,9 +222,8 @@ export function installAssembledBootEnv(): void { beforeEach(() => { localStorage.clear() // The locale service derives its provisional locale from the browser and - // takes an explicit choice only from Host settings, which this lane's - // fixture transport does not serve; pinning the navigator is what selects - // English here. + // takes an explicit choice only from Host settings. This scenario serves no + // locale setting, so pinning the navigator selects English. Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true }) Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true }) document.title = 'DeepSeek Harness' @@ -228,11 +235,23 @@ export function installAssembledBootEnv(): void { }) afterEach(async () => { - await act(async () => { await unmount?.() }) + const failures: unknown[] = [] + try { + await act(async () => { await unmount?.() }) + } catch (error) { + failures.push(error) + } + try { + mountedRemote?.assertNoUnmatched() + } catch (error) { + failures.push(error) + } unmount = undefined + mountedRemote = undefined cleanup() delete win.__DSH_BOOT__ delete win.__ModuleLoader__ + delete win.__DSH_TRANSPORT__ document.body.innerHTML = '' document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() }) document.title = '' @@ -243,19 +262,23 @@ export function installAssembledBootEnv(): void { delete ownNavigator.languages delete ownNavigator.language vi.unstubAllGlobals() + if (failures.length > 0) throw new AggregateError(failures, 'assembled boot teardown failed') }) } /** - * Mount the assembled application on the fixture transport; the teardown + * Mount the assembled application on an isolated RemoteMock transport; the teardown * registered by installAssembledBootEnv disposes it. - * @param search - fixture query string used to select deterministic host behavior. * @param options - composition changes applied to this mount. + * @returns the test-owned RemoteMock world. */ -export function mountAssembledApp(search = '?fixture', options: AssembledBootOptions = {}): void { +export function mountAssembledApp(options: AssembledBootOptions = {}): AssembledRemote { const excluded = new Set(options.exclude) const plugins = PLUGINS.filter(plugin => !excluded.has(plugin.id)) - history.replaceState(null, '', `/${search}`) + const remote = createAssembledRemote(options.remote) + mountedRemote = remote.mock + win.__DSH_TRANSPORT__ = { rpc: remote.mock.rpc } + history.replaceState(null, '', '/') const root = document.createElement('div') root.id = 'root' document.body.appendChild(root) @@ -281,6 +304,7 @@ export function mountAssembledApp(search = '?fixture', options: AssembledBootOpt void entry.run() unmount = () => entry.dispose() }) + return remote } /** diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts new file mode 100644 index 0000000000..b0b44f9db1 --- /dev/null +++ b/apps/web/tests/assembled-remote.ts @@ -0,0 +1,376 @@ +/** RemoteMock scenario for built-client tests that do not own a Host. */ + +import { readFileSync } from 'node:fs' +import { randomUUID } from 'node:crypto' +import { join } from 'node:path' +import { ok, RemoteMock } from '@deepseek-ai/dsh-remote-mock' +import { remoteDefaultResponses } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/remote-default-responses.ts' + +interface SessionSummary { + readonly sessionId: string + updatedAt: number + running: boolean + blank: boolean + readonly parentSessionId?: string + readonly origin?: 'subagent' + readonly cwd?: string + readonly projections?: { + readonly asOfSeq: number + readonly values: Readonly> + } +} + +interface WorkspaceView { + readonly workspaceId: string + readonly path: string + readonly title: string + sessionIds: string[] + readonly createdAt: string + updatedAt: string +} + +interface EventRecord { + readonly type: 'event' + readonly event: { + readonly seq: number + readonly time: number + readonly type: string + readonly data: unknown + readonly surfaceOp?: string + } +} + +interface FollowSnapshot { + readonly type: 'snapshot' + readonly header: Readonly> + readonly cursor: number + readonly records: readonly EventRecord[] + readonly hasMore: boolean + readonly projections: Readonly> + readonly assistantStream?: Readonly> +} + +interface ControlBaseline { + readonly type: 'baseline' + readonly value: { + readonly queues: Readonly> + readonly jobs: Readonly> + readonly approvals: readonly unknown[] + readonly questions: readonly unknown[] + readonly projections: Readonly> + }>> + } +} + +interface CapturedFixture { + readonly sessionList: { readonly ok: true; readonly value: { readonly items: readonly SessionSummary[] } } + readonly settingsDescribe: unknown + readonly credentialsDescribe: unknown + readonly modelCatalog: unknown + readonly agentPresets: unknown + readonly commands: unknown + readonly workspace: { + readonly type: 'baseline' + readonly value: { + readonly items: readonly WorkspaceView[] + readonly archivedSessionIds: readonly string[] + } + } + readonly control: ControlBaseline + readonly remoteEvents: readonly unknown[] + readonly follow: FollowSnapshot + readonly attachment: unknown +} + +export interface AssembledRemoteOptions { + /** Return the fixture's image-dimension admission error from Session prompt. */ + readonly rejectPrompt?: boolean +} + +export interface AssembledRemote { + readonly mock: RemoteMock +} + +const fixtureSource = readFileSync( + join(process.cwd(), 'apps/web/tests/fixtures/assembled-remote.fixture.json'), + 'utf8', +) + +/** Create one isolated RemoteMock world for an assembled built-client case. */ +export function createAssembledRemote(options: AssembledRemoteOptions = {}): AssembledRemote { + const fixture = JSON.parse(fixtureSource) as CapturedFixture + const sessions = [...structuredClone(fixture.sessionList.value.items)] + const workspaces = [...structuredClone(fixture.workspace.value.items)] + const records = new Map([[ + 'fx-alpha', + structuredClone(fixture.follow.records) as EventRecord[], + ]]) + const attachments = new Map([['fixture:image', structuredClone(fixture.attachment)]]) + let nextSession = 1 + + const mock = RemoteMock.create({ host: { home: '/home/fixture' } }).load(remoteDefaultResponses) + mock.load({ + unary: { + 'settings/describe': structuredClone(fixture.settingsDescribe), + 'credentials/describe': structuredClone(fixture.credentialsDescribe), + 'session/modelCatalog': structuredClone(fixture.modelCatalog), + 'agentPresets/list': structuredClone(fixture.agentPresets), + 'commands/list': structuredClone(fixture.commands), + 'settings/canOpenAgentPresetDirectory': ok(true), + 'settings/openSettingsDocument': ok({ opened: true }), + 'settings/openAgentPresetDirectory': ok({ opened: true }), + 'subagents/list': ok({ entries: [], parentAvailable: true }), + 'skills/list': ok({ skills: [] }), + 'session/canOpenWorkspacePath': ok(true), + 'session/openWorkspacePath': ok({ opened: true }), + 'session/updateQueue': { + ok: false, + error: { + code: 'session/queue-item-not-found', + message: 'assembled fixture has no pending queue item', + details: {}, + }, + }, + 'session/cancel': ok({ accepted: true }), + }, + }) + + mock.stream('$events', (_args, stream) => { + for (const frame of fixture.remoteEvents) stream.push(structuredClone(frame)) + }) + mock.stream('session/control', (_args, stream) => { + stream.push(structuredClone(fixture.control)) + }) + mock.stream('workspace/follow', (_args, stream) => { + stream.push({ + type: 'baseline', + value: { + items: structuredClone(workspaces), + archivedSessionIds: structuredClone(fixture.workspace.value.archivedSessionIds), + }, + }) + }) + mock.stream('session/follow', ([args], stream) => { + const request = recordValue(args, 'request') + const sessionId = followedSessionId(request) + if (sessionId === 'fx-alpha') { + stream.push(structuredClone(fixture.follow)) + return + } + const summary = sessions.find(candidate => candidate.sessionId === sessionId) + if (summary === undefined) throw new Error(`assembled fixture: no Session ${sessionId}`) + const sessionRecords = records.get(sessionId) ?? [] + stream.push({ + type: 'snapshot', + header: { + version: 3, + id: sessionId, + createdAt: summary.updatedAt, + cwd: summary.cwd, + isSeeded: false, + }, + cursor: sessionRecords.at(-1)?.event.seq ?? -1, + records: structuredClone(sessionRecords), + hasMore: false, + projections: structuredClone(fixture.control.value.projections['fx-gamma'] ?? { + asOfSeq: -1, + values: {}, + }), + ...isAssistantStreamRequested(request) ? { assistantStream: { revision: 0 } } : {}, + }) + }) + + mock.unary('$events/result', (result: unknown) => { + const eventId = recordString(result, 'eventId') + mock.streams.push('$events', { type: 'cancel', eventId }) + return ok(undefined) + }) + mock.unary('session/list', () => ok({ items: structuredClone(sessions) })) + mock.unary('workspace/create', (request: unknown) => { + const path = recordString(recordValue(request, 'request'), 'path') + const existing = workspaces.find(workspace => workspace.path === path) + if (existing !== undefined) return ok({ workspace: structuredClone(existing), created: false }) + const now = new Date().toISOString() + const workspace = { + workspaceId: `fx-ws-${String(workspaces.length + 1)}`, + path, + title: path.split('/').filter(Boolean).at(-1) ?? path, + sessionIds: [], + createdAt: now, + updatedAt: now, + } + workspaces.unshift(workspace) + mock.streams.push('workspace/follow', { type: 'upsert', workspace: structuredClone(workspace) }) + return ok({ workspace: structuredClone(workspace), created: true }) + }) + mock.unary('session/create', (request: unknown) => { + request = recordValue(request, 'request') + const requestedId = optionalRecordString(request, 'sessionId') + const sessionId = requestedId ?? `fx-${String(nextSession++)}` + const existing = sessions.find(candidate => candidate.sessionId === sessionId) + if (existing !== undefined) return ok({ sessionId }) + const workspaceId = optionalRecordString(request, 'workspaceId') + const workspace = workspaces.find(candidate => candidate.workspaceId === workspaceId) + const cwd = workspace?.path ?? optionalRecordString(request, 'cwd') ?? '/tmp/fixture' + const summary: SessionSummary = { + sessionId, + updatedAt: Date.now(), + running: false, + blank: true, + cwd, + projections: structuredClone(fixture.control.value.projections['fx-gamma'] ?? { + asOfSeq: -1, + values: {}, + }), + } + sessions.push(summary) + records.set(sessionId, []) + if (workspace !== undefined && !workspace.sessionIds.includes(sessionId)) { + workspace.sessionIds = [sessionId, ...workspace.sessionIds] + workspace.updatedAt = new Date().toISOString() + mock.streams.push('workspace/follow', { + type: 'upsert', + workspace: structuredClone(workspace), + }) + } + mock.streams.push('$events', { type: 'emit', event: 'api-session/added', args: [structuredClone(summary)] }) + return ok({ sessionId }) + }) + mock.unary('session/attachment', (request: unknown) => { + request = recordValue(request, 'request') + const attachmentId = recordString(request, 'attachmentId') + return attachments.get(attachmentId) ?? { + ok: false, + error: { + code: 'session/attachment-invalid', + message: `assembled fixture attachment ${attachmentId} is missing`, + details: { reason: 'ATTACHMENT_NOT_FOUND' }, + }, + } + }) + mock.unary('session/prompt', (request: unknown) => { + request = recordValue(request, 'request') + if (options.rejectPrompt === true) { + return { + ok: false, + error: { + code: 'session/attachment-invalid', + message: 'assembled fixture: image side exceeds the deployment limit', + details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' }, + }, + } + } + const sessionId = recordString(request, 'sessionId') + const requestId = recordString(request, 'requestId') + const sessionRecords = records.get(sessionId) ?? [] + const content = recordArray(request, 'content').map((part) => { + if (!isRecord(part) || part.type !== 'image') return part + const attachmentId = `assembled:${randomUUID()}` + const data = recordString(part, 'data') + const attachment = { + attachmentId, + mediaType: recordString(part, 'mediaType'), + bytes: Math.max(1, Math.floor(data.length * 3 / 4)), + width: 160, + height: 90, + ...optionalRecordString(part, 'name') === undefined + ? {} + : { name: optionalRecordString(part, 'name') }, + } + attachments.set(attachmentId, ok({ attachment, data })) + return { type: 'image', attachment } + }) + const turn = sessionRecords.length + const turnEvent = eventOf(sessionRecords.length, 'turn/start', { turn }) + const userEvent = eventOf(sessionRecords.length + 1, 'user/message', { + content, + source: { kind: 'user', rpcId: requestId }, + role: 'user', + id: randomUUID(), + }, 'append') + sessionRecords.push(turnEvent, userEvent) + records.set(sessionId, sessionRecords) + mock.streams.push('session/follow', turnEvent, follows(sessionId)) + mock.streams.push('session/follow', userEvent, follows(sessionId)) + return ok({ accepted: true }) + }) + mock.unary('commands/execute', (args: unknown) => { + const line = optionalRecordString(args, 'line') ?? '' + const name = /^\/(\S+)/u.exec(line.trim())?.[1] + if (name === undefined || !['compact', 'echo', 'goal', 'permission', 'plan'].includes(name)) { + return ok(undefined) + } + return ok({ + commandId: `assembled-command-${randomUUID()}`, + result: { + kind: 'success', + ...name === 'echo' ? { text: line.replace(/^\/echo\s*/u, '') } : {}, + }, + }) + }) + + return { mock } +} + +function eventOf( + seq: number, + type: string, + data: unknown, + surfaceOp?: string, +): EventRecord { + return { + type: 'event', + event: { + seq, + time: Date.now(), + type, + data, + ...(surfaceOp === undefined ? {} : { surfaceOp }), + }, + } +} + +function follows(sessionId: string): (args: readonly unknown[]) => boolean { + return ([args]) => followedSessionId(recordValue(args, 'request')) === sessionId +} + +function followedSessionId(value: unknown): string { + if (!isRecord(value) || !isRecord(value.address)) throw new TypeError('assembled fixture follow request is invalid') + return value.address.kind === 'session' + ? recordString(value.address, 'sessionId') + : recordString(value.address, 'childSessionId') +} + +function isAssistantStreamRequested(value: unknown): boolean { + return isRecord(value) && value.assistantStream === true +} + +function recordString(value: unknown, key: string): string { + const selected = isRecord(value) ? value[key] : undefined + if (typeof selected !== 'string') throw new TypeError(`assembled fixture ${key} must be a string`) + return selected +} + +function optionalRecordString(value: unknown, key: string): string | undefined { + const selected = isRecord(value) ? value[key] : undefined + if (selected === undefined) return undefined + if (typeof selected !== 'string') throw new TypeError(`assembled fixture ${key} must be a string`) + return selected +} + +function recordArray(value: unknown, key: string): readonly unknown[] { + const selected = isRecord(value) ? value[key] : undefined + if (!Array.isArray(selected)) throw new TypeError(`assembled fixture ${key} must be an array`) + return selected +} + +function recordValue(value: unknown, key: string): unknown { + if (!isRecord(value) || !(key in value)) throw new TypeError(`assembled fixture ${key} is missing`) + return value[key] +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} diff --git a/apps/web/tests/built-boot.expected.e2e.ts b/apps/web/tests/built-boot.expected.e2e.ts index e30ac22630..17a741dac4 100644 --- a/apps/web/tests/built-boot.expected.e2e.ts +++ b/apps/web/tests/built-boot.expected.e2e.ts @@ -4,7 +4,7 @@ // reach a surface only the built bundles expose; this one asserts that the // graph assembles at all — staged activation across the immediately tier and // the inject layers, per-plugin CSS injection, and a rendered journey reaching -// chat content from the keyless fixture Connection RPC. +// chat content from the keyless RemoteMock scenario. // // Component behavior remains owned by per-package suites (SlotTestRuntime // benches over src). This smoke additionally pins the resident interaction @@ -92,7 +92,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn expect(waitingRow.querySelector('[data-state="ongoing"]')).toBeNull() within(waitingRow).getByText('Waiting for answer') - // Opening a session reaches chat content through the fixture transport. + // Opening a session reaches chat content through the RemoteMock transport. fireEvent.click(waitingTitle) await waitFor(() => { expect(document.querySelector('[data-sample="bash"]')).not.toBeNull() @@ -162,7 +162,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn }) it('boots without ui-chat and does not select another conversation view implicitly', async () => { - mountAssembledApp('?fixture', { exclude: ['@deepseek-ai/dsh-client-ui-chat'] }) + mountAssembledApp({ exclude: ['@deepseek-ai/dsh-client-ui-chat'] }) const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) const boot = Reflect.get(window, '__DSH_BOOT__') as { entries: Array<{ id: string }> } | undefined diff --git a/apps/web/tests/command-image-envelope.expected.e2e.ts b/apps/web/tests/command-image-envelope.expected.e2e.ts index 565eaa3524..ef288f5818 100644 --- a/apps/web/tests/command-image-envelope.expected.e2e.ts +++ b/apps/web/tests/command-image-envelope.expected.e2e.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom // The command attachment envelope over the BUILT client graph (real -// bundles via AppWebEntry, keyless fixture Connection RPC): an enter +// bundles via AppWebEntry, keyless RemoteMock transport): an enter // submission carrying composer attachments resolves only through a command whose // descriptor declares `input.attachments`. A non-declaring command refuses with // one composer error banner and everything retained; a declaring command diff --git a/apps/web/tests/fixtures/assembled-remote.fixture.json b/apps/web/tests/fixtures/assembled-remote.fixture.json new file mode 100644 index 0000000000..34d3112dfe --- /dev/null +++ b/apps/web/tests/fixtures/assembled-remote.fixture.json @@ -0,0 +1,5859 @@ +{ + "sessionList": { + "ok": true, + "value": { + "items": [ + { + "sessionId": "fx-alpha", + "updatedAt": 1789268875874, + "running": true, + "blank": false, + "cwd": "/tmp/fixture" + }, + { + "sessionId": "fx-beta", + "updatedAt": 1789268815874, + "running": false, + "blank": false, + "parentSessionId": "fx-alpha", + "cwd": "/tmp/fixture" + }, + { + "sessionId": "fx-gamma", + "updatedAt": 1789268755874, + "running": false, + "blank": false, + "cwd": "/tmp/fixture" + } + ] + } + }, + "settingsDescribe": { + "ok": true, + "value": { + "writable": true, + "hasDocument": true, + "namespaces": [ + { + "ns": "llm-deepseek", + "schema": {}, + "value": { + "apiKeyEnv": "DEEPSEEK_API_KEY" + }, + "applies": "live", + "secrets": [ + { + "path": [ + "apiKey" + ], + "set": false + } + ], + "revision": 0 + } + ] + } + }, + "credentialsDescribe": { + "ok": true, + "value": { + "DEEPSEEK_API_KEY": { + "configured": true, + "source": "file", + "writable": true + } + } + }, + "modelCatalog": { + "ok": true, + "value": { + "default": { + "provider": "deepseek-official", + "model": "deepseek-v4-flash" + }, + "routableProviders": [ + "deepseek-official", + "openai", + "acme-gateway" + ], + "groups": [ + { + "id": "deepseek-official", + "name": "DeepSeek", + "models": [ + { + "id": "deepseek-v4-flash", + "name": "DeepSeek-V4-Flash", + "description": "快速响应", + "reasoning": { + "efforts": [ + { + "id": "off", + "name": "Off" + }, + { + "id": "high", + "name": "High" + }, + { + "id": "max", + "name": "Max" + } + ], + "defaultEffort": "high" + } + }, + { + "id": "deepseek-v4-pro", + "name": "DeepSeek-V4-Pro", + "description": "复杂任务", + "reasoning": { + "efforts": [ + { + "id": "off", + "name": "Off" + }, + { + "id": "high", + "name": "High" + }, + { + "id": "max", + "name": "Max" + } + ], + "defaultEffort": "high" + } + } + ] + }, + { + "id": "openai", + "name": "OpenAI", + "models": [ + { + "id": "gpt-5", + "name": "GPT-5", + "reasoning": { + "efforts": [ + { + "id": "off", + "name": "Off" + }, + { + "id": "medium", + "name": "Medium" + }, + { + "id": "high", + "name": "High" + }, + { + "id": "max", + "name": "Max" + } + ], + "defaultEffort": "medium" + } + } + ] + } + ], + "failures": [] + } + }, + "agentPresets": { + "ok": true, + "value": { + "presets": [ + { + "id": "standard", + "trust": "system", + "isDefault": true + }, + { + "id": "minimal", + "trust": "system", + "isDefault": false + }, + { + "id": "my-agent", + "trust": "user", + "isDefault": false + } + ], + "authorable": true, + "modeSelectionEnabled": true + } + }, + "commands": { + "ok": true, + "value": [ + { + "name": "compact", + "description": "fixture:压缩当前会话上下文" + }, + { + "name": "echo", + "description": "fixture:回显参数", + "input": { + "hint": "text to echo" + } + }, + { + "definitionId": "@deepseek-ai/dsh-command-goal", + "name": "goal", + "description": "Set or view the goal for a long-running task", + "input": { + "hint": "", + "attachments": true + } + }, + { + "definitionId": "@deepseek-ai/dsh-permission-presets", + "name": "permission", + "description": "Switch the permission preset (sandbox mode + approval policy)", + "input": { + "hint": "" + } + }, + { + "definitionId": "@deepseek-ai/dsh-plan-mode", + "name": "plan", + "description": "Enter or leave plan mode", + "input": { + "hint": "[off|message]", + "attachments": true + } + } + ] + }, + "workspace": { + "type": "baseline", + "value": { + "items": [ + { + "workspaceId": "fx-ws-fixture", + "path": "/tmp/fixture", + "title": "fixture", + "sessionIds": [ + "fx-alpha", + "fx-beta", + "fx-gamma" + ], + "createdAt": "2026-09-13T03:02:55.884Z", + "updatedAt": "2026-09-13T03:02:55.884Z" + }, + { + "workspaceId": "fx-ws-home", + "path": "/home/fixture/Documents/project", + "title": "project", + "sessionIds": [], + "createdAt": "2026-09-13T03:02:55.884Z", + "updatedAt": "2026-09-13T03:02:55.884Z" + } + ], + "archivedSessionIds": [] + } + }, + "control": { + "type": "baseline", + "value": { + "queues": { + "fx-alpha": [], + "fx-beta": [], + "fx-gamma": [] + }, + "jobs": { + "fx-alpha": [], + "fx-beta": [], + "fx-gamma": [] + }, + "approvals": [], + "questions": [], + "projections": { + "fx-alpha": { + "asOfSeq": 552, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "title": "Fixture 历史会话", + "todos": [ + { + "content": "梳理需求", + "status": "completed" + }, + { + "content": "实现 fixture 样本", + "status": "in_progress" + }, + { + "content": "跑后台构建", + "status": "in_progress" + }, + { + "content": "浏览器验收", + "status": "pending" + } + ], + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 1914, + "outputTokens": 708, + "cacheReadTokens": 6880, + "cacheWriteTokens": 32 + }, + "contextPressure": { + "pressureTokens": 104, + "contextWindow": 128000 + }, + "contextBreakdown": { + "systemTokens": 17, + "toolsTokens": 0, + "messageTokens": 4427 + }, + "sessionStats": { + "turns": 75, + "steps": 87, + "llmMs": 69600, + "toolMs": 24800, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + }, + "fx-beta": { + "asOfSeq": -1, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "todos": null, + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0 + }, + "contextPressure": {}, + "contextBreakdown": { + "systemTokens": 0, + "toolsTokens": 0, + "messageTokens": 0 + }, + "sessionStats": { + "turns": 0, + "steps": 0, + "llmMs": 0, + "toolMs": 0, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + }, + "fx-gamma": { + "asOfSeq": -1, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "todos": null, + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0 + }, + "contextPressure": {}, + "contextBreakdown": { + "systemTokens": 0, + "toolsTokens": 0, + "messageTokens": 0 + }, + "sessionStats": { + "turns": 0, + "steps": 0, + "llmMs": 0, + "toolMs": 0, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + } + } + } + }, + "remoteEvents": [ + { + "type": "ready", + "clientId": "36b204ff-b72f-4720-b58a-c86bebca8c42", + "host": { + "home": "/home/fixture" + } + }, + { + "type": "waterfall", + "event": "approval/request", + "eventId": "fx-interaction-approval", + "agentId": "fx-alpha", + "request": { + "toolName": "dangerous_tool", + "reason": "fixture 常驻审批(可答:批准/拒绝后消失)" + } + }, + { + "type": "waterfall", + "event": "user-questions/request", + "eventId": "fx-interaction-question", + "agentId": "fx-alpha", + "request": { + "questions": [ + { + "id": "harness-profile", + "header": "偏好", + "question": "你现在更想招哪类 Agent/Harness 候选人?", + "options": [ + { + "label": "工程落地型 (Recommended)", + "description": "更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。" + }, + { + "label": "研究潜力型", + "description": "更看重 Agent 理解、训练评测思路和长期成长空间。" + }, + { + "label": "均衡型", + "description": "同时要求工程能力和 Agent 认知,但可能筛选门槛更高。" + } + ] + }, + { + "id": "work-mode", + "header": "方式", + "question": "你希望候选人优先展示哪种工作方式?", + "options": [ + { + "label": "先做小型原型 (Recommended)", + "description": "用可运行结果尽快验证关键假设。" + }, + { + "label": "先写完整设计", + "description": "先收敛边界、协议和风险,再开始实现。" + } + ] + }, + { + "id": "signals", + "header": "信号", + "question": "哪些面试信号最重要?", + "detail": "按当前招聘目标选择;跳过则视为不设偏好。", + "multiSelect": true, + "options": [ + { + "label": "系统设计" + }, + { + "label": "代码质量" + }, + { + "label": "Agent 产品判断" + } + ] + } + ] + } + } + ], + "follow": { + "type": "snapshot", + "header": { + "version": 3, + "id": "fx-alpha", + "createdAt": 1789268875874, + "cwd": "/tmp/fixture", + "isSeeded": false + }, + "cursor": 552, + "records": [ + { + "type": "event", + "event": { + "seq": 365, + "time": 1789265568675, + "type": "turn/start", + "data": { + "turn": 51 + } + } + }, + { + "type": "event", + "event": { + "seq": 366, + "time": 1789265569475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 51:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "781d1628-a401-4577-9608-fad94469115f" + } + } + }, + { + "type": "event", + "event": { + "seq": 367, + "time": 1789265570275, + "type": "step/start", + "data": { + "turn": 51, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 368, + "time": 1789265571075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 51, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 51:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "41db22fd-d174-446f-8d4f-dd0769071b92" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 51:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265571075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 369, + "time": 1789265571875, + "type": "step/end", + "data": { + "turn": 51, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 370, + "time": 1789265572675, + "type": "turn/end", + "data": { + "turn": 51, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 371, + "time": 1789265573475, + "type": "turn/start", + "data": { + "turn": 52 + } + } + }, + { + "type": "event", + "event": { + "seq": 372, + "time": 1789265574275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 52:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "ca2e9326-9086-477e-b80d-32005b85cbc6" + } + } + }, + { + "type": "event", + "event": { + "seq": 373, + "time": 1789265575075, + "type": "step/start", + "data": { + "turn": 52, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 374, + "time": 1789265575875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 52, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "reasoning", + "text": "思考过程 52:这是一段可折叠的 reasoning 内容。" + }, + { + "type": "text", + "text": "回答 52:这是 fixture 生成的历史回复正文。" + }, + { + "type": "tool-call", + "id": "fx-call-52", + "name": "echo", + "arguments": "{\"text\":\"turn 52\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "38c0ab81-0fc5-401d-a876-f0d1e766de03" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "reasoning" + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "reasoning", + "text": "思考过程 52:这是一段可折叠的 reasoning 内容。" + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "text", + "text": "回答 52:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-start", + "index": 2, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "block-end", + "index": 2, + "block": { + "type": "tool-call", + "id": "fx-call-52", + "name": "echo", + "arguments": "{\"text\":\"turn 52\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265575875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 375, + "time": 1789265576675, + "type": "tool/call", + "data": { + "turn": 52, + "step": 0, + "callId": "fx-call-52", + "name": "echo", + "arguments": "{\"text\":\"turn 52\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 376, + "time": 1789265577475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 52, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-52" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-52", + "content": [ + { + "type": "text", + "text": "ECHO: TURN 52" + } + ], + "isError": false + } + ], + "role": "user", + "id": "e9bbfe6c-96f3-44b6-b65c-89997783be1e" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 377, + "time": 1789265578275, + "type": "step/end", + "data": { + "turn": 52, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 378, + "time": 1789265579075, + "type": "step/start", + "data": { + "turn": 52, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 379, + "time": 1789265579875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 52, + "step": 1, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "工具结果已消化(turn 52)。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "891ea3be-04ca-4adc-a1b5-70c311b5b0fd" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "工具结果已消化(turn 52)。" + } + } + }, + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265579875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 380, + "time": 1789265580675, + "type": "step/end", + "data": { + "turn": 52, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 381, + "time": 1789265581475, + "type": "turn/end", + "data": { + "turn": 52, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 382, + "time": 1789265582275, + "type": "turn/start", + "data": { + "turn": 53 + } + } + }, + { + "type": "event", + "event": { + "seq": 383, + "time": 1789265583075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 53:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "c26142ac-056b-47cb-bf77-6e554e565484" + } + } + }, + { + "type": "event", + "event": { + "seq": 384, + "time": 1789265583875, + "type": "step/start", + "data": { + "turn": 53, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 385, + "time": 1789265584675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 53, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 53:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "cc41f0c0-2a74-4bc9-b5a2-f02f4c455569" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 53:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265584675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 386, + "time": 1789265585475, + "type": "step/end", + "data": { + "turn": 53, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 387, + "time": 1789265586275, + "type": "turn/end", + "data": { + "turn": 53, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 388, + "time": 1789265587075, + "type": "turn/start", + "data": { + "turn": 54 + } + } + }, + { + "type": "event", + "event": { + "seq": 389, + "time": 1789265587875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 54:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "eadaefc3-d83e-471e-ad64-8df6ae40c1eb" + } + } + }, + { + "type": "event", + "event": { + "seq": 390, + "time": 1789265588675, + "type": "step/start", + "data": { + "turn": 54, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 391, + "time": 1789265589475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 54, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 54:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "b8f5e883-ce1f-42ef-8106-dfa130620801" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 54:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265589475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 392, + "time": 1789265590275, + "type": "step/end", + "data": { + "turn": 54, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 393, + "time": 1789265591075, + "type": "turn/end", + "data": { + "turn": 54, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 394, + "time": 1789265591875, + "type": "turn/start", + "data": { + "turn": 55 + } + } + }, + { + "type": "event", + "event": { + "seq": 395, + "time": 1789265592675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 55:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "116535cf-1160-4aff-bd8f-f89e4fc6c41f" + } + } + }, + { + "type": "event", + "event": { + "seq": 396, + "time": 1789265593475, + "type": "step/start", + "data": { + "turn": 55, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 397, + "time": 1789265594275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 55, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "reasoning", + "text": "思考过程 55:这是一段可折叠的 reasoning 内容。" + }, + { + "type": "text", + "text": "回答 55:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "6a80e9b4-ca99-40ef-8de8-facbc8c27b76" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "reasoning" + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "reasoning", + "text": "思考过程 55:这是一段可折叠的 reasoning 内容。" + } + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "text", + "text": "回答 55:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265594275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 398, + "time": 1789265595075, + "type": "step/end", + "data": { + "turn": 55, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 399, + "time": 1789265595875, + "type": "turn/end", + "data": { + "turn": 55, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 400, + "time": 1789265596675, + "type": "turn/start", + "data": { + "turn": 56 + } + } + }, + { + "type": "event", + "event": { + "seq": 401, + "time": 1789265597475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 56:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "093791f8-cb5b-4106-853d-306ed532feec" + } + } + }, + { + "type": "event", + "event": { + "seq": 402, + "time": 1789265598275, + "type": "step/start", + "data": { + "turn": 56, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 403, + "time": 1789265599075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 56, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 56:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "bc0ebb05-d212-4f30-a3eb-c4c209ff7103" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 56:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265599075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 404, + "time": 1789265599875, + "type": "step/end", + "data": { + "turn": 56, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 405, + "time": 1789265600675, + "type": "turn/end", + "data": { + "turn": 56, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 406, + "time": 1789265601475, + "type": "turn/start", + "data": { + "turn": 57 + } + } + }, + { + "type": "event", + "event": { + "seq": 407, + "time": 1789265602275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 57:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "4196af12-5f06-4244-a83e-281b0e2e2565" + } + } + }, + { + "type": "event", + "event": { + "seq": 408, + "time": 1789265603075, + "type": "step/start", + "data": { + "turn": 57, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 409, + "time": 1789265603875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 57, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "回答 57:这是 fixture 生成的历史回复正文。" + }, + { + "type": "tool-call", + "id": "fx-call-57", + "name": "echo", + "arguments": "{\"text\":\"turn 57\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "dc114329-e3f4-43a4-bbd7-3524203a99e9" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "回答 57:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "tool-call", + "id": "fx-call-57", + "name": "echo", + "arguments": "{\"text\":\"turn 57\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265603875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 410, + "time": 1789265604675, + "type": "tool/call", + "data": { + "turn": 57, + "step": 0, + "callId": "fx-call-57", + "name": "echo", + "arguments": "{\"text\":\"turn 57\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 411, + "time": 1789265605475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 57, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-57" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-57", + "content": [ + { + "type": "text", + "text": "ECHO: TURN 57" + } + ], + "isError": false + } + ], + "role": "user", + "id": "652931ce-6eb0-4400-8173-9735b82fdf8e" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 412, + "time": 1789265606275, + "type": "step/end", + "data": { + "turn": 57, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 413, + "time": 1789265607075, + "type": "step/start", + "data": { + "turn": 57, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 414, + "time": 1789265607875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 57, + "step": 1, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "工具结果已消化(turn 57)。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "02030b82-e0d9-4e8f-a17c-bfeaa78a74d0" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "工具结果已消化(turn 57)。" + } + } + }, + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 9, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265607875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 415, + "time": 1789265608675, + "type": "step/end", + "data": { + "turn": 57, + "step": 1 + } + } + }, + { + "type": "event", + "event": { + "seq": 416, + "time": 1789265609475, + "type": "turn/end", + "data": { + "turn": 57, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 417, + "time": 1789265610275, + "type": "turn/start", + "data": { + "turn": 58 + } + } + }, + { + "type": "event", + "event": { + "seq": 418, + "time": 1789265611075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 58:fixture 历史消息,用于翻页与渲染验收。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "d08478d2-30ac-49fa-b3ca-f71e8d4517ee" + } + } + }, + { + "type": "event", + "event": { + "seq": 419, + "time": 1789265611875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "[fixture] 上下文注入(turn 58)" + } + ], + "source": { + "kind": "plugin", + "plugin": "fixture" + }, + "role": "user", + "id": "cbb94cfa-48f7-453b-9825-e61432572399" + } + } + }, + { + "type": "event", + "event": { + "seq": 420, + "time": 1789265612675, + "type": "step/start", + "data": { + "turn": 58, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 421, + "time": 1789265613475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 58, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "reasoning", + "text": "思考过程 58:这是一段可折叠的 reasoning 内容。" + }, + { + "type": "text", + "text": "回答 58:这是 fixture 生成的历史回复正文。" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "271e5020-8deb-418e-a4c2-d6609f31aa60" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "reasoning" + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "reasoning", + "text": "思考过程 58:这是一段可折叠的 reasoning 内容。" + } + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "text", + "text": "回答 58:这是 fixture 生成的历史回复正文。" + } + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265613475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 422, + "time": 1789265614275, + "type": "step/end", + "data": { + "turn": 58, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 423, + "time": 1789265615075, + "type": "turn/end", + "data": { + "turn": 58, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 424, + "time": 1789265615875, + "type": "turn/start", + "data": { + "turn": 59 + } + } + }, + { + "type": "event", + "event": { + "seq": 425, + "time": 1789265616675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "用户字面量:# 不渲染 `code` [link](https://example.com)" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "14861e74-05a9-4e2b-b98a-c907999daa42" + } + } + }, + { + "type": "event", + "event": { + "seq": 426, + "time": 1789265617475, + "type": "step/start", + "data": { + "turn": 59, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 427, + "time": 1789265618275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 59, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "# Markdown fixture\n\nAssistant output renders **strong text**, *emphasis*, and `inline code`.\n\n- first item\n - nested item\n\n| Area | State |\n| --- | --- |\n| history | rendered |\n| streaming | stable |\n\n[DeepSeek](https://www.deepseek.com)\n\n```ts\nconst markdown = true\n```" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "07a55bdb-ff08-40b3-8922-0422a2f04a87" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "# Markdown fixture\n\nAssistant output renders **strong text**, *emphasis*, and `inline code`.\n\n- first item\n - nested item\n\n| Area | State |\n| --- | --- |\n| history | rendered |\n| streaming | stable |\n\n[DeepSeek](https://www.deepseek.com)\n\n```ts\nconst markdown = true\n```" + } + } + }, + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265618275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 428, + "time": 1789265619075, + "type": "step/end", + "data": { + "turn": 59, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 429, + "time": 1789265619875, + "type": "turn/end", + "data": { + "turn": 59, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 430, + "time": 1789265620675, + "type": "turn/start", + "data": { + "turn": 60 + } + } + }, + { + "type": "event", + "event": { + "seq": 431, + "time": 1789265621475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 60:bash 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "de206a9a-a621-435f-88f0-9613ff152eb5" + } + } + }, + { + "type": "event", + "event": { + "seq": 432, + "time": 1789265622275, + "type": "step/start", + "data": { + "turn": 60, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 433, + "time": 1789265623075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 60, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-60", + "name": "bash", + "arguments": "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "4d338006-4348-4002-8a03-c730802f6f51" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-60", + "name": "bash", + "arguments": "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + } + } + }, + { + "type": "chunk", + "time": 1789265623075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 434, + "time": 1789265623875, + "type": "tool/call", + "data": { + "turn": 60, + "step": 0, + "callId": "fx-call-60", + "name": "bash", + "arguments": "{\"command\":\"ls -la\\necho done\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 435, + "time": 1789265624675, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 60, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-60" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-60", + "content": [ + { + "type": "text", + "text": "total 2\ndrwxr-xr-x fixture\n-rw-r--r-- demo.txt" + } + ], + "isError": false + } + ], + "role": "user", + "id": "aa4c1898-7bec-4831-8ba1-0700f4ae897e" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 436, + "time": 1789265625475, + "type": "step/end", + "data": { + "turn": 60, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 437, + "time": 1789265626275, + "type": "turn/end", + "data": { + "turn": 60, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 438, + "time": 1789265627075, + "type": "turn/start", + "data": { + "turn": 61 + } + } + }, + { + "type": "event", + "event": { + "seq": 439, + "time": 1789265627875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 61:write 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "6fd7a2c3-2ead-41a0-b954-21883b4bc03d" + } + } + }, + { + "type": "event", + "event": { + "seq": 440, + "time": 1789265628675, + "type": "step/start", + "data": { + "turn": 61, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 441, + "time": 1789265629475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 61, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-61", + "name": "write", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "a3c1b7d4-200f-49b7-b550-bbc5ad98dbc9" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-61", + "name": "write", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265629475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 442, + "time": 1789265630275, + "type": "tool/call", + "data": { + "turn": 61, + "step": 0, + "callId": "fx-call-61", + "name": "write", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 443, + "time": 1789265631075, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 61, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-61" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-61", + "content": [ + { + "type": "text", + "text": "wrote notes/demo.txt" + } + ], + "isError": false + } + ], + "role": "user", + "id": "4eedd943-773b-43d3-a104-e13550015670" + }, + "meta": { + "diffs": [ + { + "path": "notes/demo.txt", + "oldText": null, + "newText": "hello fixture\n" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 444, + "time": 1789265631875, + "type": "step/end", + "data": { + "turn": 61, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 445, + "time": 1789265632675, + "type": "turn/end", + "data": { + "turn": 61, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 446, + "time": 1789265633475, + "type": "turn/start", + "data": { + "turn": 62 + } + } + }, + { + "type": "event", + "event": { + "seq": 447, + "time": 1789265634275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 62:edit 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "8f4e6682-2963-4daa-a912-84ff053ac32e" + } + } + }, + { + "type": "event", + "event": { + "seq": 448, + "time": 1789265635075, + "type": "step/start", + "data": { + "turn": 62, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 449, + "time": 1789265635875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 62, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-62", + "name": "edit", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "f1b3b777-d4ec-4b33-80ac-fa4e9d87885a" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-62", + "name": "edit", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265635875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 450, + "time": 1789265636675, + "type": "tool/call", + "data": { + "turn": 62, + "step": 0, + "callId": "fx-call-62", + "name": "edit", + "arguments": "{\"file_path\":\"notes/demo.txt\",\"old_string\":\"hello\",\"new_string\":\"hello fixture\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 451, + "time": 1789265637475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 62, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-62" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-62", + "content": [ + { + "type": "text", + "text": "已编辑" + } + ], + "isError": false + } + ], + "role": "user", + "id": "37d72542-2cbc-4821-8897-329da97fe462" + }, + "meta": { + "diffs": [ + { + "path": "notes/demo.txt", + "oldText": "hello", + "newText": "hello fixture" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 452, + "time": 1789265638275, + "type": "step/end", + "data": { + "turn": 62, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 453, + "time": 1789265639075, + "type": "turn/end", + "data": { + "turn": 62, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 454, + "time": 1789265639875, + "type": "turn/start", + "data": { + "turn": 63 + } + } + }, + { + "type": "event", + "event": { + "seq": 455, + "time": 1789265640675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 63:write 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "8e09ed22-fb26-4b3a-8ef5-1e2ac363e0f8" + } + } + }, + { + "type": "event", + "event": { + "seq": 456, + "time": 1789265641475, + "type": "step/start", + "data": { + "turn": 63, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 457, + "time": 1789265642275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 63, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-63", + "name": "write", + "arguments": "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "65f7aa11-c436-4b13-8715-63437a194d29" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-63", + "name": "write", + "arguments": "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265642275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 458, + "time": 1789265643075, + "type": "tool/call", + "data": { + "turn": 63, + "step": 0, + "callId": "fx-call-63", + "name": "write", + "arguments": "{\"file_path\":\"notes/new-demo.txt\",\"content\":\"hello fixture\\n\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 459, + "time": 1789265643875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 63, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-63" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-63", + "content": [ + { + "type": "text", + "text": "已写入" + } + ], + "isError": false + } + ], + "role": "user", + "id": "0d0c76cf-f5b2-4f77-97cb-b0b66a67d135" + }, + "meta": { + "diffs": [ + { + "path": "notes/new-demo.txt", + "oldText": null, + "newText": "hello fixture\n" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 460, + "time": 1789265644675, + "type": "step/end", + "data": { + "turn": 63, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 461, + "time": 1789265645475, + "type": "turn/end", + "data": { + "turn": 63, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 462, + "time": 1789265646275, + "type": "turn/start", + "data": { + "turn": 64 + } + } + }, + { + "type": "event", + "event": { + "seq": 463, + "time": 1789265647075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 64:edit 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "99fcaad3-6e35-4e4f-a359-f97c673c72a2" + } + } + }, + { + "type": "event", + "event": { + "seq": 464, + "time": 1789265647875, + "type": "step/start", + "data": { + "turn": 64, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 465, + "time": 1789265648675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 64, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-64", + "name": "edit", + "arguments": "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "e0ae34f7-02f8-4093-a01b-e92d87fcbc3c" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-64", + "name": "edit", + "arguments": "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265648675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 466, + "time": 1789265649475, + "type": "tool/call", + "data": { + "turn": 64, + "step": 0, + "callId": "fx-call-64", + "name": "edit", + "arguments": "{\"file_path\":\"src/config.ts\",\"old_string\":\"const timeout = 30\",\"new_string\":\"const timeout = 60\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 467, + "time": 1789265650275, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 64, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-64" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-64", + "content": [ + { + "type": "text", + "text": "已编辑" + } + ], + "isError": false + } + ], + "role": "user", + "id": "11a489c7-466c-45ba-a5de-228fecd455f2" + }, + "meta": { + "diffs": [ + { + "path": "src/config.ts", + "oldText": "const timeout = 30", + "newText": "const timeout = 60" + }, + { + "path": "src/config.ts", + "oldText": "retries: 1", + "newText": "retries: 3" + } + ] + } + } + } + }, + { + "type": "event", + "event": { + "seq": 468, + "time": 1789265651075, + "type": "step/end", + "data": { + "turn": 64, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 469, + "time": 1789265651875, + "type": "turn/end", + "data": { + "turn": 64, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 470, + "time": 1789265652675, + "type": "turn/start", + "data": { + "turn": 65 + } + } + }, + { + "type": "event", + "event": { + "seq": 471, + "time": 1789265653475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 65:run_code 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "350becdf-2087-439b-be5e-cb19abf1dfff" + } + } + }, + { + "type": "event", + "event": { + "seq": 472, + "time": 1789265654275, + "type": "step/start", + "data": { + "turn": 65, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 473, + "time": 1789265655075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 65, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-65", + "name": "run_code", + "arguments": "{\"code\":\"const listing = await tools.bash({ command: \\\"ls notes\\\", description: \\\"List notes\\\" })\\nconst demo = await tools.read({ file_path: \\\"notes/demo.txt\\\" })\\nawait tools.read({ file_path: \\\"notes/missing.txt\\\" }).catch(() => \\\"tolerated\\\")\\nreturn { listing, demo }\",\"description\":\"Read the notes files and summarize\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "4af251a1-bd9f-4e7a-a7e8-9d4ac6f0f8e5" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-65", + "name": "run_code", + "arguments": "{\"code\":\"const listing = await tools.bash({ command: \\\"ls notes\\\", description: \\\"List notes\\\" })\\nconst demo = await tools.read({ file_path: \\\"notes/demo.txt\\\" })\\nawait tools.read({ file_path: \\\"notes/missing.txt\\\" }).catch(() => \\\"tolerated\\\")\\nreturn { listing, demo }\",\"description\":\"Read the notes files and summarize\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265655075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 474, + "time": 1789265655875, + "type": "tool/call", + "data": { + "turn": 65, + "step": 0, + "callId": "fx-call-65", + "name": "run_code", + "arguments": "{\"code\":\"const listing = await tools.bash({ command: \\\"ls notes\\\", description: \\\"List notes\\\" })\\nconst demo = await tools.read({ file_path: \\\"notes/demo.txt\\\" })\\nawait tools.read({ file_path: \\\"notes/missing.txt\\\" }).catch(() => \\\"tolerated\\\")\\nreturn { listing, demo }\",\"description\":\"Read the notes files and summarize\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 475, + "time": 1789265656675, + "type": "tool/ptc-dispatch-start", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:1", + "name": "bash", + "arguments": { + "command": "ls notes", + "description": "List notes" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 476, + "time": 1789265657475, + "type": "tool/ptc-dispatch", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:1", + "name": "bash", + "arguments": { + "command": "ls notes", + "description": "List notes" + }, + "isError": false, + "content": [ + { + "type": "text", + "text": "demo.txt\nnew-demo.txt" + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 477, + "time": 1789265658275, + "type": "tool/ptc-dispatch-start", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:2", + "name": "read", + "arguments": { + "file_path": "notes/demo.txt" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 478, + "time": 1789265659075, + "type": "tool/ptc-dispatch", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:2", + "name": "read", + "arguments": { + "file_path": "notes/demo.txt" + }, + "isError": false, + "content": [ + { + "type": "text", + "text": "hello fixture\n" + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 479, + "time": 1789265659875, + "type": "tool/ptc-dispatch-start", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:3", + "name": "read", + "arguments": { + "file_path": "notes/missing.txt" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 480, + "time": 1789265660675, + "type": "tool/ptc-dispatch", + "data": { + "rootCallId": "fx-call-65", + "parentCallId": "fx-call-65", + "subCallId": "fx-call-65:ptc:3", + "name": "read", + "arguments": { + "file_path": "notes/missing.txt" + }, + "isError": true, + "content": [ + { + "type": "text", + "text": "Error: ENOENT: notes/missing.txt not found" + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 481, + "time": 1789265661475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 65, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-65" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-65", + "content": [ + { + "type": "text", + "text": "{\"listing\":\"demo.txt\\nnew-demo.txt\",\"demo\":\"hello fixture\\n\"}" + } + ], + "isError": false + } + ], + "role": "user", + "id": "ca689e1a-ebfb-4465-b187-43c3490abb9b" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 482, + "time": 1789265662275, + "type": "step/end", + "data": { + "turn": 65, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 483, + "time": 1789265663075, + "type": "turn/end", + "data": { + "turn": 65, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 484, + "time": 1789265663875, + "type": "turn/start", + "data": { + "turn": 66 + } + } + }, + { + "type": "event", + "event": { + "seq": 485, + "time": 1789265664675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 66:bash 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "4a5bb1d5-3698-407a-bde1-5f8acd048eab" + } + } + }, + { + "type": "event", + "event": { + "seq": 486, + "time": 1789265665475, + "type": "step/start", + "data": { + "turn": 66, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 487, + "time": 1789265666275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 66, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-66", + "name": "bash", + "arguments": "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "68d3d448-1cac-44fd-8a6d-5446684ac98a" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-66", + "name": "bash", + "arguments": "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265666275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 488, + "time": 1789265667075, + "type": "tool/call", + "data": { + "turn": 66, + "step": 0, + "callId": "fx-call-66", + "name": "bash", + "arguments": "{\"command\":\"pnpm run check\",\"description\":\"fixture 终端样本\",\"workdir\":\"/tmp/fixture/deep/nested\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 489, + "time": 1789265667875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 66, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-66" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-66", + "content": [ + { + "type": "text", + "text": "\u001b[1mRunning 4 checks\u001b[0m\n\u001b[32m✓\u001b[0m typecheck 1.82s\n\u001b[32m✓\u001b[0m lint 0.94s\n\u001b[32m✓\u001b[0m duplication 2.10s\n\u001b[31m✗\u001b[0m unit 8.41s\n\n\u001b[90mpackages/client/ui-primitives/tests/terminal-block.client.spec.tsx\u001b[0m\n \u001b[31mFAIL\u001b[0m caps output at the configured line budget\n expected 16 lines, received 24\n\nNAME LINES BRANCHES FUNCTIONS UNCOVERED\nTerminalBlock.tsx 100% 100% 100% -\nansi.ts 100% 100% 100% -\nclipboard.ts 100% 100% 100% -\nCodeBlock.tsx 98.4% 96.2% 100% 41-43\nhighlight.ts 100% 100% 100% -\nPill.tsx 100% 100% 100% -\nStateDot.tsx 100% 100% 100% -\nmarkdown/Markdown.tsx 100% 100% 100% -\n\n\u001b[31m1 of 4 checks failed\u001b[0m\n[exit code: 1]" + } + ], + "isError": false + } + ], + "role": "user", + "id": "0da2afe6-5036-4d36-b9f3-ff562585507f" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 490, + "time": 1789265668675, + "type": "step/end", + "data": { + "turn": 66, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 491, + "time": 1789265669475, + "type": "turn/end", + "data": { + "turn": 66, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 492, + "time": 1789265670275, + "type": "turn/start", + "data": { + "turn": 67 + } + } + }, + { + "type": "event", + "event": { + "seq": 493, + "time": 1789265671075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 67:grep 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "8dab4a18-bae1-492c-a43a-8337ae80abc3" + } + } + }, + { + "type": "event", + "event": { + "seq": 494, + "time": 1789265671875, + "type": "step/start", + "data": { + "turn": 67, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 495, + "time": 1789265672675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 67, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-67", + "name": "grep", + "arguments": "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "53469dfd-6af1-4eb2-9273-53ea475b6e94" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-67", + "name": "grep", + "arguments": "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265672675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 496, + "time": 1789265673475, + "type": "tool/call", + "data": { + "turn": 67, + "step": 0, + "callId": "fx-call-67", + "name": "grep", + "arguments": "{\"pattern\":\"SEARCH_MAX_LINES\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 497, + "time": 1789265674275, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 67, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-67" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-67", + "content": [ + { + "type": "text", + "text": "Found 9 of 42 matches\n\npackages/client/ui-primitives/src/SearchBlock.tsx\nLine 16: export const DEFAULT_SEARCH_MAX_LINES = 16\nLine 138: export function SearchBlock(props: SearchBlockProps) {\nLine 141: const [collapsed, setCollapsed] = useState>(() => new Set())\npackages/client/ui-tool/src/client/tool/models/search-card-model.ts\nLine 45: export const CHAT_SEARCH_MAX_LINES = 8\nLine 130: export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {\npackages/client/ui-tool/src/client/tool/toolviews/search-row.tsx\nLine 34: export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {\nLine 36: const search = searchCardModel(block)\nLine 56: search={search}\nLine 78: yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)\n\n(Full grep result stored at: fixture://spill/grep-66. Read it to see every match.)" + } + ], + "isError": false + } + ], + "role": "user", + "id": "9a3df388-356c-4440-b0bf-b09e0dda0387" + }, + "meta": { + "shape": "matches", + "files": [ + { + "path": "packages/client/ui-primitives/src/SearchBlock.tsx", + "matches": [ + { + "lineNumber": 16, + "line": "export const DEFAULT_SEARCH_MAX_LINES = 16" + }, + { + "lineNumber": 138, + "line": "export function SearchBlock(props: SearchBlockProps) {" + }, + { + "lineNumber": 141, + "line": " const [collapsed, setCollapsed] = useState>(() => new Set())" + } + ] + }, + { + "path": "packages/client/ui-tool/src/client/tool/models/search-card-model.ts", + "matches": [ + { + "lineNumber": 45, + "line": "export const CHAT_SEARCH_MAX_LINES = 8" + }, + { + "lineNumber": 130, + "line": "export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {" + } + ] + }, + { + "path": "packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx", + "matches": [ + { + "lineNumber": 34, + "line": "export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {" + }, + { + "lineNumber": 36, + "line": " const search = searchCardModel(block)" + }, + { + "lineNumber": 56, + "line": " search={search}" + }, + { + "lineNumber": 78, + "line": " yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)" + } + ] + } + ], + "truncated": true, + "total": 42 + } + } + } + }, + { + "type": "event", + "event": { + "seq": 498, + "time": 1789265675075, + "type": "step/end", + "data": { + "turn": 67, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 499, + "time": 1789265675875, + "type": "turn/end", + "data": { + "turn": 67, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 500, + "time": 1789265676675, + "type": "turn/start", + "data": { + "turn": 68 + } + } + }, + { + "type": "event", + "event": { + "seq": 501, + "time": 1789265677475, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 68:glob 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "728f42ae-723b-485f-bf63-11b28892a89d" + } + } + }, + { + "type": "event", + "event": { + "seq": 502, + "time": 1789265678275, + "type": "step/start", + "data": { + "turn": 68, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 503, + "time": 1789265679075, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 68, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-68", + "name": "glob", + "arguments": "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "b6c6950d-d67b-45eb-a718-1a4fdb241e05" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-68", + "name": "glob", + "arguments": "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265679075, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 504, + "time": 1789265679875, + "type": "tool/call", + "data": { + "turn": 68, + "step": 0, + "callId": "fx-call-68", + "name": "glob", + "arguments": "{\"pattern\":\"**/SearchBlock*\",\"path\":\"packages/client\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 505, + "time": 1789265680675, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 68, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-68" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-68", + "content": [ + { + "type": "text", + "text": "packages/client/ui-primitives/src/SearchBlock.tsx\npackages/client/ui-primitives/src/SearchBlock.module.css\npackages/client/ui-tool/src/client/tool/models/search-card-model.ts\npackages/client/ui-tool/src/client/tool/toolviews/search-row.tsx\npackages/client/ui-tool/tests/search-card.client.spec.tsx\n\n(Showing 5 of 23 paths. Full sorted result stored at: fixture://spill/glob-67. Read it to see every path.)" + } + ], + "isError": false + } + ], + "role": "user", + "id": "e6d88943-d180-472b-9c1b-d59a21a50ab9" + }, + "meta": { + "shape": "paths", + "paths": [ + "packages/client/ui-primitives/src/SearchBlock.tsx", + "packages/client/ui-primitives/src/SearchBlock.module.css", + "packages/client/ui-tool/src/client/tool/models/search-card-model.ts", + "packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx", + "packages/client/ui-tool/tests/search-card.client.spec.tsx" + ], + "truncated": true, + "total": 23 + } + } + } + }, + { + "type": "event", + "event": { + "seq": 506, + "time": 1789265681475, + "type": "step/end", + "data": { + "turn": 68, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 507, + "time": 1789265682275, + "type": "turn/end", + "data": { + "turn": 68, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 508, + "time": 1789265683075, + "type": "turn/start", + "data": { + "turn": 69 + } + } + }, + { + "type": "event", + "event": { + "seq": 509, + "time": 1789265683875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 69:read 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "f0564417-a09b-4e3a-8dd3-c0bd41604c23" + } + } + }, + { + "type": "event", + "event": { + "seq": 510, + "time": 1789265684675, + "type": "step/start", + "data": { + "turn": 69, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 511, + "time": 1789265685475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 69, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-69", + "name": "read", + "arguments": "{\"file_path\":\"packages/client/ui-primitives/src/ReadBlock.tsx\",\"offset\":41}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "bfbff068-730d-4cf8-b647-6086e170221a" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-69", + "name": "read", + "arguments": "{\"file_path\":\"packages/client/ui-primitives/src/ReadBlock.tsx\",\"offset\":41}" + } + } + }, + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265685475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 512, + "time": 1789265686275, + "type": "tool/call", + "data": { + "turn": 69, + "step": 0, + "callId": "fx-call-69", + "name": "read", + "arguments": "{\"file_path\":\"packages/client/ui-primitives/src/ReadBlock.tsx\",\"offset\":41}" + } + } + }, + { + "type": "event", + "event": { + "seq": 513, + "time": 1789265687075, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 69, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-69" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-69", + "content": [ + { + "type": "text", + "text": "packages/client/ui-primitives/src/ReadBlock.tsx\nfile\n\n41: export interface ReadBlockProps {\n42: label?: string | undefined\n43: lines: readonly ReadBlockLine[]\n44: totalLines: number\n45: lang?: string | undefined\n46: maxLines?: number | undefined\n47: className?: string | undefined\n48: }\n49: \n50: // A windowed read keeps the file line numbers in the gutter.\n51: const marker = \"fixture read sample\"\n\n(Showing lines 41-51 of 180. Use offset=52 to continue.)\n" + } + ], + "isError": false + } + ], + "role": "user", + "id": "937c02dd-63d8-4106-9310-a9df2055f0e9" + }, + "meta": { + "path": "packages/client/ui-primitives/src/ReadBlock.tsx", + "offset": 41, + "lines": [ + { + "number": 41, + "text": "export interface ReadBlockProps {" + }, + { + "number": 42, + "text": " label?: string | undefined" + }, + { + "number": 43, + "text": " lines: readonly ReadBlockLine[]" + }, + { + "number": 44, + "text": " totalLines: number" + }, + { + "number": 45, + "text": " lang?: string | undefined" + }, + { + "number": 46, + "text": " maxLines?: number | undefined" + }, + { + "number": 47, + "text": " className?: string | undefined" + }, + { + "number": 48, + "text": "}" + }, + { + "number": 49, + "text": "" + }, + { + "number": 50, + "text": "// A windowed read keeps the file line numbers in the gutter." + }, + { + "number": 51, + "text": "const marker = \"fixture read sample\"" + } + ], + "totalLines": 180, + "lang": "ts" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 514, + "time": 1789265687875, + "type": "step/end", + "data": { + "turn": 69, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 515, + "time": 1789265688675, + "type": "turn/end", + "data": { + "turn": 69, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 516, + "time": 1789265689475, + "type": "turn/start", + "data": { + "turn": 70 + } + } + }, + { + "type": "event", + "event": { + "seq": 517, + "time": 1789265690275, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 70:web_search 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "c7ba3fc7-e437-4d77-b212-e766b1ca1c47" + } + } + }, + { + "type": "event", + "event": { + "seq": 518, + "time": 1789265691075, + "type": "step/start", + "data": { + "turn": 70, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 519, + "time": 1789265691875, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 70, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-70", + "name": "web_search", + "arguments": "{\"queries\":[\"deepseek harness architecture\"]}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "70f9aaef-f006-4b53-986d-631458679ef6" + }, + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-70", + "name": "web_search", + "arguments": "{\"queries\":[\"deepseek harness architecture\"]}" + } + } + }, + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 20, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 4 + } + } + }, + { + "type": "chunk", + "time": 1789265691875, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 520, + "time": 1789265692675, + "type": "tool/call", + "data": { + "turn": 70, + "step": 0, + "callId": "fx-call-70", + "name": "web_search", + "arguments": "{\"queries\":[\"deepseek harness architecture\"]}" + } + } + }, + { + "type": "event", + "event": { + "seq": 521, + "time": 1789265693475, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 70, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-70" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-70", + "content": [ + { + "type": "text", + "text": "Search results for deepseek harness architecture." + } + ], + "isError": false + } + ], + "role": "user", + "id": "c2ddb04d-74c9-4a2f-83f6-47a48fc6fbc2" + }, + "meta": { + "answer": "DeepSeek Harness is a plugin-based agent harness on vendored Cordis where **every capability is a plugin**.", + "sources": [ + { + "url": "https://github.com/deepseek-ai/deepseek-harness", + "title": "DeepSeek Harness — plugin-based agent harness", + "snippet": "Everything is a plugin: session, tools, agent-loop, and LLM adapters all mount on the same Cordis context.", + "publishedAt": "2026-07-01" + }, + { + "url": "https://www.deepseek.com/blog/harness-architecture", + "snippet": "The capability-seam pattern splits each capability into interface, implementation, and consumer packages." + }, + { + "url": "https://docs.deepseek.com/harness/plugins", + "title": "Writing a harness plugin", + "publishedAt": "2026-06-15" + } + ], + "truncated": true + } + } + } + }, + { + "type": "event", + "event": { + "seq": 522, + "time": 1789265694275, + "type": "step/end", + "data": { + "turn": 70, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 523, + "time": 1789265695075, + "type": "turn/end", + "data": { + "turn": 70, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 524, + "time": 1789265695875, + "type": "turn/start", + "data": { + "turn": 71 + } + } + }, + { + "type": "event", + "event": { + "seq": 525, + "time": 1789265696675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 71:web_fetch 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "b164bd50-e038-4ada-a92e-42b483efab9a" + } + } + }, + { + "type": "event", + "event": { + "seq": 526, + "time": 1789265697475, + "type": "step/start", + "data": { + "turn": 71, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 527, + "time": 1789265698275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 71, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-71", + "name": "web_fetch", + "arguments": "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "72a35941-d5a4-44ec-9b4d-f14303cf165b" + }, + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-71", + "name": "web_fetch", + "arguments": "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}" + } + } + }, + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 21, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265698275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 528, + "time": 1789265699075, + "type": "tool/call", + "data": { + "turn": 71, + "step": 0, + "callId": "fx-call-71", + "name": "web_fetch", + "arguments": "{\"url\":\"https://www.deepseek.com/blog/harness-architecture\"}" + } + } + }, + { + "type": "event", + "event": { + "seq": 529, + "time": 1789265699875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 71, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-71" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-71", + "content": [ + { + "type": "text", + "text": "# Harness architecture\n\nEverything is a plugin." + } + ], + "isError": false + } + ], + "role": "user", + "id": "57cf5cd3-3daf-4e2c-bb16-f8695bc2ab8c" + }, + "meta": { + "url": "https://www.deepseek.com/blog/harness-architecture", + "statusCode": 200, + "truncated": false + } + } + } + }, + { + "type": "event", + "event": { + "seq": 530, + "time": 1789265700675, + "type": "step/end", + "data": { + "turn": 71, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 531, + "time": 1789265701475, + "type": "turn/end", + "data": { + "turn": 71, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 532, + "time": 1789265702275, + "type": "turn/start", + "data": { + "turn": 72 + } + } + }, + { + "type": "event", + "event": { + "seq": 533, + "time": 1789265703075, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 72:请完整列出全部一百条条目。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "35a4608a-bd98-46a2-af6a-233b28470390" + } + } + }, + { + "type": "event", + "event": { + "seq": 534, + "time": 1789265703875, + "type": "step/start", + "data": { + "turn": 72, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 535, + "time": 1789265704675, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 72, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "006a595a-cd1e-4137-af5c-e36dc98f354e" + }, + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被" + } + } + }, + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 22, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265704675, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 536, + "time": 1789265705475, + "type": "step/end", + "data": { + "turn": 72, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 537, + "time": 1789265706275, + "type": "turn/end", + "data": { + "turn": 72, + "reason": { + "kind": "max-tokens" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 538, + "time": 1789265707075, + "type": "turn/start", + "data": { + "turn": 73 + } + } + }, + { + "type": "event", + "event": { + "seq": 539, + "time": 1789265707875, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "image", + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + } + }, + { + "type": "text", + "text": "历史用户图片" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "f7aa4d0e-5b76-4ccd-a566-150a227b0c27" + } + } + }, + { + "type": "event", + "event": { + "seq": 540, + "time": 1789265708675, + "type": "step/start", + "data": { + "turn": 73, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 541, + "time": 1789265709475, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 73, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "结构化模型图片:" + }, + { + "type": "image", + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + } + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-vision" + }, + "id": "35080466-4027-4eba-95b8-538f1447867b" + }, + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "text" + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "text", + "text": "结构化模型图片:" + } + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-start", + "index": 1, + "blockType": "image" + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "block-end", + "index": 1, + "block": { + "type": "image", + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + } + } + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 23, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265709475, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 542, + "time": 1789265710275, + "type": "step/end", + "data": { + "turn": 73, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 543, + "time": 1789265711075, + "type": "turn/end", + "data": { + "turn": 73, + "reason": { + "kind": "completed" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 544, + "time": 1789265711875, + "type": "turn/start", + "data": { + "turn": 74 + } + } + }, + { + "type": "event", + "event": { + "seq": 545, + "time": 1789265712675, + "type": "user/message", + "surfaceOp": "append", + "data": { + "content": [ + { + "type": "text", + "text": "问题 74:todo_write 样本。" + } + ], + "source": { + "kind": "user" + }, + "role": "user", + "id": "df61981f-bcff-4984-9d9e-de8754da7bc8" + } + } + }, + { + "type": "event", + "event": { + "seq": 546, + "time": 1789265713475, + "type": "step/start", + "data": { + "turn": 74, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 547, + "time": 1789265714275, + "type": "assistant/message", + "surfaceOp": "append", + "data": { + "turn": 74, + "step": 0, + "message": { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "id": "fx-call-74", + "name": "todo_write", + "arguments": "{\"todos\":[{\"content\":\"梳理需求\",\"status\":\"completed\"},{\"content\":\"实现 fixture 样本\",\"status\":\"in_progress\"},{\"content\":\"跑后台构建\",\"status\":\"in_progress\"},{\"content\":\"浏览器验收\",\"status\":\"pending\"}]}" + } + ], + "source": { + "kind": "model", + "provider": "fixture", + "model": "fx-1" + }, + "id": "c346330a-d740-45f6-9cbe-fb7b20d9f741" + }, + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + }, + "stream": [ + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "block-start", + "index": 0, + "blockType": "tool-call" + } + }, + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "block-end", + "index": 0, + "block": { + "type": "tool-call", + "id": "fx-call-74", + "name": "todo_write", + "arguments": "{\"todos\":[{\"content\":\"梳理需求\",\"status\":\"completed\"},{\"content\":\"实现 fixture 样本\",\"status\":\"in_progress\"},{\"content\":\"跑后台构建\",\"status\":\"in_progress\"},{\"content\":\"浏览器验收\",\"status\":\"pending\"}]}" + } + } + }, + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "usage", + "usage": { + "inputTokens": 24, + "outputTokens": 8, + "cacheReadTokens": 80, + "cacheWriteTokens": 0 + } + } + }, + { + "type": "chunk", + "time": 1789265714275, + "chunk": { + "type": "finish", + "reason": { + "kind": "stop" + } + } + } + ] + } + } + }, + { + "type": "event", + "event": { + "seq": 548, + "time": 1789265715075, + "type": "tool/call", + "data": { + "turn": 74, + "step": 0, + "callId": "fx-call-74", + "name": "todo_write", + "arguments": "{\"todos\":[{\"content\":\"梳理需求\",\"status\":\"completed\"},{\"content\":\"实现 fixture 样本\",\"status\":\"in_progress\"},{\"content\":\"跑后台构建\",\"status\":\"in_progress\"},{\"content\":\"浏览器验收\",\"status\":\"pending\"}]}" + } + } + }, + { + "type": "event", + "event": { + "type": "todo/write", + "time": 1789265715475, + "data": { + "todos": [ + { + "content": "梳理需求", + "status": "completed" + }, + { + "content": "实现 fixture 样本", + "status": "in_progress" + }, + { + "content": "跑后台构建", + "status": "in_progress" + }, + { + "content": "浏览器验收", + "status": "pending" + } + ] + }, + "seq": 549 + } + }, + { + "type": "event", + "event": { + "seq": 550, + "time": 1789265715875, + "type": "tool/result", + "surfaceOp": "append", + "data": { + "turn": 74, + "step": 0, + "message": { + "source": { + "kind": "tool", + "callId": "fx-call-74" + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "fx-call-74", + "content": [ + { + "type": "text", + "text": "Updated todo list: 1 pending, 2 in progress, 1 completed." + } + ], + "isError": false + } + ], + "role": "user", + "id": "499968fd-a83f-45ad-80fe-b1d73651ff13" + } + } + } + }, + { + "type": "event", + "event": { + "seq": 551, + "time": 1789265716675, + "type": "step/end", + "data": { + "turn": 74, + "step": 0 + } + } + }, + { + "type": "event", + "event": { + "seq": 552, + "time": 1789265717475, + "type": "turn/end", + "data": { + "turn": 74, + "reason": { + "kind": "completed" + } + } + } + } + ], + "hasMore": true, + "projections": { + "asOfSeq": 552, + "values": { + "modelSelection": { + "lastUsed": null, + "next": null + }, + "title": "Fixture 历史会话", + "todos": [ + { + "content": "梳理需求", + "status": "completed" + }, + { + "content": "实现 fixture 样本", + "status": "in_progress" + }, + { + "content": "跑后台构建", + "status": "in_progress" + }, + { + "content": "浏览器验收", + "status": "pending" + } + ], + "permissions": { + "options": [ + { + "value": "workspace-write", + "name": "workspace-write", + "description": "Write inside the workspace and permitted temporary directories; wider retries require approval." + }, + { + "value": "danger-full-access", + "name": "danger-full-access", + "description": "Full file access without approval prompts." + } + ], + "currentValue": "workspace-write" + }, + "plan": { + "active": false, + "pending": false + }, + "goal": null, + "tokenUsage": { + "uncachedInputTokens": 1914, + "outputTokens": 708, + "cacheReadTokens": 6880, + "cacheWriteTokens": 32 + }, + "contextPressure": { + "pressureTokens": 104, + "contextWindow": 128000 + }, + "contextBreakdown": { + "systemTokens": 17, + "toolsTokens": 0, + "messageTokens": 4427 + }, + "sessionStats": { + "turns": 75, + "steps": 87, + "llmMs": 69600, + "toolMs": 24800, + "ttftMs": 0, + "ttftSteps": 0, + "decodeMs": 0, + "decodeTokens": 0 + }, + "imageLimits": { + "maxImageBytes": 5242880, + "maxImagesPerMessage": 20, + "maxMessageImageBytes": 104857600, + "maxImagePixels": 40000000, + "maxImageDimension": 2000, + "mediaTypes": [ + "image/png", + "image/jpeg", + "image/webp", + "image/gif" + ] + } + } + }, + "assistantStream": { + "revision": 0 + } + }, + "attachment": { + "ok": true, + "value": { + "attachment": { + "attachmentId": "fixture:image", + "mediaType": "image/png", + "bytes": 247, + "width": 160, + "height": 90, + "name": "fixture-image.png" + }, + "data": "iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==" + } + } +} diff --git a/apps/web/tests/image-display.expected.e2e.ts b/apps/web/tests/image-display.expected.e2e.ts index e35486abfe..adbb742d0b 100644 --- a/apps/web/tests/image-display.expected.e2e.ts +++ b/apps/web/tests/image-display.expected.e2e.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom // Multimodal image surfaces over the BUILT client graph (the ptc-fixture -// idiom: real bundles via AppWebEntry, keyless fixture Connection RPC). +// idiom: real bundles via AppWebEntry, keyless RemoteMock transport). // Opens the fixture history session whose turn 73 carries an image in BOTH a // user message and an assistant message, and pins the product surfaces: the // history ImageGallery loading real fixture bytes through the authorized @@ -169,7 +169,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov // A file drag anywhere over the page raises the full-viewport overlay whose // desc line carries the projected limits — copy that can only render after - // the imageLimits projection crossed the real fixture transport. + // the imageLimits projection crossed the RemoteMock transport. const image = new File([new Uint8Array([137, 80, 78, 71])], 'dropped.png', { type: 'image/png' }) const dataTransfer = { types: ['Files'], files: [image], dropEffect: 'none' } fireEvent.dragEnter(document.body, { dataTransfer }) @@ -207,7 +207,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov }) it('renders a host dimension rejection with the projected 2000px limit', async () => { - mountAssembledApp('?fixture&fixturePrompt=reject') + mountAssembledApp({ remote: { rejectPrompt: true } }) const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 }) const start = tree.querySelector('button[aria-label="New session in fixture"]') diff --git a/apps/web/tests/max-tokens-notice.expected.e2e.ts b/apps/web/tests/max-tokens-notice.expected.e2e.ts index 5634315a32..38d23ee815 100644 --- a/apps/web/tests/max-tokens-notice.expected.e2e.ts +++ b/apps/web/tests/max-tokens-notice.expected.e2e.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // Assembled max-tokens snapshot: boots the real built `packages/client/*/lib/ // client.js` bundles through AppWebEntry's ModuleLoader path against the -// keyless fixture Connection RPC, opens the fixture session, and pins the +// keyless RemoteMock transport, opens the fixture session, and pins the // surface its max-tokens turn (72) reaches — the turn-end notice row that a // provider output-cap truncation must render instead of ending silently. // diff --git a/apps/web/tests/replay-round-trip.e2e.ts b/apps/web/tests/replay-round-trip.e2e.ts index db60b35753..fce61a7bb3 100644 --- a/apps/web/tests/replay-round-trip.e2e.ts +++ b/apps/web/tests/replay-round-trip.e2e.ts @@ -213,7 +213,7 @@ describe('web e2e: fresh round trip through the real assembly', () => { it.skipIf(MODE === 'record')('expands and collapses the reasoning fold from its click target', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-round-trip-think')) // Interaction over the REAL wire-delivered transcript (the fixture-client - // tier pins the same gesture against the fixture Connection RPC; this one runs on + // tier pins the same gesture against RemoteMock; this one runs on // follow-stream-fed state). Runs after the golden capture so the committed // aria surface stays the untouched settled state. await expandTurnProcesses(page) diff --git a/apps/web/tests/search-card.expected.e2e.ts b/apps/web/tests/search-card.expected.e2e.ts index b54d14e8e7..181a6448cb 100644 --- a/apps/web/tests/search-card.expected.e2e.ts +++ b/apps/web/tests/search-card.expected.e2e.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // Assembled search-card snapshot: boots the real built workspace client bundles // through AppWebEntry's ModuleLoader path against the keyless -// fixture Connection RPC (no API key, no model round), opens the fixture +// RemoteMock transport (no API key, no model round), opens the fixture // session, and pins the search card the `grep` turn (fixture turn 67) renders in // the assembled application. The built-boot smoke proves the graph boots but // intentionally carries no behavior assertions; this is the assembled-output check diff --git a/apps/web/tests/submission-echo.e2e.ts b/apps/web/tests/submission-echo.e2e.ts index 7693f3dc87..239c2c86e0 100644 --- a/apps/web/tests/submission-echo.e2e.ts +++ b/apps/web/tests/submission-echo.e2e.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -// Local submission echo over the BUILT client graph (keyless fixture Connection RPC +// Local submission echo over the BUILT client graph (keyless RemoteMock // transport): a text-plus-image send paints its echo bubble synchronously on // the submit keystroke — before serialization, transport, or the fixture's // durable admission — with the composer already cleared and editable, and the diff --git a/apps/web/tests/todo-row.expected.e2e.ts b/apps/web/tests/todo-row.expected.e2e.ts index 21fa70d9bf..8bc45f4dfb 100644 --- a/apps/web/tests/todo-row.expected.e2e.ts +++ b/apps/web/tests/todo-row.expected.e2e.ts @@ -1,7 +1,7 @@ // @vitest-environment jsdom // Assembled todo snapshot: boots the real built `packages/client/*/lib/ // client.js` bundles through AppWebEntry's ModuleLoader path against the -// keyless fixture Connection RPC, opens the fixture session, and pins the +// keyless RemoteMock transport, opens the fixture session, and pins the // two surfaces the fixture's parallel plan (turn 74, two items `in_progress`) // reaches — the `todo_write` tool row and the dock's plan strip. // diff --git a/apps/web/tests/trajectory-image-display.expected.e2e.ts b/apps/web/tests/trajectory-image-display.expected.e2e.ts index 315d3baa1e..855e798e55 100644 --- a/apps/web/tests/trajectory-image-display.expected.e2e.ts +++ b/apps/web/tests/trajectory-image-display.expected.e2e.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom // Trajectory image surfaces over the BUILT client graph (the ptc-fixture -// idiom: real bundles via AppWebEntry, keyless fixture Connection RPC). +// idiom: real bundles via AppWebEntry, keyless RemoteMock transport). // Opens the fixture history session whose turn 73 carries an image in BOTH a // user message and an assistant message, and pins the Trajectory surfaces: // selecting the ledger record renders the shared ui-attachment gallery from diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 606d227b8f..0504de6821 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -135,6 +135,12 @@ }, { "path": "../../packages/client/modules" + }, + { + "path": "../../packages/test-support/client-runtime" + }, + { + "path": "../../packages/test-support/remote-mock" } ] } diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 985db89e9e..b84b7acb4e 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", + "@deepseek-ai/dsh-remote-mock": "workspace:^", "@deepseek-ai/dsh-typert-registry": "workspace:^", "@deepseek-ai/dsh-util-crypto": "workspace:^", "@types/ws": "^8.18.1", diff --git a/packages/api/gateway/tests/gateway.client.spec.ts b/packages/api/gateway/tests/gateway.client.spec.ts index 7fb0ec8bdc..eea7b53343 100644 --- a/packages/api/gateway/tests/gateway.client.spec.ts +++ b/packages/api/gateway/tests/gateway.client.spec.ts @@ -3,8 +3,10 @@ import { Context, Service } from '@deepseek-ai/cordis' import type { Fiber } from '@deepseek-ai/cordis' import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { z } from 'zod' +import { RemoteMock } from '@deepseek-ai/dsh-remote-mock' import { apply as applyConnection, + type ClientTransportHooks, type ConnectionGeneration, type ConnectionGenerationSource, type ConnectionHandle, @@ -1975,11 +1977,17 @@ describe('Client Typert API', () => { }) }) - it('publishes the Fixture Host facts after Remote events report ready', async () => { + it('publishes injected Host facts after Remote events report ready', async () => { const locationDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'location') + const transportDescriptor = Object.getOwnPropertyDescriptor(globalThis, '__DSH_TRANSPORT__') + const mock = RemoteMock.create({ host: { home: '/home/mock' } }) Object.defineProperty(globalThis, 'location', { configurable: true, - value: { hostname: '127.0.0.1', search: '?fixture' }, + value: { hostname: '127.0.0.1', search: '' }, + }) + Object.defineProperty(globalThis, '__DSH_TRANSPORT__', { + configurable: true, + value: { rpc: mock.rpc } satisfies ClientTransportHooks, }) const ctx = new Context() try { @@ -1987,15 +1995,17 @@ describe('Client Typert API', () => { await ctx.plugin({ inject: [], apply: applyConnection }) await ctx.plugin({ inject, apply }) const connection = ctx.get('connection') as ConnectionHandle | undefined - if (connection === undefined) throw new Error('fixture Connection service is unavailable') + if (connection === undefined) throw new Error('injected Connection service is unavailable') await vi.waitFor(() => { - expect(connection.generation.getSnapshot()?.host.home).toBe('/home/fixture') + expect(connection.generation.getSnapshot()?.host.home).toBe('/home/mock') }) } finally { await ctx.fiber.dispose() if (locationDescriptor === undefined) Reflect.deleteProperty(globalThis, 'location') else Object.defineProperty(globalThis, 'location', locationDescriptor) + if (transportDescriptor === undefined) Reflect.deleteProperty(globalThis, '__DSH_TRANSPORT__') + else Object.defineProperty(globalThis, '__DSH_TRANSPORT__', transportDescriptor) } }) diff --git a/packages/client/connection/README.i18n.yaml b/packages/client/connection/README.i18n.yaml index 756a10b3c6..7e91c669e0 100644 --- a/packages/client/connection/README.i18n.yaml +++ b/packages/client/connection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/connection/README.md -README.md: 0010f93a7fa06055756b7f07467eb01a685cfe39 -README.zh.md: 8db4ef1f558be022af54c3b7c617124b8649e9d9 +README.md: c8c2b2f6edb0ce43a07a664bcc2b3c9ece047b7b +README.zh.md: 2c25c0d3520fd7e5d3eeeec98c41cc1d3d4d25d8 diff --git a/packages/client/connection/README.md b/packages/client/connection/README.md index 0010f93a7f..c8c2b2f6ed 100644 --- a/packages/client/connection/README.md +++ b/packages/client/connection/README.md @@ -27,8 +27,6 @@ The package carries browser-to-Host Remote calls, exact Fetch responses, and con The browser uses HTTP POST for Remote unary calls. API Gateway owns the `/api/remote.mux` WebSocket and its logical streams; shell-owned compositions provide equivalent Remote streams through `connection.rpc.open` without opening a WebSocket. The browser plugin reads the page transport, recovery settings, and location, then delegates to `installConnection(ctx, options)`. A composition that owns its carrier may call the same installer directly; the whole-client test tier does so. Each invocation creates one Context-owned service, so several Client trees can use different carriers in one realm. The Host half always provides the carrier-neutral RPC and exact `GET`/`HEAD`/`POST` route registries. When a Web carrier is present it also owns the sole `/api` route, Fetch bridge, browser authentication, and Host/Origin checks; a shell-owned carrier dispatches the shared Fetch handler directly. Each exact route declares buffered or streaming request-body handling before the bridge reads any bytes. Typert Gateway claims generated Remote endpoints, feature packages register non-JSON responses such as Session-log downloads and raw file uploads, and unclaimed requests return 404. Loopback hostname classification remains package-internal to the browser-facing Client state. Browser raw-body transfer is provided by [`dsh-client-file-upload`](../file-upload/README.md). -The browser fixture follows the [Session Controller fork semantics](../../api/session-controller/README.md#use-this-package). - ----- diff --git a/packages/client/connection/README.zh.md b/packages/client/connection/README.zh.md index 8db4ef1f55..2c25c0d352 100644 --- a/packages/client/connection/README.zh.md +++ b/packages/client/connection/README.zh.md @@ -27,8 +27,6 @@ kind: "package-reference" 浏览器通过 HTTP POST 执行 Remote 一元调用;API Gateway 自己拥有 `/api/remote.mux` WebSocket 及其逻辑流。由 shell 持有的组合通过 `connection.rpc.open` 提供等价的 Remote 流,不打开 WebSocket。浏览器插件读取页面 transport、恢复设置与 location,再委托 `installConnection(ctx, options)`。持有自身载体的组合可以直接调用同一个安装函数;整机客户端测试档就是这一消费者。每次调用都会创建一个归所属 Context 的服务,因此同一 realm 中的多棵 Client 树可以使用不同载体。Host half 始终提供与载体无关的 RPC 注册表和精确 `GET`/`HEAD`/`POST` 路由注册表。存在 Web 载体时,它还持有唯一 `/api` route、Fetch bridge、浏览器认证与 Host/Origin 校验;由 shell 持有的载体则直接分派共享 Fetch handler。每条精确路由会在 bridge 读取任何字节前声明缓冲或流式请求体处理方式。Typert Gateway 认领生成的 Remote endpoint,功能包注册 Session 日志下载、原始文件上传等非 JSON 响应,未认领的请求返回 404。Loopback hostname 判定只供浏览器侧当前页面状态使用,留在包内。浏览器原始请求体传输由 [`dsh-client-file-upload`](../file-upload/README.zh.md) 提供。 -浏览器 fixture 遵循 [Session Controller 的 fork 语义](../../api/session-controller/README.zh.md#use-this-package)。 - ----- diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 4732dcdd71..913a7bf56e 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-connection", - "description": "Authenticated RPC transport, generation lifecycle, and browser fixture", + "description": "Authenticated RPC transport and generation lifecycle", "version": "0.1.5-rc.2", "publishConfig": { "access": "public" @@ -48,16 +48,10 @@ }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-api-session-controller": "workspace:^", "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", - "@deepseek-ai/dsh-commands": "workspace:^", - "@deepseek-ai/dsh-host-directory-picker": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", - "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-tool-todo": "workspace:^", - "@deepseek-ai/dsh-util-values": "workspace:^" + "@deepseek-ai/dsh-session": "workspace:^" } } diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts deleted file mode 100644 index b270122a4a..0000000000 --- a/packages/client/connection/src/client/fixture.ts +++ /dev/null @@ -1,4053 +0,0 @@ -// Standalone browser fixture for UI development without a server. - -import { - createAssistantMessage, - createSystemMessage, - createToolResultMessage, - createUserMessage, -} from '@deepseek-ai/dsh-llm/message' -import { brandString } from '@deepseek-ai/dsh-brand' -import type { MessageId, ToolCallId } from '@deepseek-ai/dsh-llm/brand' -import type { - AssistantMessage, - ContentBlock, - MessageSource, - StreamChunk, - TokenUsage, - ToolResultMessage, - UserMessage, -} from '@deepseek-ai/dsh-llm' -import { LlmAttemptId } from '@deepseek-ai/dsh-llm/brand' -import { - AssistantStreamAccumulator, - expandAssistantStream, - type AssistantStreamRecord, -} from '@deepseek-ai/dsh-llm/assistant-stream' -import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' -import type { - SessionEvent, - SessionId, - SessionSeqCursor, -} from '@deepseek-ai/dsh-session/types' -import { SESSION_FORMAT_VERSION, SessionSeq } from '@deepseek-ai/dsh-session/types' -import type { JsonValue } from '@deepseek-ai/dsh-util-values' -import type { TodoItem } from '@deepseek-ai/dsh-tool-todo/client' -// Type-only: the brand constructor is host-side; the fixture casts at its -// wire-fabrication boundary (the schema layer's one-cast-point posture). -import type { CommandDefinitionId, CommandId } from '@deepseek-ai/dsh-commands/brand' -import type { CommandDescriptor, CommandExecution, CommandResult } from '@deepseek-ai/dsh-commands/types' -import type { CredentialInfo } from '@deepseek-ai/dsh-credentials/types' -import type { DirectoryListing as FixtureDirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types' -import type { SettingsDescribeValue, SettingsNamespaceView } from '@deepseek-ai/dsh-settings/types' -import { deriveEventMessage, foldSurface } from '@deepseek-ai/dsh-session/surface' -import type { RpcResult } from './api.ts' -import { randomUuid } from './random-uuid.ts' -import type { - ClientConnectionRpc, ConnectionRpcFailure, ConnectionRpcResult, -} from '../rpc.ts' - -const FIXTURE_SESSION_SEARCH_RESULT_LIMIT = 20 - -interface ModelSelection { - readonly provider: string - readonly model: string - readonly reasoningEffort?: string -} - -interface ModelProviderGroup { - readonly id: string - readonly name: string - readonly models: readonly { - readonly id: string - readonly name: string - readonly description?: string - readonly reasoning?: { - readonly efforts: readonly { readonly id: string; readonly name: string; readonly description?: string }[] - readonly defaultEffort?: string - } - }[] -} - -/* jscpd:ignore-start -- The standalone fixture mirrors host timing without importing a target implementation. */ -function isFixtureTokenDelta(chunk: StreamChunk): boolean { - switch (chunk.type) { - case 'text-delta': - case 'reasoning-delta': - return chunk.text !== '' - case 'tool-call-delta': - return chunk.argumentsDelta !== '' || chunk.name !== undefined - default: - return false - } -} -/* jscpd:ignore-end */ - -interface FixtureSessionSummary { - readonly sessionId: SessionId - updatedAt: number - running: boolean - blank: boolean - readonly parentSessionId?: SessionId - readonly origin?: 'subagent' - readonly cwd?: string - readonly agentPreset?: string - readonly projections?: FixtureProjectionsBlock -} - -interface FixtureProjectionsBlock { - readonly asOfSeq: number - readonly values: Readonly> -} - -interface FixtureHistoryEntry { - readonly type: 'event' - readonly event: SessionEvent -} - -type FixtureHistoryRecord = FixtureHistoryEntry - -type FixtureSessionAddress = - | { readonly kind: 'session'; readonly sessionId: SessionId } - | { - readonly kind: 'subagent' - readonly parentSessionId: SessionId - readonly childSessionId: SessionId - readonly mode: 'one-shot' | 'continuable' - } - -interface FixtureFollowRequest { - readonly address: FixtureSessionAddress - readonly maxMessages?: number - readonly assistantStream?: true -} - -interface FixturePageRequest { - readonly address: FixtureSessionAddress - readonly throughSeq: number - readonly beforeSeq?: number - readonly maxMessages?: number -} - -interface FixtureSessionWireHeader { - readonly version: number - readonly id: SessionId - readonly createdAt: number - readonly cwd?: string - readonly parentSession?: SessionId - readonly isSeeded: boolean - readonly origin?: 'subagent' - readonly delegationDepth?: number - readonly agentPreset?: string -} - -type FixtureFollowFrame = - | { - readonly type: 'snapshot' - readonly header: FixtureSessionWireHeader - readonly cursor: number - readonly records: readonly FixtureHistoryRecord[] - readonly hasMore: boolean - readonly projections: FixtureProjectionsBlock - readonly assistantStream?: FixtureAssistantStreamBaseline - } - | FixtureHistoryEntry - | { readonly type: 'assistant-stream'; readonly frame: FixtureAssistantStreamFrame } - -type FixtureFollowEventFrame = Exclude - -interface FixtureAssistantStreamBaseline { - readonly revision: number - readonly activeAttempt?: { - readonly attemptId: ReturnType - readonly startedAfterSeq: number - readonly turn: number - readonly step: number - readonly nextIndex: number - readonly stream: readonly AssistantStreamRecord[] - } -} - -/** Assistant frame emitted by the standalone fixture; tests pin it to the controller wire type. */ -export type FixtureAssistantStreamFrame = - | { - readonly type: 'start' - readonly attemptId: ReturnType - readonly revision: number - readonly startedAfterSeq: SessionSeqCursor - readonly turn: number - readonly step: number - } - | { - readonly type: 'chunk' - readonly attemptId: ReturnType - readonly revision: number - readonly index: number - readonly time: number - readonly chunk: JsonValue - } - | { - readonly type: 'end' - readonly attemptId: ReturnType - readonly revision: number - readonly index: number - readonly outcome: - | { - readonly kind: 'committed' - readonly eventType: 'assistant/message' | 'assistant/attempt' - readonly seq: number - } - | { readonly kind: 'abandoned' } - } - -interface FixtureRemoteEventNotificationFrame { - readonly type: 'emit' - readonly event: string - readonly args: readonly unknown[] -} - -interface FixtureRemoteEventInvocationFrame { - readonly type: 'waterfall' - readonly event: string - readonly eventId: string - readonly agentId: SessionId - readonly request: Readonly> -} - -interface FixtureRemoteEventCancellationFrame { - readonly type: 'cancel' - readonly eventId: string -} - -type FixtureRemoteEventFrame = - | FixtureRemoteEventNotificationFrame - | FixtureRemoteEventInvocationFrame - | FixtureRemoteEventCancellationFrame - -interface FixtureRemoteEventResult { - readonly clientId: string - readonly eventId: string - readonly outcome: - | { readonly kind: 'next' } - | { readonly kind: 'result'; readonly value?: unknown } - | { - readonly kind: 'rejected' - readonly error: { - readonly name: string - readonly message: string - readonly code?: string - readonly details?: unknown - } - } -} - -interface FixtureRemoteEventReadyFrame { - readonly type: 'ready' - readonly clientId: string - readonly host: { readonly home: string } -} - -interface FixtureProjectionFrame { - readonly type: 'projection' - readonly sessionId: SessionId - readonly key: string - readonly value: unknown - readonly seq: number -} - -interface FixtureQuestionItem { - readonly id: string - readonly header?: string - readonly question: string - readonly detail?: string - readonly multiSelect?: boolean - readonly options?: readonly { readonly label: string; readonly description?: string }[] -} - -type FixtureControlFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly queues: Readonly> - readonly jobs: Readonly> - readonly approvals: readonly never[] - readonly questions: readonly never[] - readonly projections: Readonly> - } - } - | FixtureProjectionFrame - -type FixturePromptPart = - | { readonly type: 'text'; readonly text: string } - | { - readonly type: 'image' - readonly mediaType: ImageAttachmentRef['mediaType'] - readonly data: string - readonly name?: string - } - -interface FixtureSessionApi { - list(request: { readonly cursor?: string }): Promise> - search( - request: { readonly query: string }, - signal: AbortSignal, - ): Promise> - create(request: { - readonly workspaceId?: WorkspaceId - readonly cwd?: string - readonly sessionId?: SessionId - readonly agentPreset?: string - }): Promise> - rename(request: { readonly sessionId: SessionId; readonly title: string }): Promise> - fork(request: { readonly sessionId: SessionId; readonly atSeq?: number }): Promise> - history(request: { - readonly sessionId: SessionId - readonly throughSeq?: number - readonly beforeSeq?: number - readonly maxMessages?: number - }): Promise> - selectModel(request: { - readonly sessionId: SessionId - readonly provider: string - readonly model: string - readonly reasoningEffort?: string - }): Promise> - prompt(request: { - readonly requestId: string - readonly sessionId: SessionId - readonly mode: 'queue' | 'steer' - readonly content: readonly FixturePromptPart[] - readonly clientTimeZone?: string - }): Promise> - attachment(request: { - readonly sessionId: SessionId - readonly attachmentId: AttachmentIdType - }): Promise> - updateQueue(request: { - readonly sessionId: SessionId - readonly itemId: MessageId - readonly action: unknown - }): Promise> - cancel(request: { readonly sessionId: SessionId }): Promise> -} - -type WorkspaceId = string & { readonly __fixtureWorkspaceId: 'WorkspaceId' } - -interface WorkspaceView { - readonly workspaceId: WorkspaceId - readonly path: string - readonly title: string - readonly sessionIds: readonly SessionId[] - readonly createdAt: string - readonly updatedAt: string -} - -interface WorkspaceCreateRequest { readonly path: string } -interface WorkspaceCreateValue { readonly workspace: WorkspaceView; readonly created: boolean } -interface WorkspaceRenameRequest { readonly workspaceId: WorkspaceId; readonly title: string } -interface WorkspaceValue { readonly workspace: WorkspaceView } -interface WorkspaceDeleteRequest { readonly workspaceId: WorkspaceId } -interface WorkspaceDeleteValue { readonly deleted: true } -interface WorkspaceInsertBeforeRequest { - readonly workspaceId: WorkspaceId - readonly beforeWorkspaceId?: WorkspaceId -} -interface WorkspaceOrderValue { readonly workspaceIds: readonly WorkspaceId[] } -interface WorkspaceInsertSessionBeforeRequest { - readonly workspaceId: WorkspaceId - readonly sessionId: SessionId - readonly beforeSessionId?: SessionId -} -interface WorkspaceArchiveSessionRequest { readonly sessionId: SessionId } -interface WorkspaceUnarchiveSessionRequest { readonly sessionId: SessionId } -interface WorkspaceArchiveValue { readonly archivedSessionIds: readonly SessionId[] } - -type WorkspaceFollowFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly items: readonly WorkspaceView[] - readonly archivedSessionIds: readonly SessionId[] - } - } - | { readonly type: 'upsert'; readonly workspace: WorkspaceView } - | { readonly type: 'remove'; readonly workspaceId: WorkspaceId } - | { readonly type: 'order'; readonly workspaceIds: readonly WorkspaceId[] } - | { readonly type: 'archived'; readonly archivedSessionIds: readonly SessionId[] } - -interface FixtureWorkspaceApi { - create(request: WorkspaceCreateRequest): Promise> - rename(request: WorkspaceRenameRequest): Promise> - delete(request: WorkspaceDeleteRequest): Promise> - insertBefore(request: WorkspaceInsertBeforeRequest): Promise> - insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise> - archiveSession(request: WorkspaceArchiveSessionRequest): Promise> - unarchiveSession(request: WorkspaceUnarchiveSessionRequest): Promise> -} - -interface FixtureWorkspace { - workspaceId: WorkspaceId - path: string - title: string - sessionIds: SessionId[] - createdAt: string - updatedAt: string -} - -function text(t: string): ContentBlock[] { - return [{ type: 'text', text: t }] -} - -function userMessage(content: ContentBlock[], source: MessageSource = { kind: 'user' }): UserMessage { - return createUserMessage({ content, source }) -} - -function assistantMessage(content: ContentBlock[], model = 'fx-1'): AssistantMessage { - return createAssistantMessage({ - content, - source: { provider: 'fixture', model }, - }) -} - -function toolResultMessage(callId: string, content: ContentBlock[], isError: boolean): ToolResultMessage { - return createToolResultMessage({ callId: brandString(callId), content, isError }) -} - -const MARKDOWN_FIXTURE = [ - '# Markdown fixture', - '', - 'Assistant output renders **strong text**, *emphasis*, and `inline code`.', - '', - '- first item', - ' - nested item', - '', - '| Area | State |', - '| --- | --- |', - '| history | rendered |', - '| streaming | stable |', - '', - '[DeepSeek](https://www.deepseek.com)', - '', - '```ts', - 'const markdown = true', - '```', -].join('\n') - -const USER_MARKDOWN_LITERAL = '用户字面量:# 不渲染 `code` [link](https://example.com)' - -/** - * SGR wrapper for the terminal output sample below: authoring the escapes as - * `\u001b` keeps literal control bytes out of this source file. - * @param code - the SGR parameter (an ANSI color or attribute number). - * @param body - the text the attribute applies to. - * @returns the body wrapped in the attribute and a reset. - */ -function sgr(code: number, body: string): string { - return `\u001b[${code}m${body}\u001b[0m` -} - -/** - * Terminal output sample for fixture turn 66, authored to carry every feature - * the terminal card draws that turn 60's two prompt rows cannot reach: - * basic-16 SGR foreground runs (green, red, bright-black) that must resolve to - * `--dsw-*` tokens, a bold run, column-aligned table rows that must scroll - * rather than fold, more than DEFAULT_TERMINAL_MAX_LINES (16) lines so the - * height cap collapses the middle. This constant is the visible body; the call - * site appends the shell result's `[exit code: N]` marker so Client derivation - * can consume it into the terminal status pill. - */ -const TERMINAL_OUTPUT_FIXTURE = [ - sgr(1, 'Running 4 checks'), - `${sgr(32, '\u2713')} typecheck 1.82s`, - `${sgr(32, '\u2713')} lint 0.94s`, - `${sgr(32, '\u2713')} duplication 2.10s`, - `${sgr(31, '\u2717')} unit 8.41s`, - '', - sgr(90, 'packages/client/ui-primitives/tests/terminal-block.client.spec.tsx'), - ` ${sgr(31, 'FAIL')} caps output at the configured line budget`, - ' expected 16 lines, received 24', - '', - 'NAME LINES BRANCHES FUNCTIONS UNCOVERED', - 'TerminalBlock.tsx 100% 100% 100% -', - 'ansi.ts 100% 100% 100% -', - 'clipboard.ts 100% 100% 100% -', - 'CodeBlock.tsx 98.4% 96.2% 100% 41-43', - 'highlight.ts 100% 100% 100% -', - 'Pill.tsx 100% 100% 100% -', - 'StateDot.tsx 100% 100% 100% -', - 'markdown/Markdown.tsx 100% 100% 100% -', - '', - sgr(31, '1 of 4 checks failed'), -].join('\n') - -/** - * Structured grep metadata for the search sample (turn 67). `truncated` with a - * larger `total` than the retained match count exercises the search card's - * capped indicator; the file with more than CHAT_SEARCH_MAX_LINES rows - * exercises its head/tail height cap. - */ -const SEARCH_MATCHES_FIXTURE: { path: string; matches: { lineNumber: number; line: string }[] }[] = [ - { - path: 'packages/client/ui-primitives/src/SearchBlock.tsx', - matches: [ - { lineNumber: 16, line: 'export const DEFAULT_SEARCH_MAX_LINES = 16' }, - { lineNumber: 138, line: 'export function SearchBlock(props: SearchBlockProps) {' }, - { lineNumber: 141, line: ' const [collapsed, setCollapsed] = useState>(() => new Set())' }, - ], - }, - { - path: 'packages/client/ui-tool/src/client/tool/models/search-card-model.ts', - matches: [ - { lineNumber: 45, line: 'export const CHAT_SEARCH_MAX_LINES = 8' }, - { lineNumber: 130, line: 'export function searchCardModel(block: ToolCallBlock): SearchCardModel | null {' }, - ], - }, - { - path: 'packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx', - matches: [ - { lineNumber: 34, line: 'export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {' }, - { lineNumber: 36, line: ' const search = searchCardModel(block)' }, - { lineNumber: 56, line: ' search={search}' }, - { lineNumber: 78, line: " yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow)" }, - ], - }, -] - -const SEARCH_MATCHES_TEXT = [ - 'Found 9 of 42 matches', - '', - ...SEARCH_MATCHES_FIXTURE.map(file => - [file.path, ...file.matches.map(m => `Line ${m.lineNumber}: ${m.line}`)].join('\n')), - '', - '(Full grep result stored at: fixture://spill/grep-66. Read it to see every match.)', -].join('\n') - -const SEARCH_PATHS_FIXTURE = [ - 'packages/client/ui-primitives/src/SearchBlock.tsx', - 'packages/client/ui-primitives/src/SearchBlock.module.css', - 'packages/client/ui-tool/src/client/tool/models/search-card-model.ts', - 'packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx', - 'packages/client/ui-tool/tests/search-card.client.spec.tsx', -] - -const SEARCH_PATHS_TEXT = [ - ...SEARCH_PATHS_FIXTURE, - '', - '(Showing 5 of 23 paths. Full sorted result stored at: fixture://spill/glob-67. Read it to see every path.)', -].join('\n') - -const READ_SAMPLE_FIRST_LINE = 41 -const READ_SAMPLE_SOURCE = [ - 'export interface ReadBlockProps {', - ' label?: string | undefined', - ' lines: readonly ReadBlockLine[]', - ' totalLines: number', - ' lang?: string | undefined', - ' maxLines?: number | undefined', - ' className?: string | undefined', - '}', - '', - '// A windowed read keeps the file line numbers in the gutter.', - 'const marker = "fixture read sample"', -] -const READ_SAMPLE_LINES = READ_SAMPLE_SOURCE.map((text, index) => ({ number: READ_SAMPLE_FIRST_LINE + index, text })) -const READ_SAMPLE_PATH = 'packages/client/ui-primitives/src/ReadBlock.tsx' -const READ_SAMPLE_TOTAL = 180 -const READ_SAMPLE_LAST_LINE = READ_SAMPLE_FIRST_LINE + READ_SAMPLE_SOURCE.length - 1 -const READ_SAMPLE_TEXT = [ - `${READ_SAMPLE_PATH}`, - 'file', - '', - ...READ_SAMPLE_SOURCE.map((text, index) => `${READ_SAMPLE_FIRST_LINE + index}: ${text}`), - '', - `(Showing lines ${READ_SAMPLE_FIRST_LINE}-${READ_SAMPLE_LAST_LINE} of ${READ_SAMPLE_TOTAL}. Use offset=${READ_SAMPLE_LAST_LINE + 1} to continue.)`, - '', -].join('\n') - -/** - * The `web_search` result metadata for the web-search turn. The sources cover a - * titled source with a snippet and date, a hostname-label fallback, and a - * titled source without a snippet; `truncated` exercises the capped indicator. - */ -const WEB_SEARCH_META = { - answer: 'DeepSeek Harness is a plugin-based agent harness on vendored Cordis where **every capability is a plugin**.', - sources: [ - { - url: 'https://github.com/deepseek-ai/deepseek-harness', - title: 'DeepSeek Harness — plugin-based agent harness', - snippet: 'Everything is a plugin: session, tools, agent-loop, and LLM adapters all mount on the same Cordis context.', - publishedAt: '2026-07-01', - }, - { - url: 'https://www.deepseek.com/blog/harness-architecture', - snippet: 'The capability-seam pattern splits each capability into interface, implementation, and consumer packages.', - }, - { - url: 'https://docs.deepseek.com/harness/plugins', - title: 'Writing a harness plugin', - publishedAt: '2026-06-15', - }, - ], - truncated: true, -} satisfies JsonValue - -/** The `web_fetch` result metadata for the web-fetch turn. */ -const WEB_FETCH_META = { - url: 'https://www.deepseek.com/blog/harness-architecture', - statusCode: 200, - truncated: false, -} satisfies JsonValue - -const DEEPSEEK_REASONING = { - efforts: [ - { id: 'off', name: 'Off' }, - { id: 'high', name: 'High' }, - { id: 'max', name: 'Max' }, - ], - defaultEffort: 'high', -} - -const OPENAI_REASONING = { - efforts: [ - { id: 'off', name: 'Off' }, - { id: 'medium', name: 'Medium' }, - { id: 'high', name: 'High' }, - { id: 'max', name: 'Max' }, - ], - defaultEffort: 'medium', -} - -/** Catalog served by `session/modelCatalog` (fresh copies per call). */ -function fixtureModelGroups(): ModelProviderGroup[] { - return [ - { - id: 'deepseek-official', - name: 'DeepSeek', - models: [ - { - id: 'deepseek-v4-flash', - name: 'DeepSeek-V4-Flash', - description: '快速响应', - reasoning: DEEPSEEK_REASONING, - }, - { - id: 'deepseek-v4-pro', - name: 'DeepSeek-V4-Pro', - description: '复杂任务', - reasoning: DEEPSEEK_REASONING, - }, - ], - }, - { - id: 'openai', - name: 'OpenAI', - models: [{ id: 'gpt-5', name: 'GPT-5', reasoning: OPENAI_REASONING }], - }, - ] -} - -function sid(id: string): SessionId { - return id as SessionId -} - -const FIXTURE_IMAGE_DATA = 'iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAYAAAA/xl1SAAAAvklEQVR42u3SMQ0AAAjAMIyhELM4AAe8PD1qYFlk9cCXEAEDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGBANiQDAgBgQDYkAwIAYEA2JAMCAGxIBCYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIBgQAwIBsSAYEAMCAbEgGBADAgGxIAYEAyIAcGAGBAMiAHBgBgQDIgBwYAYEAyIAcGAGBAMiAHBgBgQDIgB4bYWLb6pnOb1xAAAAABJRU5ErkJggg==' -const FIXTURE_IMAGE_REF: ImageAttachmentRef = { - attachmentId: 'fixture:image' as AttachmentIdType, - mediaType: 'image/png', - bytes: 247, - width: 160, - height: 90, - name: 'fixture-image.png', -} - -/** Deterministic provider billing attached to fixture assistant messages. */ -function fixtureUsage(turn: number, step: number): TokenUsage { - return { - inputTokens: 20 + turn % 5, - outputTokens: 8 + step, - cacheReadTokens: turn === 0 ? 0 : 80, - cacheWriteTokens: turn % 10 === 0 ? 4 : 0, - } -} - -/** Build a lossless settled stream for static fixture messages. */ -function fixtureSettledStream( - message: AssistantMessage, - usage: TokenUsage, - time: number, -): AssistantStreamRecord[] { - const stream: AssistantStreamRecord[] = [] - for (const [index, block] of message.content.entries()) { - stream.push( - { type: 'chunk', time, chunk: { type: 'block-start', index, blockType: block.type } }, - { type: 'chunk', time, chunk: { type: 'block-end', index, block } }, - ) - } - stream.push( - { type: 'chunk', time, chunk: { type: 'usage', usage } }, - { type: 'chunk', time, chunk: { type: 'finish', reason: { kind: 'stop' } } }, - ) - return stream -} - -/** Rendered system prompt of the fx-alpha history: surface node 0. */ -const FIXTURE_SYSTEM_PROMPT = '你是 DeepSeek Harness 的 fixture 助手。用简洁的中文回答,并在需要时调用工具。' - -/** fx-alpha history script: 75 turns (~150+ messages -> 4 pages at PAGE_MESSAGES=50), - * mixing reasoning blocks / tool call+result / context. */ -function buildAlphaLog(): SessionEvent[] { - const events: Record[] = [] - let time = Date.now() - 3_600_000 - const push = (e: Record): number => { - const seq = events.length - const data = e['data'] as Record | undefined - const nextTime = time + 800 - const authored = e['type'] === 'assistant/message' && data !== undefined - ? { - ...e, - data: { - ...data, - usage: fixtureUsage(data['turn'] as number, data['step'] as number), - stream: fixtureSettledStream( - data['message'] as AssistantMessage, - fixtureUsage(data['turn'] as number, data['step'] as number), - nextTime, - ), - }, - } - : e - events.push({ seq, time: (time = nextTime), ...authored }) - return seq - } - // Completed fixture requests retain the route capacity recorded with them. - push({ - type: 'request/context', - data: { provider: 'deepseek-official', model: 'deepseek-v4-flash', contextWindow: 128_000 }, - }) - for (let turn = 0; turn < 60; turn++) { - push({ type: 'turn/start', data: { turn } }) - // The rendered system prompt is surface node 0, ahead of the first user message. - if (turn === 0) { - push({ - type: 'system/message', surfaceOp: 'append', - data: { turn, step: 0, message: createSystemMessage(FIXTURE_SYSTEM_PROMPT, '@deepseek-ai/dsh-system-prompt') }, - }) - } - const userSeq = push({ - type: 'user/message', surfaceOp: 'append', - data: userMessage(text(turn === 59 ? USER_MARKDOWN_LITERAL : `问题 ${turn}:fixture 历史消息,用于翻页与渲染验收。`)), - }) - if (turn === 0) { - push({ - type: 'session/title', - data: { title: 'Fixture 历史会话', messageSeqs: [userSeq], source: { kind: 'fallback' } }, - }) - } - if (turn % 9 === 4) { - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text(`[fixture] 上下文注入(turn ${turn})`), { kind: 'plugin', plugin: 'fixture' }) }) - } - push({ type: 'step/start', data: { turn, step: 0 } }) - const withTool = turn % 5 === 2 - const withReasoning = turn % 3 === 1 - const blocks: ContentBlock[] = [] - if (withReasoning) blocks.push({ type: 'reasoning', text: `思考过程 ${turn}:这是一段可折叠的 reasoning 内容。` }) - blocks.push({ type: 'text', text: turn === 59 ? MARKDOWN_FIXTURE : `回答 ${turn}:这是 fixture 生成的历史回复正文。` }) - if (withTool) { - const callId = `fx-call-${turn}` - blocks.push({ type: 'tool-call', id: callId, name: 'echo', arguments: `{"text":"turn ${turn}"}` } as ContentBlock) - push({ type: 'assistant/message', surfaceOp: 'append', data: { turn, step: 0, message: assistantMessage(blocks) } }) - push({ type: 'tool/call', data: { turn, step: 0, callId, name: 'echo', arguments: `{"text":"turn ${turn}"}` } }) - push({ type: 'tool/result', surfaceOp: 'append', data: { turn, step: 0, message: toolResultMessage(callId, text(`ECHO: TURN ${turn}`), turn % 25 === 12) } }) - push({ type: 'step/end', data: { turn, step: 0 } }) - push({ type: 'step/start', data: { turn, step: 1 } }) - push({ type: 'assistant/message', surfaceOp: 'append', data: { turn, step: 1, message: assistantMessage(text(`工具结果已消化(turn ${turn})。`)) } }) - push({ type: 'step/end', data: { turn, step: 1 } }) - } else { - push({ type: 'assistant/message', surfaceOp: 'append', data: { turn, step: 0, message: assistantMessage(blocks) } }) - push({ type: 'step/end', data: { turn, step: 0 } }) - } - push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) - } - // The structured samples use real first-party names and result metadata so - // the fixture follows the same event-to-card path as a persisted Session. - // `echo` above remains the unknown-tool fallback. - const toolTurn = ( - turn: number, - name: string, - args: string, - resultText: string, - resultMeta?: JsonValue, - ): void => { - const callId = `fx-call-${turn}` - push({ type: 'turn/start', data: { turn } }) - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text(`问题 ${turn}:${name} 样本。`)) }) - push({ type: 'step/start', data: { turn, step: 0 } }) - push({ - type: 'assistant/message', surfaceOp: 'append', - data: { turn, step: 0, message: assistantMessage([{ type: 'tool-call', id: callId, name, arguments: args } as ContentBlock]) }, - }) - push({ type: 'tool/call', data: { turn, step: 0, callId, name, arguments: args } }) - push({ - type: 'tool/result', - surfaceOp: 'append', - data: { - turn, - step: 0, - message: toolResultMessage(callId, text(resultText), false), - ...resultMeta === undefined ? {} : { meta: resultMeta }, - }, - }) - push({ type: 'step/end', data: { turn, step: 0 } }) - push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) - } - // A two-line command, so the fixture covers the terminal card's one-row-per- - // command-line prompt (and that the card still marks the call exactly once). - toolTurn( - 60, - 'bash', - '{"command":"ls -la\\necho done","description":"fixture 终端样本","workdir":"/tmp/fixture"}', - 'total 2\ndrwxr-xr-x fixture\n-rw-r--r-- demo.txt', - ) - toolTurn( - 61, - 'write', - '{"file_path":"notes/demo.txt","content":"hello fixture\\n"}', - 'wrote notes/demo.txt', - { diffs: [{ path: 'notes/demo.txt', oldText: null, newText: 'hello fixture\n' }] }, - ) - toolTurn( - 62, - 'edit', - '{"file_path":"notes/demo.txt","old_string":"hello","new_string":"hello fixture"}', - '已编辑', - { diffs: [{ path: 'notes/demo.txt', oldText: 'hello', newText: 'hello fixture' }] }, - ) - toolTurn( - 63, - 'write', - '{"file_path":"notes/new-demo.txt","content":"hello fixture\\n"}', - '已写入', - { diffs: [{ path: 'notes/new-demo.txt', oldText: null, newText: 'hello fixture\n' }] }, - ) - // Turn 64: a multi-hunk edit — two scattered replacements in one file. Named - // `edit` so it lands on the keyed FileMutationRow (the resident diff card the - // single-hunk turn 62 also uses). Its result metadata carries two scattered - // hunks under one path header, so the card draws the first hunk, a `⋯` gap, - // then the second (the same-file - // second-hunk arm turns 62/63 cannot reach). - toolTurn( - 64, - 'edit', - '{"file_path":"src/config.ts","old_string":"const timeout = 30","new_string":"const timeout = 60"}', - '已编辑', - { - diffs: [ - { path: 'src/config.ts', oldText: 'const timeout = 30', newText: 'const timeout = 60' }, - { path: 'src/config.ts', oldText: 'retries: 1', newText: 'retries: 3' }, - ], - }, - ) - // Turn 65: one run_code turn with three logged sub-dispatches — the Code - // Mode acceptance surface (parent code row + nested native-identical rows, - // including an isError sub-call and a bash sub-call that must hit the same - // keyed registration a top-level bash row uses). - { - const turn = 65 - const callId = `fx-call-${turn}` - const program = 'const listing = await tools.bash({ command: "ls notes", description: "List notes" })\n' - + 'const demo = await tools.read({ file_path: "notes/demo.txt" })\n' - + 'await tools.read({ file_path: "notes/missing.txt" }).catch(() => "tolerated")\n' - + 'return { listing, demo }' - const args = JSON.stringify({ code: program, description: 'Read the notes files and summarize' }) - push({ type: 'turn/start', data: { turn } }) - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text(`问题 ${turn}:run_code 样本。`)) }) - push({ type: 'step/start', data: { turn, step: 0 } }) - push({ - type: 'assistant/message', surfaceOp: 'append', - data: { turn, step: 0, message: assistantMessage([{ type: 'tool-call', id: callId, name: 'run_code', arguments: args } as ContentBlock]) }, - }) - push({ type: 'tool/call', data: { turn, step: 0, callId, name: 'run_code', arguments: args } }) - const dispatchPair = (n: number, name: string, dispatchArgs: Record, resultText: string, isError = false): void => { - push({ - type: 'tool/ptc-dispatch-start', - data: { rootCallId: callId, parentCallId: callId, subCallId: `${callId}:ptc:${n}`, name, arguments: dispatchArgs }, - }) - push({ - type: 'tool/ptc-dispatch', - data: { - rootCallId: callId, parentCallId: callId, subCallId: `${callId}:ptc:${n}`, name, - arguments: dispatchArgs, isError, content: [{ type: 'text', text: resultText }], - }, - }) - } - dispatchPair(1, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt\nnew-demo.txt') - dispatchPair(2, 'read', { file_path: 'notes/demo.txt' }, 'hello fixture\n') - dispatchPair(3, 'read', { file_path: 'notes/missing.txt' }, 'Error: ENOENT: notes/missing.txt not found', true) - push({ - type: 'tool/result', surfaceOp: 'append', - data: { turn, step: 0, message: toolResultMessage(callId, text('{"listing":"demo.txt\\nnew-demo.txt","demo":"hello fixture\\n"}'), false) }, - }) - push({ type: 'step/end', data: { turn, step: 0 } }) - push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) - } - // Turn 74: todo_write sample — the TodoRow toolview in the flow plus the - // todo/write snapshot event feeding the TodoPanel plan strip. Two items are - // in_progress: this fixture chooses the parallel policy, so both surfaces - // must render a parallel plan rather than the first active item alone. - const fixtureTodos = [ - { content: '梳理需求', status: 'completed' }, - { content: '实现 fixture 样本', status: 'in_progress' }, - { content: '跑后台构建', status: 'in_progress' }, - { content: '浏览器验收', status: 'pending' }, - ] - // Turn 66: the terminal sample turn 60's two clean prompt rows cannot cover — - // ANSI SGR coloring, output past the terminal card's height cap, a nested cwd - // whose prompt label is its last segment, and a non-zero exit. The raw result - // includes an `[exit code: N]` marker below; Client - // derivation consumes it into the status pill before rendering the body. - // - // Ordered BEFORE the todo turn deliberately: the standing plan retires at the - // next `turn/start`, so a turn appended after it would leave the dock's plan - // strip empty and take the todo surfaces' own coverage with it. - toolTurn( - 66, - 'bash', - '{"command":"pnpm run check","description":"fixture 终端样本","workdir":"/tmp/fixture/deep/nested"}', - `${TERMINAL_OUTPUT_FIXTURE}\n[exit code: 1]`, - ) - - // Turns 67-68 carry the search card's two metadata variants: grouped matches - // and a flat path list, both truncated with a larger pre-cap total. Both use - // the keyed SearchRow registration. They stay before the todo turn for the - // same standing-plan reason as the bash turn. - toolTurn( - 67, - 'grep', - '{"pattern":"SEARCH_MAX_LINES","path":"packages/client"}', - SEARCH_MATCHES_TEXT, - { shape: 'matches', files: SEARCH_MATCHES_FIXTURE, truncated: true, total: 42 }, - ) - toolTurn( - 68, - 'glob', - '{"pattern":"**/SearchBlock*","path":"packages/client"}', - SEARCH_PATHS_TEXT, - { shape: 'paths', paths: SEARCH_PATHS_FIXTURE, truncated: true, total: 23 }, - ) - - // Turn 69: the read sample — a WINDOW past an offset so the card draws file - // line numbers starting above 1 and a "showing N of M" note (the window is - // shorter than READ_SAMPLE_TOTAL), with a `ts` language hint the shiki path - // highlights. Named `read`, so it exercises the keyed ReadRow registration. - // The run_code sub-dispatches above cover nested read calls without result - // metadata; this top-level result carries the structured window. - toolTurn( - 69, - 'read', - `{"file_path":${JSON.stringify(READ_SAMPLE_PATH)},"offset":${READ_SAMPLE_FIRST_LINE}}`, - READ_SAMPLE_TEXT, - { - path: READ_SAMPLE_PATH, - offset: READ_SAMPLE_FIRST_LINE, - lines: READ_SAMPLE_LINES, - totalLines: READ_SAMPLE_TOTAL, - lang: 'ts', - }, - ) - - // Turns 70-71 carry the web tools' result metadata. They stay before the todo - // turn because a later turn/start retires the standing plan projection. - toolTurn( - 70, - 'web_search', - '{"queries":["deepseek harness architecture"]}', - 'Search results for deepseek harness architecture.', - WEB_SEARCH_META, - ) - toolTurn( - 71, - 'web_fetch', - '{"url":"https://www.deepseek.com/blog/harness-architecture"}', - '# Harness architecture\n\nEverything is a plugin.', - WEB_FETCH_META, - ) - - // Turn 72: max-tokens sample — the provider ends the turn at its output cap - // mid-sentence, so the chat flow must render the turn-max-tokens notice - // instead of ending silently. Ordered before the todo turn for the same - // standing-plan reason the bash turn is. - push({ type: 'turn/start', data: { turn: 72 } }) - push({ type: 'user/message', surfaceOp: 'append', data: userMessage(text('问题 72:请完整列出全部一百条条目。')) }) - push({ type: 'step/start', data: { turn: 72, step: 0 } }) - push({ - type: 'assistant/message', - surfaceOp: 'append', - data: { turn: 72, step: 0, message: assistantMessage(text('条目 1:第一条。条目 2:第二条。条目 3:这一条写到一半被')) }, - }) - push({ type: 'step/end', data: { turn: 72, step: 0 } }) - push({ type: 'turn/end', data: { turn: 72, reason: { kind: 'max-tokens' } } }) - - // Turn 73: user and assistant images share one durable fixture object. - // The todo turn remains last so its standing projection stays visible. - push({ type: 'turn/start', data: { turn: 73 } }) - push({ - type: 'user/message', - surfaceOp: 'append', - data: userMessage([{ type: 'image', attachment: FIXTURE_IMAGE_REF }, ...text('历史用户图片')]), - }) - push({ type: 'step/start', data: { turn: 73, step: 0 } }) - push({ - type: 'assistant/message', - surfaceOp: 'append', - data: { - turn: 73, - step: 0, - message: assistantMessage( - [...text('结构化模型图片:'), { type: 'image', attachment: FIXTURE_IMAGE_REF }], - 'fx-vision', - ), - }, - }) - push({ type: 'step/end', data: { turn: 73, step: 0 } }) - push({ type: 'turn/end', data: { turn: 73, reason: { kind: 'completed' } } }) - - const todoArgs = JSON.stringify({ todos: fixtureTodos }) - toolTurn(74, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 2 in progress, 1 completed.') - // The real tool appends the snapshot mid-execution — between tool/call and - // tool/result — so the fixture reproduces that exact ordering (the last - // toolTurn events run ... tool/call, tool/result, step/end, turn/end). - const callIndex = events.length - 4 - const callTime = events[callIndex]?.time as number - events.splice(callIndex + 1, 0, { type: 'todo/write', time: callTime + 400, data: { todos: fixtureTodos } }) - events.forEach((e, i) => { e.seq = i }) - return events as unknown as SessionEvent[] -} - -/** - * Fixture parallel of the plan unit's lifecycle fold. The paired - * `command/done` retains successful plan selections and drops failures; - * `plan/mode` commits one. `wanted` is exposed for the prompt boundary (the - * fixture's step/start parallel). - */ -function foldPlan(log: readonly SessionEvent[]): { active: boolean; pending: boolean; wanted: boolean | null } { - let active = false - let wanted: boolean | null = null - let running: { commandId: unknown; wanted: boolean } | null = null - for (const event of log) { - const item = event as unknown as { type: string; data?: Record } - if (item.type === 'command/run' && item.data?.['name'] === 'plan') { - const args = item.data['args'] - if (typeof args !== 'string') continue - running = { commandId: item.data['commandId'], wanted: args.trim() !== 'off' } - } else if (item.type === 'command/done' - && item.data !== undefined - && running !== null - && item.data['commandId'] === running.commandId) { - wanted = item.data['kind'] === 'success' && running.wanted !== active ? running.wanted : null - running = null - } else if (item.type === 'plan/mode') { - active = item.data?.['active'] === true - wanted = null - } - } - const selected = running?.wanted ?? wanted - return { active, pending: selected !== null && selected !== active, wanted: selected } -} - -/** The plan projection's wire view over the full log. */ -function planViewOf(log: readonly SessionEvent[]): { active: boolean; pending: boolean } { - const plan = foldPlan(log) - return { active: plan.active, pending: plan.pending } -} - -/** Fixture parallel of the host's projection units: whole current values per key over the full log. */ -/** Fixture preset table (the host PermissionPresetService defaults). */ -const PERMISSION_PRESETS: Record = { - 'workspace-write': { sandbox: 'workspace-write', approval: 'ask', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.' }, - 'danger-full-access': { sandbox: 'danger-full-access', approval: 'never', description: 'Full file access without approval prompts.' }, -} - -/** Host permissions-unit parallel: fold the three knob events, derive the select over the fixture defaults. */ -function permissionSelectOf( - log: readonly SessionEvent[], -): { options: { value: string; name: string; description?: string }[]; currentValue: string } { - let preset: string | null = null - let sandbox = 'workspace-write' - let approval = 'ask' - for (const event of log) { - const item = event as { type: string; data: Record } - if (item.type === 'permission/preset') preset = item.data['preset'] as string - else if (item.type === 'sandbox/mode') sandbox = item.data['mode'] as string - else if (item.type === 'approval/policy') approval = item.data['policy'] as string - } - const matches = (spec: { sandbox: string; approval: string }): boolean => spec.sandbox === sandbox && spec.approval === approval - let currentValue = 'custom' - const folded = preset === null ? undefined : PERMISSION_PRESETS[preset] - if (preset !== null && folded !== undefined && matches(folded)) { - currentValue = preset - } else { - for (const [name, spec] of Object.entries(PERMISSION_PRESETS)) { - if (matches(spec)) { currentValue = name; break } - } - } - return { - options: [ - ...Object.entries(PERMISSION_PRESETS).map(([value, spec]) => ({ value, name: value, description: spec.description })), - ...currentValue === 'custom' ? [{ value: 'custom', name: 'Custom', description: 'Current sandbox and approval settings do not match a preset.' }] : [], - ], - currentValue, - } -} - -interface FixtureTokenUsageProjection { - uncachedInputTokens: number - outputTokens: number - cacheReadTokens: number - cacheWriteTokens: number -} - -interface FixtureUsageSample { - turn: number - step: number - usage: TokenUsage -} - -/** Read one provider usage sample from either durable carrier. */ -function usageSampleOf(event: SessionEvent): FixtureUsageSample | undefined { - if (event.type !== 'assistant/message' && event.type !== 'assistant/attempt') return undefined - let usage = event.type === 'assistant/message' ? event.data.usage : undefined - for (const member of expandAssistantStream(event.data.stream)) { - if (member.chunk.type === 'usage') usage = member.chunk.usage - } - return usage === undefined - ? undefined - : { turn: event.data.turn, step: event.data.step, usage } -} - -/** Fixture parallel of token-meter's last-sample-replacing usage projection. */ -function tokenUsageOf(log: readonly SessionEvent[]): FixtureTokenUsageProjection { - const totals: FixtureTokenUsageProjection = { - uncachedInputTokens: 0, - outputTokens: 0, - cacheReadTokens: 0, - cacheWriteTokens: 0, - } - let last: { - turn: number - step: number - buckets: FixtureTokenUsageProjection - } | null = null - for (const event of log) { - const sample = usageSampleOf(event) - if (sample === undefined) continue - const buckets: FixtureTokenUsageProjection = { - uncachedInputTokens: sample.usage.inputTokens, - outputTokens: sample.usage.outputTokens, - cacheReadTokens: sample.usage.cacheReadTokens ?? 0, - cacheWriteTokens: sample.usage.cacheWriteTokens ?? 0, - } - const previous = last?.turn === sample.turn && last.step === sample.step - ? last.buckets - : undefined - totals.uncachedInputTokens += buckets.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) - totals.outputTokens += buckets.outputTokens - (previous?.outputTokens ?? 0) - totals.cacheReadTokens += buckets.cacheReadTokens - (previous?.cacheReadTokens ?? 0) - totals.cacheWriteTokens += buckets.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) - last = { turn: sample.turn, step: sample.step, buckets } - } - return totals -} - -/** Fixture parallel of session-stats' whole-log counting and wall-time fold. */ -function sessionStatsOf(log: readonly SessionEvent[]): { - turns: number - steps: number - llmMs: number - toolMs: number - ttftMs: number - ttftSteps: number - decodeMs: number - decodeTokens: number -} { - const value = { turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0 } - let lastTurn: number | null = null - let openStep: { turn: number; step: number; startTime: number; firstTokenTime: number | null } | null = null - const pendingCalls = new Map() - for (const event of log) { - switch (event.type) { - case 'step/start': - openStep = { turn: event.data.turn, step: event.data.step, startTime: event.time, firstTokenTime: null } - break - case 'assistant/attempt': { - if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break - const first = expandAssistantStream(event.data.stream) - .find(member => isFixtureTokenDelta(member.chunk))?.time - if (openStep.firstTokenTime === null && first !== undefined) openStep.firstTokenTime = first - break - } - case 'assistant/message': { - if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break - const first = expandAssistantStream(event.data.stream) - .find(member => isFixtureTokenDelta(member.chunk))?.time - if (openStep.firstTokenTime === null && first !== undefined) openStep.firstTokenTime = first - value.llmMs += Math.max(0, event.time - openStep.startTime) - if (openStep.firstTokenTime !== null) { - value.ttftMs += Math.max(0, openStep.firstTokenTime - openStep.startTime) - value.ttftSteps += 1 - const outputTokens = event.data.usage?.outputTokens - if (typeof outputTokens === 'number' && Number.isFinite(outputTokens) && outputTokens >= 0) { - value.decodeMs += Math.max(0, event.time - openStep.firstTokenTime) - value.decodeTokens += outputTokens - } - } - openStep = null - break - } - case 'tool/call': - pendingCalls.set(event.data.callId, event.time) - break - case 'tool/result': { - const callId = event.data.message.source.callId - const dispatched = pendingCalls.get(callId) - if (dispatched === undefined) break - pendingCalls.delete(callId) - value.toolMs += Math.max(0, event.time - dispatched) - break - } - case 'step/end': - if (event.data.turn !== lastTurn) { - value.turns += 1 - lastTurn = event.data.turn - } - value.steps += 1 - openStep = null - break - case 'turn/end': - pendingCalls.clear() - break - default: - break - } - } - return value -} - -interface FixtureRequestContext { - provider: string - model: string - contextWindow?: number -} - -interface FixtureContextBreakdownProjection { - systemTokens: number - toolsTokens: number - messageTokens: number -} - -/** Fixed token-meter heuristic constants mirrored by this client-only fixture. */ -const CHARS_PER_TOKEN = 4 -const BLOCK_OVERHEAD = 4 -const ROLE_OVERHEAD = 4 - -/** Price fixture content with token-meter's fixed-density heuristic. */ -function estimateFixtureContent(blocks: readonly ContentBlock[]): number { - const densityPrice = (value: string): number => Math.ceil(value.length / CHARS_PER_TOKEN) - return blocks.reduce((tokens, block) => { - if (block.type === 'text' || block.type === 'reasoning') { - return tokens + densityPrice(block.text) + BLOCK_OVERHEAD - } - if (block.type === 'tool-call') { - return tokens + densityPrice(block.name) + densityPrice(block.arguments) + BLOCK_OVERHEAD - } - // ContentBlockMap is merge-extensible: this client graph sees only the - // base four members, but fixture turns do carry extended blocks at - // runtime, so the structural JSON fallback below is live code. - if (block.type === 'tool-result') { - return tokens + estimateFixtureContent(block.content) + BLOCK_OVERHEAD - } - return tokens + densityPrice(JSON.stringify(block)) + BLOCK_OVERHEAD - }, 0) -} - -/** - * Fixture parallel of token-meter's heuristic context-composition projection. - * The system prompt is the system-role surface node; it prices as text plus - * role framing with no block overhead and stays out of the message figure. - */ -function contextBreakdownOf(log: readonly SessionEvent[]): FixtureContextBreakdownProjection { - const headerEvent = log.findLast(event => event.type === 'request/header') - const header = headerEvent === undefined - ? undefined - : headerEvent.data.header - let systemTokens = 0 - let messageTokens = 0 - for (const seq of foldSurface(log).nodes) { - const event = log[seq] - if (event === undefined) continue - const message = deriveEventMessage(event) - if (message === null) continue - if (message.role === 'system') { - const characters = message.content.reduce( - (total, block) => total + (block.type === 'text' ? block.text.length : JSON.stringify(block).length), - 0, - ) - systemTokens = Math.ceil(characters / CHARS_PER_TOKEN) + ROLE_OVERHEAD - continue - } - messageTokens += estimateFixtureContent(message.content) + ROLE_OVERHEAD - } - return { - systemTokens, - toolsTokens: header?.tools === undefined || header.tools.length === 0 - ? 0 - : Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD, - messageTokens, - } -} - -/** Latest log-only route context, or undefined before any request ran. */ -function lastRequestContext( - log: readonly SessionEvent[], -): FixtureRequestContext | undefined { - const event = log.findLast(item => (item as { type: string }).type === 'request/context') - return event === undefined - ? undefined - : (event as unknown as { data: FixtureRequestContext }).data -} - -/** - * Fixture parallel of token-meter's request-pressure projection: the last - * provider-reported prompt size paired with the last recorded capacity. The - * two need not come from one request — see the token-meter README. The host's - * `projectedTokens` is deliberately absent: reproducing it would mean - * reimplementing the estimator client-side, and every consumer falls back to - * the bare sample, so a fixture-driven view simply lags a compaction the way - * the projection did before that field existed. - */ -function contextPressureOf( - log: readonly SessionEvent[], -): { pressureTokens?: number; contextWindow?: number } { - let pressureTokens: number | undefined - for (const event of log) { - const sample = usageSampleOf(event) - if (sample === undefined) continue - pressureTokens = sample.usage.inputTokens - + (sample.usage.cacheReadTokens ?? 0) - + (sample.usage.cacheWriteTokens ?? 0) - } - const contextWindow = lastRequestContext(log)?.contextWindow - return { - ...pressureTokens === undefined ? {} : { pressureTokens }, - ...contextWindow === undefined ? {} : { contextWindow }, - } -} - -function projectionValuesOf(log: readonly SessionEvent[]): Record { - const values: Record = {} - values['modelSelection'] = modelSelectionProjectionOf(log) - const titleEvent = log.findLast(item => (item as { type: string }).type === 'session/title') - if (titleEvent !== undefined) { - values['title'] = (titleEvent as unknown as { data: { title: string } }).data.title - } - // Always present (tool-todo unit composed): null when no plan stands. - values['todos'] = backscanTodos(log) ?? null - // Always present (permission service composed): the whole select. - values['permissions'] = permissionSelectOf(log) - // Always present (plan-mode unit composed): the {active, pending} view. - values['plan'] = planViewOf(log) - // Always present (GoalService unit composed): null before create / after clear. - values['goal'] = backscanGoal(log) - // Always present (token-meter composed): full-log provider billing. - values['tokenUsage'] = tokenUsageOf(log) - // Always present (token-meter composed): last request pressure and capacity. - values['contextPressure'] = contextPressureOf(log) - // Always present (token-meter composed): heuristic request composition. - values['contextBreakdown'] = contextBreakdownOf(log) - // Always present (session-stats unit composed): whole-log turn/step counts. - values['sessionStats'] = sessionStatsOf(log) - // Always present (attachment service composed): the deployment image - // limits, constant per boot (mirrors the attachment-local defaults). - // Deliberate host divergence: the real gateway never pushes an imageLimits - // change frame (constant unit), but the fixture's uniform baseline replay - // frames every key here, incidentally exercising higher-seq-wins. - values['imageLimits'] = { - maxImageBytes: 5 * 1024 * 1024, - maxImagesPerMessage: 20, - maxMessageImageBytes: 100 * 1024 * 1024, - maxImagePixels: 40_000_000, - maxImageDimension: 2000, - mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], - } - return values -} - -function modelSelectionProjectionOf(log: readonly SessionEvent[]): { - lastUsed: ModelSelection | null - next: ModelSelection | null -} { - let lastUsed: ModelSelection | null = null - let pending: ModelSelection | null = null - for (const event of log) { - if ((event as { type: string }).type === 'model/selection') { - pending = (event as unknown as { data: ModelSelection }).data - continue - } - if (event.type !== 'request/header') continue - lastUsed = { - provider: event.data.header.config.provider, - model: event.data.header.config.model, - ...(event.data.header.config.reasoningEffort === undefined - ? {} - : { reasoningEffort: event.data.header.config.reasoningEffort }), - } - if (sameModelSelection(pending, lastUsed)) pending = null - } - return { lastUsed, next: pending ?? lastUsed } -} - -function sameModelSelection(left: ModelSelection | null, right: ModelSelection | null): boolean { - return left === right || (left !== null && right !== null - && left.provider === right.provider - && left.model === right.model - && left.reasoningEffort === right.reasoningEffort) -} - -/** Host parallel: emit one Session control projection frame per key advanced by the event. */ -function projectionFramesOf( - id: SessionId, - log: readonly SessionEvent[], - event: SessionEvent, -): FixtureProjectionFrame[] { - const type = (event as { type: string }).type - const frames: FixtureProjectionFrame[] = [] - if (type === 'model/selection' || type === 'request/header') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'modelSelection', - value: modelSelectionProjectionOf(log), - seq: event.seq, - }) - } - // One usage sample advances both token-meter units. - if (usageSampleOf(event) !== undefined) { - frames.push( - { type: 'projection', sessionId: id, key: 'tokenUsage', value: tokenUsageOf(log), seq: event.seq }, - { type: 'projection', sessionId: id, key: 'contextPressure', value: contextPressureOf(log), seq: event.seq }, - ) - } - if (type === 'request/context') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'contextPressure', - value: contextPressureOf(log), - seq: event.seq, - }) - } - if (type === 'request/header' - || type === 'system/message' - || type === 'user/message' - || type === 'assistant/message' - || type === 'tool/result') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'contextBreakdown', - value: contextBreakdownOf(log), - seq: event.seq, - }) - } - // The stats fold's view advances on message assembly and tool settlement - // (wall times) and on step close (counts). - if (type === 'assistant/message' || type === 'tool/result' || type === 'step/end') { - frames.push({ - type: 'projection', - sessionId: id, - key: 'sessionStats', - value: sessionStatsOf(log), - seq: event.seq, - }) - } - if (frames.length > 0) return frames - if (type === 'session/title') { - const values = projectionValuesOf(log) - /* v8 ignore next -- the advancing title event is in the log, so the key is present. */ - if (!Object.hasOwn(values, 'title')) return [] - return [{ type: 'projection', sessionId: id, key: 'title', value: values['title'], seq: event.seq }] - } - // The goal domain's own durable change advances its projection. - if (type === 'goal/change') { - return [{ type: 'projection', sessionId: id, key: 'goal', value: backscanGoal(log), seq: event.seq }] - } - // Standing-plan fold: writes replace the list; turn/start clears it (null). - if (type === 'todo/write' || type === 'turn/start') { - return [{ - type: 'projection', - sessionId: id, - key: 'todos', - value: backscanTodos(log) ?? null, - seq: event.seq, - }] - } - // Knob fold: any of the three whole-value knob events advances the select. - if (type === 'permission/preset' || type === 'sandbox/mode' || type === 'approval/policy') { - return [{ - type: 'projection', - sessionId: id, - key: 'permissions', - value: permissionSelectOf(log), - seq: event.seq, - }] - } - // The plan unit advances on its two folded event kinds when the command - // lifecycle contains the input that represents a plan selection. - const commandData = event as unknown as { data: { name?: string; args?: unknown } } - if (type === 'plan/mode' || (type === 'command/run' - && commandData.data.name === 'plan' && typeof commandData.data.args === 'string')) { - return [{ - type: 'projection', - sessionId: id, - key: 'plan', - value: planViewOf(log), - seq: event.seq, - }] - } - return [] -} - -/** - * Message-boundary paging mirrors the Host contract: count `maxMessages` - * backwards from the end and cut at a turn/start boundary. - */ -function pageOf( - log: readonly SessionEvent[], - beforeSeq: number | undefined, - maxMessages: number, -): { records: FixtureHistoryRecord[]; hasMore: boolean } { - const end = beforeSeq === undefined ? log.length : Math.max(0, Math.min(beforeSeq, log.length)) - let start = 0 - let messages = 0 - for (let i = end - 1; i >= 0; i--) { - const event = log[i] - /* v8 ignore next -- dense-array guard: log seqs are array indexes, i stays within [0, end). */ - if (event === undefined) break - if (event.type === 'user/message' || event.type === 'assistant/message') messages++ - if (event.type === 'turn/start' && messages >= maxMessages) { - start = i - break - } - } - const records = log.slice(start, end).map((event): FixtureHistoryRecord => ({ type: 'event', event })) - return { records, hasMore: start > 0 } -} - -/** Fixture mirror of host session-scoped attachment authorization. */ -function logReferencesAttachment(log: readonly SessionEvent[], attachmentId: string): boolean { - const visit = (value: unknown): boolean => { - if (Array.isArray(value)) return value.some(visit) - if (typeof value !== 'object' || value === null) return false - const record = value as Record - if (record.attachmentId === attachmentId) return true - return Object.values(record).some(visit) - } - return log.some(event => visit(event.data)) -} - -/** Fixture mirror of first-party message extraction used by session-query. */ -function searchBlockText(block: ContentBlock): string[] { - switch (block.type) { - case 'text': - return [block.text] - case 'reasoning': - return [] - case 'tool-call': - return [block.name, block.arguments] - case 'tool-result': - return block.content.flatMap(searchBlockText) - default: - return [] - } -} - -/** One current-surface user/assistant document, if searchable. */ -function searchEventText(event: SessionEvent): string { - const content = event.type === 'user/message' - ? event.data.content - : event.type === 'assistant/message' - ? event.data.message.content - : undefined - if (content === undefined) return '' - return content.flatMap(searchBlockText).map(part => part.trim()).filter(Boolean).join('\n') -} - -interface FixtureSearchToken { - value: string - /** Inclusive code-point offset in the whitespace-normalized display text. */ - start: number - /** Exclusive code-point offset in the whitespace-normalized display text. */ - end: number -} - -/** - * Browser-safe approximation of SQLite FTS5 unicode61 token boundaries. - * Keeping phrase matching token-based prevents the development fixture from - * promising arbitrary within-token substring behavior that production lacks. - */ -function searchTokenSpans(value: string): { text: string; tokens: FixtureSearchToken[] } { - const text = value.replace(/\s+/gu, ' ').trim() - const characters = Array.from(text) - const tokens: FixtureSearchToken[] = [] - let start: number | undefined - let raw = '' - const flush = (end: number): void => { - if (start !== undefined) { - const folded = raw.normalize('NFD').replace(/\p{M}+/gu, '').toLowerCase() - if (folded !== '') tokens.push({ value: folded, start, end }) - } - start = undefined - raw = '' - } - for (let index = 0; index < characters.length; index++) { - const character = characters[index] as string - const tokenBase = character.normalize('NFD').replace(/\p{M}+/gu, '') - if (tokenBase === '') { - if (start !== undefined) raw += character - continue - } - if (/^[\p{L}\p{N}\p{Co}]+$/u.test(tokenBase)) { - start ??= index - raw += character - } else { - flush(index) - } - } - flush(characters.length) - return { text, tokens } -} - -interface FixturePhraseMatch { - count: number - start: number - end: number -} - -/** Count exact contiguous token-phrase occurrences and retain the first display span. */ -function phraseMatch(document: readonly FixtureSearchToken[], phrase: readonly string[]): FixturePhraseMatch { - if (phrase.length === 0 || phrase.length > document.length) return { count: 0, start: 0, end: 0 } - let count = 0 - let firstStart = 0 - let firstEnd = 0 - for (let start = 0; start <= document.length - phrase.length; start++) { - if (!phrase.every((token, offset) => document[start + offset]?.value === token)) continue - count++ - if (count === 1) { - firstStart = document[start]?.start ?? 0 - firstEnd = document[start + phrase.length - 1]?.end ?? firstStart - } - } - return { count, start: firstStart, end: firstEnd } -} - -/** Match-centered fixture excerpt, bounded by Unicode code points for the sidebar. */ -function searchSnippet(value: string, matchStart: number, matchEnd: number): string { - const characters = Array.from(value) - if (characters.length <= 120) return value - const boundedStart = Math.min(Math.max(0, matchStart), characters.length - 1) - const boundedEnd = Math.min( - characters.length, - Math.max(boundedStart + 1, matchEnd), - ) - const center = Math.floor((boundedStart + boundedEnd) / 2) - let start = Math.min( - characters.length - 118, - Math.max(0, center - Math.floor(118 / 2)), - ) - let end = start + 118 - if (start === 0) { - end = 119 - } else if (end === characters.length) { - start = characters.length - 119 - } - return `${start > 0 ? '…' : ''}${characters.slice(start, end).join('')}${end < characters.length ? '…' : ''}` -} - -interface FixtureSearchCandidate { - sessionId: SessionId - seq: number - time: number - text: string - matchCount: number - matchStart: number - matchEnd: number - documentLength: number -} - -/** Mirrors `packages/session-query/session-query-sqlite/src/index.ts`; update both together. */ -function compareSearchCandidates(a: FixtureSearchCandidate, b: FixtureSearchCandidate): number { - if (a.matchCount !== b.matchCount) return b.matchCount - a.matchCount - if (a.documentLength !== b.documentLength) return a.documentLength - b.documentLength - if (a.time !== b.time) return b.time - a.time - if (a.sessionId !== b.sessionId) return a.sessionId < b.sessionId ? -1 : 1 - return b.seq - a.seq -} - -/** - * Current plan projection over the full log (host parallel: latest todo/write - * with no later turn/start; a new turn retires the previous plan). - */ -function backscanTodos(log: readonly SessionEvent[]): TodoItem[] | undefined { - for (let i = log.length - 1; i >= 0; i--) { - const event = log[i] - if (event === undefined) continue - if (event.type === 'turn/start') return undefined - if (event.type === 'todo/write') return event.data.todos - } - return undefined -} - -/** Fixture-local mirror of the goal projection value (dsh-goal's GoalProjection shape). */ -interface FxGoalProjection { - goal: { - id: string - revision: number - objective: string - phase: 'active' | 'paused' | 'blocked' | 'complete' - maxGoalRounds: number - } - roundsStarted: number - createdAt: number - updatedAt: number -} - -/** One durable goal change. */ -type FxGoalChange = - | { kind: 'goal/change'; version: 1; operation: 'clear'; cleared: { id: string; revision: number }; clearedAt: number } - | { - kind: 'goal/change' - version: 1 - operation: 'create' | 'edit' | 'pause' | 'resume' | 'complete' - goal: FxGoalProjection['goal'] - roundsStarted: number - createdAt: number - updatedAt: number - } - -/** - * Current goal projection over the full log (host parallel: the GoalService - * unit's last-wins fold of goal/change whole values; clear returns null). - */ -function backscanGoal(log: readonly SessionEvent[]): FxGoalProjection | null { - for (let i = log.length - 1; i >= 0; i--) { - const event = log[i] as unknown as { - type: string - data?: FxGoalChange - } | undefined - if (event === undefined || event.type !== 'goal/change' || event.data === undefined) continue - const change = event.data - if (change.operation === 'clear') return null - return { goal: change.goal, roundsStarted: change.roundsStarted, createdAt: change.createdAt, updatedAt: change.updatedAt } - } - return null -} - -interface StreamConn { - push(value: Value): void -} - -interface ReasoningChunkStormState { - sessionId: string - chunkCount: number - chunksPerInterval: number - intervalMs: number - emitted: number - marker: string - emitting: boolean -} - -/** Deterministic fixture branches used by keyless Web assembly tests. */ -export interface FixtureOptions { - /** Start with no real Workspace or Session. */ - empty?: boolean - /** Reject every prompt before appending its user event. */ - rejectPrompt?: boolean - /** Publish the Session but fail its Workspace account write. */ - failWorkspaceAttach?: boolean - /** Publish and frame the Session, then throw instead of returning create. */ - dropSessionCreateResponse?: boolean - /** Order of the two successful create frames. */ - createFrameOrder?: 'session-first' | 'workspace-first' - /** Announce one Agent write to `notes/demo.txt` shortly after a `workspaceFiles/changes` stream opens. */ - fileChanges?: boolean -} - -/** File observation payload, mirrored so this Client fixture names no Host package. */ -type FixtureWorkspaceFileChange = - | { readonly absolutePath: string; readonly version: string } - | { readonly absolutePath: string; readonly absent: true } - -/** Host subscription acknowledgement followed by file observations. */ -type FixtureWorkspaceFileWatchFrame = - | { readonly kind: 'ready' } - | { readonly kind: 'change'; readonly change: FixtureWorkspaceFileChange } - -/** Inbox pump shared by both stream generators (FrameQueue pattern: ONE abort listener hung - * outside the loop — a per-iteration {once:true} listener never fires for non-final rounds and - * piles up for the stream's lifetime). breakNow force-ends the stream without the - * client's signal (timing hook: simulated connection loss). */ -class FxInbox implements StreamConn { - private readonly inbox: Value[] = [] - private wake: (() => void) | null = null - private broken = false - - push(value: Value): void { - this.inbox.push(value) - this.wake?.() - } - - breakNow(): void { - this.broken = true - this.wake?.() - } - - /** Read through a method: breakNow()/abort flip state across yields, so narrowing from the loop condition must not stick. */ - private isLive(signal: AbortSignal): boolean { - return !signal.aborted && !this.broken - } - - async *drain(signal: AbortSignal): AsyncGenerator { - const onAbort = (): void => this.wake?.() - signal.addEventListener('abort', onAbort) - try { - while (this.isLive(signal)) { - while (this.inbox.length > 0) yield this.inbox.shift() as Value - if (!this.isLive(signal)) break - await new Promise((resolve) => { - this.wake = resolve - }) - this.wake = null - } - } finally { - signal.removeEventListener('abort', onAbort) - } - } -} - -/** Fixture RPC face over one in-memory state graph. */ -export interface FixtureWorld { - /** Generic Remote caller for the endpoints business services own. */ - readonly rpc: ClientConnectionRpc -} - -/** - * Build the fixture RPC face over one in-memory state graph. - * @param options - fixture branches for empty state and failure timing. - * @returns the Remote RPC face. - */ -export function createFixtureFaces(options: FixtureOptions = {}): FixtureWorld { - return createFixtureWorld(options) -} - -/** Build the fixture's Remote RPC face over one state graph. */ -function createFixtureWorld(options: FixtureOptions): FixtureWorld { - // The resident fixture sessions all carry history, so none of them is blank. - const sessions: FixtureSessionSummary[] = options.empty ? [] : [ - { sessionId: sid('fx-alpha'), updatedAt: Date.now(), running: true, blank: false, cwd: '/tmp/fixture' }, - { sessionId: sid('fx-beta'), updatedAt: Date.now() - 60_000, running: false, blank: false, parentSessionId: sid('fx-alpha'), cwd: '/tmp/fixture' }, - { sessionId: sid('fx-gamma'), updatedAt: Date.now() - 120_000, running: false, blank: false, cwd: '/tmp/fixture' }, - ] - const logs = new Map([[sid('fx-alpha'), buildAlphaLog()]]) - const goalActivations = new Map() - const modelSelections = new Map(sessions.map(session => [ - session.sessionId, - { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, - ])) - const attachments = new Map([[ - String(FIXTURE_IMAGE_REF.attachmentId), - { attachment: FIXTURE_IMAGE_REF, data: FIXTURE_IMAGE_DATA }, - ]]) - /** Credential store double: set/unset flip the describe badge, values never read back. */ - const fixtureCredentials = new Map([ - // The assembled fixture represents an already-configured shipped - // DeepSeek route so unrelated GUI journeys do not enter first-run setup. - ['DEEPSEEK_API_KEY', true], - ]) - - /** Canonical fixture implementation of the generated Settings Remote contract. */ - const settingsRemotes = { - // Only the resolved DeepSeek address needed by first-run readiness is - // represented here. Fixture-backed journeys do not open its Models editor; - // real schema-driven forms ride the HTTP transport. - describe(): RpcResult { - return { - ok: true, - value: { - writable: true, - hasDocument: true, - namespaces: [{ - ns: 'llm-deepseek', - schema: {}, - value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, - applies: 'live', - secrets: [{ path: ['apiKey'], set: false }], - revision: 0, - }], - }, - } - }, - update(ns: string): ConnectionRpcResult { - return { - ok: false, - error: { - code: 'settings/rejected', - message: 'fixture: the minimal readiness settings descriptor is read-only', - details: { ns }, - }, - } - }, - replace(ns: string): ConnectionRpcResult { - return { - ok: false, - error: { - code: 'settings/rejected', - message: 'fixture: the minimal readiness settings descriptor is read-only', - details: { ns }, - }, - } - }, - mutate(ns: string): ConnectionRpcResult { - // A Remote failure code is free-form, unlike the unary error vocabulary. - return { - ok: false, - error: { - code: 'settings/rejected', - message: 'fixture: no settings namespaces are registered', - details: { ns }, - }, - } - }, - openSettingsDocument(): RpcResult<{ opened: true }> { - return { ok: true, value: { opened: true } } - }, - openAgentPresetDirectory(agentPreset: string): RpcResult< - { opened: true } | { opened: false; path: string } - > { - const existing = fixturePresets.get(agentPreset) - if (existing === undefined || existing.trust === 'system') { - return { - ok: false, - error: { - code: 'agent-preset/read-only', - message: `agent preset "${agentPreset}" ships with the deployment`, - details: { agentPreset, reason: 'it ships with the deployment' }, - }, - } - } - return { ok: true, value: { opened: true } } - }, - } - - const credentialRemotes = { - describe(refs: readonly string[]): RpcResult> { - return { - ok: true, - value: Object.fromEntries(refs.map(ref => [ref, { - configured: fixtureCredentials.has(ref), - ...fixtureCredentials.has(ref) ? { source: 'file' } : {}, - writable: true, - }])), - } - }, - set(ref: string): RpcResult { - fixtureCredentials.set(ref, true) - return { ok: true, value: undefined } - }, - unset(ref: string): RpcResult { - fixtureCredentials.delete(ref) - return { ok: true, value: undefined } - }, - } - - /** - * Preset compositions the fixture serves. Held as state rather than - * constants so the settings editor's save and delete are exercisable: the - * roster a GUI journey sees after writing is the text it wrote. - */ - const fixturePresets = new Map([ - ['standard', { trust: 'system', content: "- id: tool-bash\n name: '@deepseek-ai/dsh-tool-bash'\n" }], - ['minimal', { trust: 'system', content: "- id: tool-web-search\n name: '@deepseek-ai/dsh-tool-web-search'\n" }], - ['my-agent', { trust: 'user', content: "- id: tool-read\n name: '@deepseek-ai/dsh-tool-read'\n" }], - ]) - let fixtureDefaultPreset = 'standard' - const nextTurn = new Map([[sid('fx-alpha'), 75]]) - let nextSession = 1 - // Workspace entities mirroring the host registry: the fixture sessions all - // live under one workspace, whose account carries them in attach order. - const wid = (raw: string): WorkspaceId => raw as WorkspaceId - const fixtureEpoch = new Date(Date.now() - 300_000).toISOString() - const FIXTURE_HOME = '/home/fixture' - const workspaces: FixtureWorkspace[] = options.empty ? [] : [{ - workspaceId: wid('fx-ws-fixture'), - path: '/tmp/fixture', - title: 'fixture', - sessionIds: [sid('fx-alpha'), sid('fx-beta'), sid('fx-gamma')], - createdAt: fixtureEpoch, - updatedAt: fixtureEpoch, - }, { - workspaceId: wid('fx-ws-home'), - path: `${FIXTURE_HOME}/Documents/project`, - title: 'project', - sessionIds: [], - createdAt: fixtureEpoch, - updatedAt: fixtureEpoch, - }] - let nextWorkspace = 1 - // Registry-global archive set mirroring the host: archived sessions keep - // their workspace accounting slot and only grouping surfaces hide them. - const archivedSessionIds: SessionId[] = [] - const workspaceSnapshot = (workspace: FixtureWorkspace): WorkspaceView => ({ - ...workspace, - sessionIds: [...workspace.sessionIds], - }) - const workspaceBaseline = (): Extract => ({ - type: 'baseline', - value: { - items: workspaces.map(workspaceSnapshot), - archivedSessionIds: [...archivedSessionIds], - }, - }) - - // In-memory browse tree behind the fixture's `browse` picker capability — - // deterministic content mirroring the design mock so assembled Web tests - // and snapshots can walk it. Leaves are materialized lazily: a child listed - // by its parent lists as empty until something is created inside it. - const directoryTree = new Map([ - ['/', ['home']], - ['/home', ['fixture']], - [FIXTURE_HOME, ['Documents', 'Downloads', '.config']], - [`${FIXTURE_HOME}/Documents`, [ - 'project', 'deepseek-iOS', 'deepseek-android', 'deepseek-platform', - 'deepseek-web', 'deepseek-harness', 'deepseek-app', 'deepseek-landing-blog', - ]], - ]) - const childrenOf = (path: string): string[] | undefined => { - const known = directoryTree.get(path) - if (known !== undefined) return known - const parent = path.slice(0, path.lastIndexOf('/')) || '/' - const name = path.slice(path.lastIndexOf('/') + 1) - return directoryTree.get(parent)?.includes(name) === true ? [] : undefined - } - const crumbsOf = (path: string): { name: string; path: string; hidden: boolean }[] => { - const crumbs = [{ name: '/', path: '/', hidden: false }] - let acc = '' - for (const segment of path.split('/').filter(Boolean)) { - acc += `/${segment}` - crumbs.push({ name: segment, path: acc, hidden: false }) - } - return crumbs - } - /** Resident waterfalls retain their event ids across Remote Event generations. */ - const pendingApprovalEventId = 'fx-interaction-approval' - let approvalPending = !options.empty - const pendingQuestionEventId = 'fx-interaction-question' - let questionPending = !options.empty - const fixtureQuestions: readonly FixtureQuestionItem[] = [ - { - id: 'harness-profile', - header: '偏好', - question: '你现在更想招哪类 Agent/Harness 候选人?', - options: [ - { label: '工程落地型 (Recommended)', description: '更看重能直接做 runtime、tool executor、sandbox、trace 和线上问题排查。' }, - { label: '研究潜力型', description: '更看重 Agent 理解、训练评测思路和长期成长空间。' }, - { label: '均衡型', description: '同时要求工程能力和 Agent 认知,但可能筛选门槛更高。' }, - ], - }, - { - id: 'work-mode', - header: '方式', - question: '你希望候选人优先展示哪种工作方式?', - options: [ - { label: '先做小型原型 (Recommended)', description: '用可运行结果尽快验证关键假设。' }, - { label: '先写完整设计', description: '先收敛边界、协议和风险,再开始实现。' }, - ], - }, - { - id: 'signals', - header: '信号', - question: '哪些面试信号最重要?', - detail: '按当前招聘目标选择;跳过则视为不设偏好。', - multiSelect: true, - options: [ - { label: '系统设计' }, - { label: '代码质量' }, - { label: 'Agent 产品判断' }, - ], - }, - ] - - const controlConns = new Set>() - const followConns = new Map>>() - interface FixtureAttemptState { - readonly attemptId: ReturnType - readonly startedAfterSeq: number - readonly turn: number - readonly step: number - readonly stream: AssistantStreamAccumulator - index: number - } - const activeAttempts = new Map() - const assistantRevisions = new Map() - const workspaceConns = new Set>() - const remoteEventConns = new Map>() - const emitControl = (frame: FixtureControlFrame): void => { - for (const conn of controlConns) conn.push(frame) - } - const emitWorkspace = (frame: Exclude): void => { - for (const conn of workspaceConns) conn.push(frame) - } - const emitRemote = (event: string, args: readonly unknown[]): void => { - for (const conn of remoteEventConns.values()) conn.push({ type: 'emit', event, args }) - } - const emitRemoteFrame = (frame: FixtureRemoteEventFrame): void => { - for (const conn of remoteEventConns.values()) conn.push(frame) - } - const emitFollow = (sessionId: SessionId, entry: FixtureHistoryEntry): void => { - for (const conn of followConns.get(sessionId) ?? []) conn.push(entry) - } - const emitAssistant = (sessionId: SessionId, frame: FixtureAssistantStreamFrame): void => { - for (const conn of followConns.get(sessionId) ?? []) conn.push({ type: 'assistant-stream', frame }) - } - const nextAssistantRevision = (sessionId: SessionId): number => { - const revision = (assistantRevisions.get(sessionId) ?? 0) + 1 - assistantRevisions.set(sessionId, revision) - return revision - } - const beginAssistant = (sessionId: SessionId, turn: number, step: number): FixtureAttemptState => { - const attemptId = LlmAttemptId(`${sessionId}:fixture:${String(nextAssistantRevision(sessionId))}`) - const lastSeq = logOf(sessionId).length - 1 - const startedAfterSeq = lastSeq < 0 ? -1 : SessionSeq(lastSeq) - const attempt = { - attemptId, startedAfterSeq, turn, step, - stream: new AssistantStreamAccumulator(), index: 0, - } - activeAttempts.set(sessionId, attempt) - emitAssistant(sessionId, { - type: 'start', attemptId, revision: assistantRevisions.get(sessionId) as number, - startedAfterSeq, turn, step, - }) - return attempt - } - const pushAssistant = (sessionId: SessionId, chunk: StreamChunk): void => { - const attempt = activeAttempts.get(sessionId) - if (attempt === undefined) throw new Error(`fixture: no active Assistant attempt for ${sessionId}`) - const timed = attempt.stream.push({ time: Date.now(), chunk }) - emitAssistant(sessionId, { - type: 'chunk', attemptId: attempt.attemptId, revision: nextAssistantRevision(sessionId), - index: attempt.index++, time: timed.time, chunk: timed.chunk as unknown as JsonValue, - }) - } - const commitAssistant = (sessionId: SessionId, event: SessionEvent): void => { - const attempt = activeAttempts.get(sessionId) - if (attempt === undefined) throw new Error(`fixture: no active Assistant attempt for ${sessionId}`) - activeAttempts.delete(sessionId) - emitAssistant(sessionId, { - type: 'end', attemptId: attempt.attemptId, revision: nextAssistantRevision(sessionId), - index: attempt.index, - outcome: { - kind: 'committed', - eventType: event.type === 'assistant/message' ? 'assistant/message' : 'assistant/attempt', - seq: event.seq, - }, - }) - } - - function sessionOk(value: T): Promise> { - return Promise.resolve({ ok: true, value }) - } - - function sessionErr(error: ConnectionRpcFailure): Promise> { - return Promise.resolve({ ok: false, error }) - } - - const summaryOf = (id: SessionId): FixtureSessionSummary | undefined => sessions.find(s => s.sessionId === id) - const requireRemoteSession = ( - request: { readonly sessionId: SessionId }, - ): Promise> | undefined => { - if (summaryOf(request.sessionId) !== undefined) return undefined - return sessionErr({ - code: 'session/not-found', - message: `no session ${request.sessionId}`, - details: { sessionId: request.sessionId }, - }) - } - const setRunning = (id: SessionId, running: boolean): void => { - const summary = summaryOf(id) - if (summary === undefined || summary.running === running) return - summary.running = running - emitRemote('api-session/status', [id, running]) - } - const logOf = (id: SessionId): SessionEvent[] => { - let log = logs.get(id) - if (log === undefined) { - log = [] - logs.set(id, log) - } - return log - } - const append = (id: SessionId, e: Record): SessionEvent => { - const log = logOf(id) - const event = { seq: SessionSeq(log.length), time: Date.now(), ...e } as unknown as SessionEvent - log.push(event) - emitFollow(id, { type: 'event', event }) - // Host eager-drive parallel: a unit-advancing event pushes its finished value. - for (const frame of projectionFramesOf(id, log, event)) emitControl(frame) - if (event.type === 'user/message' && event.data.source.kind === 'user') { - const summary = summaryOf(id) - if (summary !== undefined) summary.updatedAt = event.time - emitRemote('api-session/activity', [id, event.time]) - } - return event - } - - /** Append one durable goal/change (host GoalService parallel). */ - const appendGoalChange = (id: SessionId, change: FxGoalChange): FxGoalProjection => { - const log = logOf(id) - append(id, { - type: 'goal/change', - data: change, - }) - return backscanGoal(log) as FxGoalProjection - } - - /** Update process-local goal activation and publish the forwarded edge. */ - const setGoalActivation = (id: SessionId, activation: 'armed' | 'disarmed'): void => { - const current = backscanGoal(logOf(id)) - if (current === null) { - if (!goalActivations.delete(id)) return - emitRemote('goal/activation-changed', [{ sessionId: id }]) - return - } - const previous = goalActivations.get(id) - goalActivations.set(id, activation) - if (previous === activation) return - emitRemote('goal/activation-changed', [{ - sessionId: id, - goal: { id: current.goal.id, revision: current.goal.revision, activation }, - }]) - } - - type FxGoalRef = { id: string; revision: number } - type FxGoalView = FxGoalProjection['goal'] & { - roundsStarted: number - createdAt: number - updatedAt: number - activation: 'armed' | 'disarmed' - } - - const goalFailure = (message: string): RpcResult => ({ - ok: false, - error: { code: 'gateway/internal', message, details: {} }, - }) - - const requireGoalSession = (id: SessionId): RpcResult | undefined => ( - summaryOf(id) === undefined - ? { ok: false, error: { code: 'session/not-found', message: `no session ${id}`, details: { sessionId: id } } } - : undefined - ) - - /** Canonical fixture implementation of the generated Commands Remote contract. */ - const commandRemotes = { - list(id: SessionId): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - return { - ok: true, - value: [ - { name: 'compact', description: 'fixture:压缩当前会话上下文' }, - { name: 'echo', description: 'fixture:回显参数', input: { hint: 'text to echo' } }, - { definitionId: brandString('@deepseek-ai/dsh-command-goal'), name: 'goal', description: 'Set or view the goal for a long-running task', input: { hint: '', attachments: true } }, - { definitionId: brandString('@deepseek-ai/dsh-permission-presets'), name: 'permission', description: 'Switch the permission preset (sandbox mode + approval policy)', input: { hint: '' } }, - { definitionId: brandString('@deepseek-ai/dsh-plan-mode'), name: 'plan', description: 'Enter or leave plan mode', input: { hint: '[off|message]', attachments: true } }, - ], - } - }, - execute(id: SessionId, line: string, attachments: readonly unknown[] = []): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - // Structured split mirroring the Host parser: name + verbatim rawInput - // (separator whitespace included) — the run payload carries no line. - const match = /^\/(\S+)((?:\s.*)?)$/.exec(line.trim()) - const name = match?.[1] - const args = match?.[2] ?? '' - // Mirror the Host image policy AFTER command resolution, matching the - // executor's order (an unknown name answers undefined and logs no - // lifecycle): the declaration rejection covers every known command - // without `input.attachments`, and the two producer grammar rejections cover - // the declaring commands' control-only lines. The fixture stores no - // bytes, so an accepted batch is acknowledged and dropped. - const known = ['permission', 'goal', 'compact', 'echo', 'plan'] - if (attachments.length > 0 && name !== undefined && known.includes(name)) { - const rejection = name !== 'goal' && name !== 'plan' - ? `/${name} does not accept attachments` - : name === 'goal' && args.trim() === '' - ? 'Attachments only accompany a goal objective: /goal or /goal edit .' - : name === 'plan' && args.trim() === 'off' - ? 'Attachments cannot accompany /plan off.' - : undefined - if (rejection !== undefined) { - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - const result: CommandResult = { kind: 'error', text: rejection } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - } - } - if (name === 'permission') { - const preset = args.trim() - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - const spec = PERMISSION_PRESETS[preset] - let result: CommandResult - if (preset === '') { - const current = permissionSelectOf(logOf(id)).currentValue - result = { kind: 'success', text: `current preset ${current} (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } - } else if (spec === undefined) { - result = { kind: 'error', text: `unknown preset "${preset}" (available: ${Object.keys(PERMISSION_PRESETS).join(', ')})` } - } else { - if (permissionSelectOf(logOf(id)).currentValue !== preset) append(id, { type: 'permission/preset', data: { preset } }) - append(id, { type: 'sandbox/mode', data: { mode: spec.sandbox } }) - append(id, { type: 'approval/policy', data: { policy: spec.approval } }) - result = { kind: 'success', text: `preset ${preset}` } - } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - } - if (name === 'goal') { - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - const objective = args.trim() - const current = backscanGoal(logOf(id)) - let text: string - if (objective === '') { - text = current === null ? 'No goal is set. Usage: /goal ' : `Current goal: ${current.goal.objective}` - } else if (current !== null && current.goal.phase !== 'complete') { - text = `A goal already exists (${current.goal.objective}). Clear it first.` - } else { - const created = appendGoalChange(id, { - kind: 'goal/change', version: 1, operation: 'create', - goal: { id: `fx-goal-${logOf(id).length}`, revision: 1, objective, phase: 'active', maxGoalRounds: 256 }, - roundsStarted: 0, createdAt: Date.now(), updatedAt: Date.now(), - }) - setGoalActivation(id, 'armed') - text = `Goal created: ${created.goal.objective}` - } - const result: CommandResult = { kind: 'success', text } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - } - const running = summaryOf(id)?.running === true - const outcomes: Record = { - compact: 'fixture:已压缩(假动作)', - echo: args.trim(), - plan: args.trim() === 'off' - ? (running ? 'Leaving plan mode (applies from the next step).' : 'Plan mode off.') - : (running - ? 'Entering plan mode (applies from the next step). Use /plan off to leave.' - : 'Plan mode on. Use /plan off to leave.'), - } - const text = name === undefined ? undefined : outcomes[name] - if (name === undefined || text === undefined) return { ok: true, value: undefined } - const commandId = `fx-cmd-${logOf(id).length}` as CommandId - append(id, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }) - if (name === 'plan' && !running) { - const plan = foldPlan(logOf(id)) - if (plan.wanted !== null && plan.wanted !== plan.active) { - append(id, { type: 'plan/mode', data: { active: plan.wanted } }) - } - } - const result: CommandResult = { kind: 'success', ...text === '' ? {} : { text } } - append(id, { type: 'command/done', data: { commandId, ...result } }) - return { ok: true, value: { commandId, result } } - }, - } - - const goalView = (id: SessionId, projection: FxGoalProjection): FxGoalView => ({ - ...projection.goal, - roundsStarted: projection.roundsStarted, - createdAt: projection.createdAt, - updatedAt: projection.updatedAt, - activation: goalActivations.get(id) ?? (projection.goal.phase === 'active' ? 'armed' : 'disarmed'), - }) - - /** Canonical fixture implementation of the generated Goal Remote contract. */ - /** Canonical fixture implementation of the generated reference-discovery Remote contracts. */ - const referenceRemotes = { - files(id: SessionId, query: string): RpcResult<{ path: string; kind: 'file' | 'directory' }[]> { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const needle = query.toLocaleLowerCase() - const items = [ - { path: 'notes', kind: 'directory' as const }, - { path: 'README.md', kind: 'file' as const }, - { path: 'notes/demo.txt', kind: 'file' as const }, - ].filter(item => item.path.toLocaleLowerCase().includes(needle)) - return { ok: true, value: items } - }, - sessions(id: SessionId, query: string): RpcResult<{ - sessionId: SessionId - label: string - cwd?: string - createdAt: number - mention: string - }[]> { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const needle = query.toLocaleLowerCase() - const value = sessions - .filter(item => item.sessionId !== id) - .filter(item => String(item.sessionId).toLocaleLowerCase().includes(needle) - || item.cwd?.toLocaleLowerCase().includes(needle) === true) - .map((item) => { - const label = item.sessionId === sid('fx-beta') ? 'Fixture child session' : String(item.sessionId) - const encoded = btoa(JSON.stringify(item.sessionId)) - .replaceAll('+', '-') - .replaceAll('/', '_') - .replace(/=+$/u, '') - return { - sessionId: item.sessionId, - label, - ...item.cwd === undefined ? {} : { cwd: item.cwd }, - createdAt: item.updatedAt, - mention: `@[${label}](dsh-session:${encoded})`, - } - }) - return { ok: true, value } - }, - } - - /** - * Workspace text reads under `?fixture`. - * - * The sample content is deliberately more than one shape: the panel's states - * (text, oversized, unreadable) are only demonstrable if the fixture can - * produce each of them, and a preview that can only ever succeed hides its - * own failure rendering. - */ - // The Session workspace the replayed conversation writes into: `list` walks a - // fixed tree under it so the file-tree tab has directories, files, one entry - // of neither kind, and one cut listing to draw. - const WORKSPACE_FILES_ROOT = '/tmp/fixture' - type FixtureWorkspaceEntry = { name: string; type: 'file' | 'directory' | 'other'; size?: number } - const workspaceFileTree = new Map([ - ['', [ - { name: '.gitignore', type: 'file', size: 24 }, - { name: 'dev.sock', type: 'other' }, - { name: 'notes', type: 'directory' }, - { name: 'package.json', type: 'file', size: 512 }, - { name: 'README.md', type: 'file', size: 640 }, - { name: 'src', type: 'directory' }, - ]], - ['notes', [ - { name: 'demo.txt', type: 'file', size: 14 }, - { name: 'new-demo.txt', type: 'file', size: 14 }, - ]], - ['src', [ - { name: 'config.ts', type: 'file', size: 211 }, - { name: 'index.ts', type: 'file', size: 88 }, - { name: 'lib', type: 'directory' }, - ]], - ['src/lib', Array.from({ length: 24 }, (_, index) => ({ - name: `module-${String(index + 1).padStart(2, '0')}.ts`, - type: 'file' as const, - size: 96 + index, - }))], - ]) - /** Resolve a `list` argument to its workspace-relative path, or undefined when it leaves the root. */ - const workspaceFilePath = (path: string): string | undefined => { - const segments: string[] = [] - for (const segment of (path.startsWith('/') ? path : `${WORKSPACE_FILES_ROOT}/${path}`).split('/')) { - if (segment === '' || segment === '.') continue - if (segment === '..') { - segments.pop() - continue - } - segments.push(segment) - } - const absolute = `/${segments.join('/')}` - if (absolute !== WORKSPACE_FILES_ROOT && !absolute.startsWith(`${WORKSPACE_FILES_ROOT}/`)) return undefined - return absolute.slice(WORKSPACE_FILES_ROOT.length + 1) - } - // Page cap mirrored from the Host default so an over-limit request fails here too. - const WORKSPACE_FILE_PAGE_LINES = 5000 - /** - * Sample text for any readable path: a heading plus two lines of copy. The - * replayed conversation's `demo` files, and any `huge` path, run past two - * default pages so paging can be exercised without a real workspace. - */ - const workspaceFileLines = (path: string): string[] => { - const name = path.slice(path.lastIndexOf('/') + 1) - const head = [`# ${name}`, '', 'fixture 模式下的示例文本,用于验收侧栏的文本预览。', '真实构建从工作区读取同名文件。'] - return path.includes('demo') || path.includes('huge') - ? [...head, ...Array.from({ length: 12_000 }, (_, index) => `第 ${index + 5} 行:用于验收分页与滚动的长文本样本。`)] - : head - } - const workspaceFileRemotes = { - list(path: string): ConnectionRpcResult<{ - path: string - entries: readonly FixtureWorkspaceEntry[] - truncated: boolean - }> { - if (path.length === 0) { - return { ok: false, error: { code: 'gateway/bad-request', message: 'path is required', details: {} } } - } - const relative = workspaceFilePath(path) - if (relative === undefined) { - return { - ok: false, - error: { code: 'workspace-file/outside-workspace', message: `${path} is outside the workspace`, details: { path } }, - } - } - const entries = workspaceFileTree.get(relative) - if (entries === undefined) { - const cut = relative.lastIndexOf('/') - const name = relative.slice(cut + 1) - const sibling = workspaceFileTree.get(cut === -1 ? '' : relative.slice(0, cut))?.find(entry => entry.name === name) - if (sibling === undefined) { - return { ok: false, error: { code: 'workspace-file/not-found', message: `no entry at ${path}`, details: { path } } } - } - return { - ok: false, - error: { - code: 'workspace-file/not-directory', - message: `${path} is a ${sibling.type}`, - details: { path, kind: sibling.type === 'file' ? 'file' : 'other' }, - }, - } - } - return { ok: true, value: { path: relative, entries, truncated: relative === 'src/lib' } } - }, - read(path: string, range: { offset?: number; limit?: number }): ConnectionRpcResult<{ - absolutePath: string - version: string - bytes: number - offset: number - text: string - lines: number - eof: boolean - }> { - const located = workspaceFileRemotes.stat(path) - if (!located.ok) return located - const offset = range.offset ?? 1 - const limit = range.limit ?? WORKSPACE_FILE_PAGE_LINES - if (!Number.isInteger(offset) || offset < 1 || !Number.isInteger(limit) || limit < 1 || limit > WORKSPACE_FILE_PAGE_LINES) { - return { ok: false, error: { code: 'gateway/bad-request', message: 'offset and limit must be positive integers within the page cap', details: {} } } - } - const lines = workspaceFileLines(path) - const page = lines.slice(offset - 1, offset - 1 + limit) - return { - ok: true, - value: { - ...located.value, - offset, - text: page.join('\n'), - lines: page.length, - eof: offset - 1 + limit >= lines.length, - }, - } - }, - stat(path: string): ConnectionRpcResult<{ absolutePath: string; version: string; bytes: number }> { - if (path.length === 0) { - return { ok: false, error: { code: 'gateway/bad-request', message: 'path is required', details: {} } } - } - if (path.endsWith('.png') || path.endsWith('.bin')) { - return { - ok: false, - error: { code: 'workspace-file/not-text', message: `${path} is not UTF-8 text`, details: { path } }, - } - } - return { - ok: true, - value: { - absolutePath: path.startsWith('/') ? path : `/${path}`, - version: 'fx-v1', - bytes: new TextEncoder().encode(workspaceFileLines(path).join('\n')).byteLength, - }, - } - }, - } - - /** - * Canonical fixture implementation of the generated Directory Picker Remote - * contract. The pick is deterministic — the keyless lanes drive the full - * pick-then-adopt path without an OS chooser — over the same design-mock - * tree the browse primitives serve. - */ - const directoryPickerRemotes = { - pick(): ConnectionRpcResult { - return { ok: true, value: `${FIXTURE_HOME}/Documents/project` } - }, - list(path?: string): ConnectionRpcResult { - const target = path ?? FIXTURE_HOME - const children = childrenOf(target) - if (children === undefined) { - return { - ok: false, - error: { code: 'directory-picker/unreadable', message: `cannot list ${target}: not in the fixture tree`, details: { path: target } }, - } - } - return { - ok: true, - value: { - path: target, - home: FIXTURE_HOME, - crumbs: crumbsOf(target), - entries: [...children].sort((a, b) => a.localeCompare(b)) - .map(name => ({ name, path: target === '/' ? `/${name}` : `${target}/${name}`, hidden: name.startsWith('.') })), - // The fixture tree is tiny; no level ever reaches a backend bound. - truncated: false, - }, - } - }, - createDirectory(parent: string, name: string): ConnectionRpcResult { - const children = childrenOf(parent) - if (children === undefined) { - return { ok: false, error: { code: 'directory-picker/create-failed', message: `missing parent ${parent}`, details: { path: parent } } } - } - // Same root special case as list's entry paths: a plain join under '/' - // would mint '//name' and fork the tree's identity. - const target = parent === '/' ? `/${name}` : `${parent}/${name}` - if (children.includes(name)) { - return { ok: false, error: { code: 'directory-picker/exists', message: `${target} already exists`, details: { path: target } } } - } - directoryTree.set(parent, [...children, name]) - directoryTree.set(target, []) - return { ok: true, value: target } - }, - } - - const goalRemotes = { - get(id: SessionId): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const current = backscanGoal(logOf(id)) - return { ok: true, value: current === null ? undefined : goalView(id, current) } - }, - create(id: SessionId, request: { objective: string; maxGoalRounds?: number }): RpcResult<{ ref: FxGoalRef }> { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const current = backscanGoal(logOf(id)) - if (current !== null && current.goal.phase !== 'complete') { - return goalFailure(`goal "${current.goal.id}" already exists`) - } - const now = Date.now() - const projection = appendGoalChange(id, { - kind: 'goal/change', version: 1, operation: 'create', - goal: { - id: `fx-goal-${logOf(id).length}`, - revision: 1, - objective: request.objective, - phase: 'active', - maxGoalRounds: request.maxGoalRounds ?? 256, - }, - roundsStarted: 0, createdAt: now, updatedAt: now, - }) - setGoalActivation(id, 'armed') - return { ok: true, value: { ref: { id: projection.goal.id, revision: projection.goal.revision } } } - }, - edit(id: SessionId, ref: FxGoalRef, request: { objective?: string; maxGoalRounds?: number }): RpcResult { - return mutateGoal(id, ref, current => ({ - ...current.goal, - revision: current.goal.revision + 1, - ...request.objective === undefined ? {} : { objective: request.objective }, - ...request.maxGoalRounds === undefined ? {} : { maxGoalRounds: request.maxGoalRounds }, - })) - }, - pause(id: SessionId, ref: FxGoalRef): RpcResult { - return mutateGoal(id, ref, current => ( - current.goal.phase === 'active' - ? { ...current.goal, revision: current.goal.revision + 1, phase: 'paused' } - : undefined - )) - }, - resume(id: SessionId, ref: FxGoalRef): RpcResult { - return mutateGoal(id, ref, current => ( - current.goal.phase === 'paused' || current.goal.phase === 'blocked' || current.goal.phase === 'active' - ? { ...current.goal, revision: current.goal.revision + 1, phase: 'active' } - : undefined - )) - }, - complete(id: SessionId, ref: FxGoalRef): RpcResult { - return mutateGoal(id, ref, current => ( - current.goal.phase === 'complete' - ? undefined - : { ...current.goal, revision: current.goal.revision + 1, phase: 'complete' } - )) - }, - clear(id: SessionId, ref: FxGoalRef): RpcResult { - const resolved = resolveGoal(id, ref) - if (!resolved.ok) return resolved - const current = resolved.value - const tombstone = { id: current.goal.id, revision: current.goal.revision + 1 } - appendGoalChange(id, { - kind: 'goal/change', version: 1, operation: 'clear', cleared: tombstone, clearedAt: Date.now(), - }) - setGoalActivation(id, 'disarmed') - return { ok: true, value: tombstone } - }, - } - - /** Resolve one current goal revision for a canonical Remote mutation. */ - function resolveGoal(id: SessionId, ref: FxGoalRef): RpcResult { - const missing = requireGoalSession(id) - if (missing !== undefined) return missing - const current = backscanGoal(logOf(id)) - if (current === null || current.goal.id !== ref.id || current.goal.revision !== ref.revision) { - return goalFailure('stale or missing goal revision') - } - return { ok: true, value: current } - } - - /** Shared CAS mutation path behind the canonical Remote verbs. */ - function mutateGoal( - id: SessionId, - ref: FxGoalRef, - next: (current: FxGoalProjection) => FxGoalProjection['goal'] | undefined, - ): RpcResult { - const resolved = resolveGoal(id, ref) - if (!resolved.ok) return resolved - const current = resolved.value - const goal = next(current) - if (goal === undefined) { - return goalFailure(`invalid goal transition from "${current.goal.phase}"`) - } - const currentActivation = goalActivations.get(id) - ?? (current.goal.phase === 'active' ? 'armed' : 'disarmed') - const activation = goal.phase === 'active' - ? current.goal.phase === 'active' ? currentActivation : 'armed' - : 'disarmed' - const projection = appendGoalChange(id, { - kind: 'goal/change', version: 1, - operation: goal.phase === current.goal.phase ? 'edit' : goal.phase === 'paused' ? 'pause' : goal.phase === 'active' ? 'resume' : 'complete', - goal, roundsStarted: current.roundsStarted, createdAt: current.createdAt, updatedAt: Date.now(), - }) - setGoalActivation(id, activation) - return { ok: true, value: goalView(id, projection) } - } - - /** Canonical fixture implementation of the generated AgentPresets Remote contract. */ - const presetRemotes = { - // Both trusts appear, because a surface must present a locally authored - // preset differently from one the deployment vetted. - list(): RpcResult<{ - presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[] - authorable: boolean - modeSelectionEnabled: boolean - }> { - return { - ok: true, - value: { - presets: [...fixturePresets].map(([id, preset]) => ({ - id, - trust: preset.trust, - isDefault: id === fixtureDefaultPreset, - })), - authorable: true, - modeSelectionEnabled: true, - }, - } - }, - select(_id: SessionId, agentPreset: string): RpcResult { - fixtureDefaultPreset = agentPreset - return { ok: true, value: agentPreset } - }, - read(agentPreset: string): RpcResult<{ agentPreset: string; trust: 'system' | 'user'; content: string }> { - const preset = fixturePresets.get(agentPreset) - if (preset === undefined) { - return { - ok: false, - error: { - code: 'agent-preset/not-found', - message: `unknown agent preset "${agentPreset}"`, - details: { agentPreset, available: [...fixturePresets.keys()] }, - }, - } - } - return { ok: true, value: { agentPreset, trust: preset.trust, content: preset.content } } - }, - copy(from: string, id: string): RpcResult { - const source = fixturePresets.get(from) - if (source === undefined) { - return { - ok: false, - error: { - code: 'agent-preset/not-found', - message: `unknown agent preset "${from}"`, - details: { agentPreset: from, available: [...fixturePresets.keys()] }, - }, - } - } - if (fixturePresets.has(id)) { - return { - ok: false, - error: { - code: 'agent-preset/invalid', - message: `agent preset "${id}" already exists`, - details: { agentPreset: id, reason: 'already exists' }, - }, - } - } - fixturePresets.set(id, { trust: 'user', content: source.content }) - return { ok: true, value: undefined } - }, - deletePreset(id: string): RpcResult { - if (fixturePresets.get(id)?.trust === 'system') { - return { - ok: false, - error: { - code: 'agent-preset/read-only', - message: `agent preset "${id}" ships with the deployment`, - details: { agentPreset: id, reason: 'it ships with the deployment' }, - }, - } - } - fixturePresets.delete(id) - return { ok: true, value: undefined } - }, - } - - /** At most one in-flight replay per session; cancel clears it. */ - const replays = new Map; finish(aborted: boolean): void }>() - - /** History transit delay; the page snapshot is taken at request time. */ - let historyDelayMs = 0 - /** One-shot history failure (timing hook: a pre-disconnect history request already doomed when reconnect lands). */ - let failNextHistory = false - /** Force-enders for currently open stream generators (timing hook: simulated connection loss). */ - const streamBreakers = new Set<() => void>() - /** Retry scenarios opened by timing hooks and completed in a later browser assertion phase. */ - const retryScenarios = new Map() - /** The single opt-in browser stress producer; normal fixture journeys never start it. */ - let activeReasoningChunkStorm: ReasoningChunkStormState | null = null - - // Browser-only timing hooks for slow history, lost frames, and reconnects. - const timingHooks = { - setHistoryDelay(ms: number): void { - historyDelayMs = ms - }, - /** Fail the NEXT history call (after its transit delay) with a transport-level throw. */ - failNextHistory(): void { - failNextHistory = true - }, - /** Log append plus follow-stream delivery (the normal live path). */ - appendUser(id: string, msg: string): void { - append(sid(id), { type: 'user/message', surfaceOp: 'append', data: userMessage(text(msg)) }) - }, - /** Append a later durable title revision through the normal raw-event + control-frame path. */ - appendTitle(id: string, title: string): void { - const log = logOf(sid(id)) - const messageSeqs = log.filter(event => event.type === 'user/message').map(event => event.seq) - append(sid(id), { type: 'session/title', data: { title, messageSeqs, source: { kind: 'provider', provider: 'fixture' } } }) - }, - /** Disarm the single active goal without a durable phase change. */ - disarmOnlyGoal(): void { - const active = [...logs.entries()].filter(([, log]) => { - const current = backscanGoal(log) - return current?.goal.phase === 'active' - }) - if (active.length !== 1) { - throw new Error(`fixture: expected one active goal, found ${String(active.length)}`) - } - const [session] = active - if (session === undefined) return - setGoalActivation(session[0], 'disarmed') - }, - /** Start an externally paced reasoning stream for the opt-in browser stress lane. */ - startReasoningChunkStorm( - id: string, - chunkCount: number, - chunksPerInterval: number, - intervalMs: number, - ): string { - if (!Number.isSafeInteger(chunkCount) || chunkCount < 1) { - throw new Error('fixture: reasoning chunk count must be a positive safe integer') - } - if (!Number.isSafeInteger(chunksPerInterval) || chunksPerInterval < 1) { - throw new Error('fixture: reasoning chunks per interval must be a positive safe integer') - } - if (!Number.isSafeInteger(intervalMs) || intervalMs < 1) { - throw new Error('fixture: reasoning interval must be a positive safe integer') - } - if (activeReasoningChunkStorm?.emitting === true) { - throw new Error('fixture: reasoning chunk storm already running') - } - - const sessionId = sid(id) - const log = logOf(sessionId) - let turn = nextTurn.get(sessionId) ?? 0 - for (const event of log) { - const candidate = (event as unknown as { data?: { turn?: unknown } }).data?.turn - if (typeof candidate === 'number') turn = Math.max(turn, candidate + 1) - } - nextTurn.set(sessionId, turn + 1) - const marker = `REASONING_STRESS_COMPLETE:${String(turn)}:${String(chunkCount)}` - const state: ReasoningChunkStormState = { - sessionId: id, - chunkCount, - chunksPerInterval, - intervalMs, - emitted: 0, - marker, - emitting: true, - } - activeReasoningChunkStorm = state - - setRunning(sessionId, true) - append(sessionId, { type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } }) - append(sessionId, { - type: 'user/message', surfaceOp: 'append', - data: userMessage(text(`Reasoning chunk stress: ${String(chunkCount)} chunks.`)), - }) - append(sessionId, { type: 'step/start', data: { turn, step: 0 } }) - beginAssistant(sessionId, turn, 0) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'reasoning' }) - - const startedAt = Date.now() - const pump = (): void => { - const elapsedIntervals = Math.floor((Date.now() - startedAt) / intervalMs) + 1 - const due = Math.max(state.emitted + chunksPerInterval, elapsedIntervals * chunksPerInterval) - const end = Math.min(due, chunkCount) - for (let index = state.emitted; index < end; index++) { - const chunkText = index === chunkCount - 1 - ? `\n${marker}` - : index % 64 === 63 ? '推理\n' : '推理' - pushAssistant(sessionId, { type: 'reasoning-delta', index: 0, text: chunkText }) - } - state.emitted = end - if (end < chunkCount) { - setTimeout(pump, intervalMs) - } else { - state.emitting = false - } - } - setTimeout(pump, 0) - return marker - }, - /** Return a copy so browser probes cannot mutate the active producer. */ - reasoningChunkStormState(): ReasoningChunkStormState | null { - return activeReasoningChunkStorm === null ? null : { ...activeReasoningChunkStorm } - }, - /** Open one failed model step whose partial remains visible until llm/retry arrives. */ - beginModelRetry(id: string): void { - const sessionId = sid(id) - const turn = nextTurn.get(sessionId) ?? 0 - nextTurn.set(sessionId, turn + 1) - retryScenarios.set(sessionId, { turn, stepStarted: true }) - setRunning(sessionId, true) - append(sessionId, { type: 'turn/start', data: { turn } }) - append(sessionId, { type: 'user/message', surfaceOp: 'append', data: { content: text('请重试这个请求'), source: { kind: 'user' } } }) - append(sessionId, { type: 'step/start', data: { turn, step: 1 } }) - beginAssistant(sessionId, turn, 1) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'text' }) - pushAssistant(sessionId, { type: 'text-delta', index: 0, text: '应撤回的半截回复' }) - }, - /** Record one retry decision; the next attempt remains in the same step. */ - scheduleModelRetry(id: string, retry = 1, delayMs = 450): void { - const sessionId = sid(id) - const scenario = retryScenarios.get(sessionId) - if (scenario === undefined) throw new Error(`fixture: no model retry scenario for ${id}`) - if (!scenario.stepStarted) { - beginAssistant(sessionId, scenario.turn, 1) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'text' }) - pushAssistant(sessionId, { type: 'text-delta', index: 0, text: `第 ${String(retry)} 次应撤回的回复` }) - scenario.stepStarted = true - } - const failure = { code: 'TRANSPORT', message: '连接被重置' } - pushAssistant(sessionId, { type: 'finish', reason: { kind: 'error', failure } }) - const attempt = activeAttempts.get(sessionId) as FixtureAttemptState - const attemptEvent = append(sessionId, { - type: 'assistant/attempt', data: { turn: scenario.turn, step: 1, stream: attempt.stream.snapshot() }, - }) - commitAssistant(sessionId, attemptEvent) - append(sessionId, { - type: 'llm/retry', - data: { - turn: scenario.turn, step: 1, - provider: 'fixture', mode: 'normal', policyKey: 'fixture-normal', - retry, maxRetries: 2, delayMs, failure, - }, - }) - scenario.stepStarted = false - }, - /** Record one retry decision, then cancel its source turn before the retry starts. */ - cancelModelRetryDuringBackoff(id: string, delayMs = 450): void { - const sessionId = sid(id) - const scenario = retryScenarios.get(sessionId) - if (scenario === undefined) throw new Error(`fixture: no model retry scenario for ${id}`) - const failure = { code: 'TRANSPORT', message: '连接被重置' } - const active = activeAttempts.get(sessionId) - if (active !== undefined) { - pushAssistant(sessionId, { type: 'finish', reason: { kind: 'error', failure } }) - const attemptEvent = append(sessionId, { - type: 'assistant/attempt', data: { turn: scenario.turn, step: 1, stream: active.stream.snapshot() }, - }) - commitAssistant(sessionId, attemptEvent) - } - append(sessionId, { - type: 'llm/retry', - data: { - turn: scenario.turn, step: 1, - provider: 'fixture', mode: 'normal', policyKey: 'fixture-normal', - retry: 1, maxRetries: 2, delayMs, failure, - }, - }) - append(sessionId, { type: 'step/end', data: { turn: scenario.turn, step: 1 } }) - append(sessionId, { type: 'turn/end', data: { turn: scenario.turn, reason: { kind: 'aborted', reason: { kind: 'user' } }, - } }) - retryScenarios.delete(sessionId) - setRunning(sessionId, false) - }, - /** Finish the timing-hook retry with a finalized response in the open step. */ - completeModelRetry(id: string): void { - const sessionId = sid(id) - const scenario = retryScenarios.get(sessionId) - if (scenario === undefined) throw new Error(`fixture: no model retry scenario for ${id}`) - retryScenarios.delete(sessionId) - const completed = '重试后的完整回复' - beginAssistant(sessionId, scenario.turn, 1) - pushAssistant(sessionId, { type: 'block-start', index: 0, blockType: 'text' }) - pushAssistant(sessionId, { type: 'text-delta', index: 0, text: completed }) - pushAssistant(sessionId, { type: 'block-end', index: 0, block: { type: 'text', text: completed } }) - pushAssistant(sessionId, { type: 'finish', reason: { kind: 'stop' } }) - const attempt = activeAttempts.get(sessionId) as FixtureAttemptState - const message = append(sessionId, { - type: 'assistant/message', - surfaceOp: 'append', - data: { - turn: scenario.turn, - step: 1, - message: assistantMessage(text(completed)), - stream: attempt.stream.snapshot(), - }, - }) - commitAssistant(sessionId, message) - append(sessionId, { type: 'step/end', data: { turn: scenario.turn, step: 1 } }) - append(sessionId, { type: 'turn/end', data: { turn: scenario.turn, reason: { kind: 'completed' } } }) - setRunning(sessionId, false) - }, - /** Log append without follow delivery: a frame lost in transit that page repair must recover. */ - appendSilent(id: string, msg: string): void { - const log = logOf(sid(id)) - log.push({ type: 'user/message', surfaceOp: 'append', seq: SessionSeq(log.length), time: Date.now(), data: userMessage(text(msg)) } as unknown as SessionEvent) - }, - /** End every open stream generator (client sees both streams close -> reconnect + resync path). */ - breakStreams(): void { - for (const breakNow of [...streamBreakers]) breakNow() - }, - } - ;(globalThis as Record).__fxTiming = timingHooks - - /** Prompt replay: chunk typewriter (80ms/frame) -> assistant/message finalize -> turn/end + running flip. */ - const startReply = (id: SessionId, turn: number, replyText: string): void => { - const step = 0 - append(id, { type: 'step/start', data: { turn, step } }) - beginAssistant(id, turn, step) - pushAssistant(id, { type: 'block-start', index: 0, blockType: 'text' }) - /* v8 ignore next -- the ?? arm needs a null match, but every fixture reply is non-empty. */ - const pieces = replyText.match(/[\s\S]{1,6}/gu) ?? [replyText] - let i = 0 - const finish = (aborted: boolean): void => { - replays.delete(id) - const done = pieces.slice(0, i).join('') - pushAssistant(id, { type: 'block-end', index: 0, block: { type: 'text', text: done } }) - pushAssistant(id, { type: 'usage', usage: fixtureUsage(turn, step) }) - if (!aborted) pushAssistant(id, { type: 'finish', reason: { kind: 'stop' } }) - const attempt = activeAttempts.get(id) as FixtureAttemptState - const message = append(id, { - type: 'assistant/message', - surfaceOp: 'append', - data: { - turn, - step, - message: assistantMessage(text(done)), - stream: attempt.stream.snapshot(), - usage: fixtureUsage(turn, step), - ...(aborted ? { interrupted: true } : {}), - }, - }) - commitAssistant(id, message) - append(id, { type: 'step/end', data: { turn, step } }) - append(id, { type: 'turn/end', data: { - turn, - reason: aborted ? { kind: 'aborted', reason: { kind: 'user' } } : { kind: 'completed' }, - } }) - setRunning(id, false) - } - const tick = (): void => { - const piece = pieces[i] - if (piece === undefined) { - finish(false) - return - } - i++ - pushAssistant(id, { type: 'text-delta', index: 0, text: piece }) - replays.set(id, { timer: setTimeout(tick, 80), finish }) - } - replays.set(id, { timer: setTimeout(tick, 80), finish }) - } - - const sessionApi: FixtureSessionApi = { - list: _request => sessionOk({ items: [...sessions].sort((a, b) => b.updatedAt - a.updatedAt) }), - search: (request, signal) => { - if (signal.aborted) { - return sessionErr({ - code: 'gateway/cancelled', - message: 'fixture session search was aborted', - details: {}, - }) - } - const query = searchTokenSpans(request.query).tokens.map(token => token.value) - const matches = sessions.flatMap((summary) => { - const log = logs.get(summary.sessionId) ?? [] - const current = new Set(foldSurface(log).nodes) - const best = log.flatMap((event): FixtureSearchCandidate[] => { - if (!current.has(event.seq)) return [] - const eventText = searchEventText(event) - const document = searchTokenSpans(eventText) - const match = phraseMatch(document.tokens, query) - if (match.count === 0) return [] - return [{ - sessionId: summary.sessionId, - seq: event.seq, - time: event.time, - text: document.text, - matchCount: match.count, - matchStart: match.start, - matchEnd: match.end, - documentLength: Array.from(eventText).length, - }] - }).sort(compareSearchCandidates)[0] - return best === undefined ? [] : [best] - }).sort(compareSearchCandidates) - return sessionOk({ - items: matches.slice(0, FIXTURE_SESSION_SEARCH_RESULT_LIMIT).map(match => ({ - sessionId: match.sessionId, - snippet: searchSnippet(match.text, match.matchStart, match.matchEnd), - })), - hasMore: matches.length > FIXTURE_SESSION_SEARCH_RESULT_LIMIT, - }) - }, - create: async (request) => { - const workspace = request.workspaceId === undefined - ? undefined - : workspaces.find(w => w.workspaceId === request.workspaceId) - if (request.workspaceId !== undefined && workspace === undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - const cwd = workspace?.path ?? request.cwd ?? '/tmp/fixture' - const requestedId = request.sessionId - const attachWorkspace = (sessionId: SessionId): void => { - /* v8 ignore next -- callers enter only when a target Workspace exists. */ - if (workspace === undefined || workspace.sessionIds.includes(sessionId)) return - workspace.sessionIds = [sessionId, ...workspace.sessionIds] - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - const attachFailure = ( - sessionId: SessionId, - workspaceId: WorkspaceId, - ): Promise> => sessionErr({ - code: 'session/workspace-attach-failed' as const, - message: `fixture rejected Workspace attachment for ${sessionId}`, - details: { sessionId, workspaceId }, - }) - if (requestedId !== undefined) { - const existing = summaryOf(requestedId) - if (existing !== undefined) { - if (existing.cwd !== cwd) { - return sessionErr({ - code: 'session/conflict', - message: `session ${requestedId} already uses ${existing.cwd ?? 'no cwd'}`, - details: { sessionId: requestedId, requestedCwd: cwd, ...existing.cwd === undefined ? {} : { existingCwd: existing.cwd } }, - }) - } - if (workspace !== undefined && !workspace.sessionIds.includes(requestedId)) { - if (options.failWorkspaceAttach) return attachFailure(requestedId, workspace.workspaceId) - attachWorkspace(requestedId) - } - return sessionOk({ sessionId: requestedId }) - } - } - const created: FixtureSessionSummary = { - sessionId: requestedId ?? sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: true, cwd, - } - sessions.push(created) - modelSelections.set(created.sessionId, { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) - const emitSession = (): void => { - emitRemote('api-session/added', [created]) - } - if (workspace !== undefined && options.failWorkspaceAttach) { - emitSession() - return attachFailure(created.sessionId, workspace.workspaceId) - } - if (workspace !== undefined && options.createFrameOrder === 'workspace-first') { - attachWorkspace(created.sessionId) - emitSession() - } else { - emitSession() - if (workspace !== undefined) attachWorkspace(created.sessionId) - } - if (options.dropSessionCreateResponse) throw new Error('fixture: dropped session.create response after publication') - return sessionOk({ sessionId: created.sessionId }) - }, - rename: (request) => { - const missing = requireRemoteSession(request) - if (missing !== undefined) return missing - const { sessionId, title } = request - const normalized = title.trim().replace(/\s+/g, ' ') - if (normalized.length === 0) { - return sessionErr({ - code: 'session/title-invalid', - message: 'session title must contain visible characters', - details: { sessionId }, - }) - } - // The append emits the durable event and its control projection frame; - // the unary response settles the caller first. - append(sessionId, { - type: 'session/title', - data: { title: normalized, messageSeqs: [], source: { kind: 'user' } }, - }) - const appended = logOf(sessionId).at(-1) as SessionEvent - return sessionOk({ title: normalized, seq: appended.seq }) - }, - fork: (request) => { - const { sessionId, atSeq } = request - const source = summaryOf(sessionId) - if (source === undefined) { - return sessionErr({ - code: 'session/not-found', - message: `no session ${sessionId}`, - details: { sessionId }, - }) - } - const log = logs.get(sessionId) ?? [] - const lastSeq = log.at(-1)?.seq ?? -1 - const anchoredBoundary = atSeq === undefined - ? undefined - : log.find(e => e.type === 'turn/end' && e.seq >= atSeq) - const boundary = anchoredBoundary - ?? (atSeq === undefined || atSeq > lastSeq - ? log.findLast(e => e.type === 'turn/end') - : undefined) - if (boundary === undefined) { - return sessionErr({ - code: 'session/fork-unavailable', - message: atSeq !== undefined && atSeq <= lastSeq - ? `session ${sessionId} has not completed the turn containing event ${String(atSeq)}` - : `session ${sessionId} has no completed turn`, - details: { sessionId }, - }) - } - const cut = boundary.seq + 1 - const child: FixtureSessionSummary = { - sessionId: sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, blank: false, - parentSessionId: sessionId, - ...source.cwd === undefined ? {} : { cwd: source.cwd }, - } - logs.set(child.sessionId, log.slice(0, cut)) - sessions.push(child) - emitRemote('api-session/added', [child]) - const workspace = workspaces.find(w => w.sessionIds.includes(sessionId)) - if (workspace !== undefined) { - workspace.sessionIds = [child.sessionId, ...workspace.sessionIds] - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - return sessionOk({ sessionId: child.sessionId }) - }, - history: async (request) => { - const log = logs.get(request.sessionId) ?? [] - const throughSeq = request.throughSeq ?? log.length - 1 - const boundedLog = log.slice(0, throughSeq + 1) - // Snapshot at request time, then deliver after the transit delay. - const page = pageOf(boundedLog, request.beforeSeq, request.maxMessages ?? 50) - const doomed = failNextHistory - failNextHistory = false - const delay = historyDelayMs - if (delay > 0) await new Promise(resolve => setTimeout(resolve, delay)) - if (doomed) throw new Error('fixture: simulated history transport failure') - return sessionOk(page) - }, - selectModel: (request) => { - const selected: ModelSelection = { - provider: request.provider, - model: request.model, - ...request.reasoningEffort === undefined - ? {} - : { reasoningEffort: request.reasoningEffort }, - } - append(request.sessionId, { type: 'model/selection', data: selected }) - modelSelections.set(request.sessionId, selected) - return sessionOk({ selected }) - }, - prompt: (request) => { - const { sessionId: id, mode, content } = request - const summary = summaryOf(id) - if (summary === undefined) { - return sessionErr({ code: 'session/not-found', message: `no session ${id}`, details: { sessionId: id } }) - } - if (options.rejectPrompt) { - if (content.some(block => block.type === 'image')) { - return sessionErr({ - code: 'session/attachment-invalid', - message: 'fixture: image side exceeds the deployment limit', - details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' }, - }) - } - return sessionErr({ - code: 'session/agent-busy', - message: 'fixture: prompt rejected before acceptance', - details: { reason: 'fixture-prompt-rejection' }, - }) - } - summary.updatedAt = Date.now() - // First accepted prompt appends events: the summary stops being blank. - summary.blank = false - const userText = content.map(b => (b.type === 'text' ? b.text : '')).join('') - const durable: ContentBlock[] = content.map((block) => { - if (block.type === 'text') return block - const attachment: ImageAttachmentRef = { - attachmentId: `fixture:${randomUuid()}` as AttachmentIdType, - mediaType: block.mediaType, - bytes: Math.max( - 1, - Math.floor(block.data.length * 3 / 4) - - (block.data.endsWith('==') ? 2 : block.data.endsWith('=') ? 1 : 0), - ), - width: 160, - height: 90, - ...block.name === undefined ? {} : { name: block.name }, - } - attachments.set(String(attachment.attachmentId), { attachment, data: block.data }) - return { type: 'image', attachment } - }) - // The host echoes the prompt's requestId as the user source's rpcId; - // the Session object retires its local submission echo on it. The - // user-rpc source member is declared by dsh-api-session-controller, - // which this standalone fixture does not import — hence the assertion. - const promptSource = { kind: 'user', rpcId: request.requestId } as MessageSource - if (mode === 'steer' && replays.has(id)) { - // Steering: the durable user/message lands inside the current turn; the replay continues. - append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(durable, promptSource) }) - return sessionOk({ accepted: true as const }) - } - const turn = nextTurn.get(id) ?? 0 - nextTurn.set(id, turn + 1) - setRunning(id, true) - append(id, { type: 'turn/start', data: { turn } }) - // Boundary flush parallel (the host's step/start observer): an outstanding - // /plan selection commits as plan/mode inside the opened turn. - const plan = foldPlan(logOf(id)) - if (plan.wanted !== null && plan.wanted !== plan.active) { - append(id, { type: 'plan/mode', data: { active: plan.wanted } }) - } - append(id, { type: 'user/message', surfaceOp: 'append', data: userMessage(durable, promptSource) }) - // Capacity parallel of the host token-meter's request/context record: - // log-only, appended inside the open turn, and deduplicated against the - // route already recorded (the fixture never varies contextWindow). - const selection = modelSelections.get(id) ?? { provider: 'deepseek', model: 'deepseek-v4-flash' } - const previousHeader = logOf(id).findLast(event => event.type === 'request/header') - const previousSelection = previousHeader?.type === 'request/header' - ? { - provider: previousHeader.data.header.config.provider, - model: previousHeader.data.header.config.model, - ...(previousHeader.data.header.config.reasoningEffort === undefined - ? {} - : { reasoningEffort: previousHeader.data.header.config.reasoningEffort }), - } - : null - if (!sameModelSelection(previousSelection, selection)) { - append(id, { - type: 'request/header', - data: { - header: { config: selection }, - reason: previousHeader === undefined ? 'initial' : 'change', - }, - }) - } - if (lastRequestContext(logOf(id))?.model !== selection.model) { - append(id, { - type: 'request/context', - data: { provider: selection.provider, model: selection.model, contextWindow: 128_000 }, - }) - } - startReply( - id, - turn, - userText === 'render markdown' - ? MARKDOWN_FIXTURE - : userText === 'report model' - ? (() => { - const selection = modelSelections.get(id) - return `当前模型:${selection?.provider ?? 'unknown'}/${selection?.model ?? 'unknown'}` - + (selection?.reasoningEffort === undefined ? '' : ` · 推理等级:${selection.reasoningEffort}`) - })() - : `回声:${userText}。这是 fixture 的流式回复,用于验证打字机增长与定稿切换。`, - ) - return sessionOk({ accepted: true as const }) - }, - attachment: (request) => { - const stored = attachments.get(String(request.attachmentId)) - if (stored === undefined) { - return sessionErr({ - code: 'session/attachment-invalid', - message: 'fixture attachment missing', - details: { reason: 'ATTACHMENT_NOT_FOUND' }, - }) - } - if (!logReferencesAttachment( - logs.get(request.sessionId) ?? [], - String(request.attachmentId), - )) { - return sessionErr({ - code: 'session/attachment-invalid', - message: 'fixture attachment is not referenced by this session', - details: { reason: 'ATTACHMENT_NOT_REFERENCED' }, - }) - } - return sessionOk(stored) - }, - updateQueue: request => sessionErr({ - code: 'session/queue-item-not-found', - message: 'fixture has no pending queue item', - details: { itemId: request.itemId }, - }), - cancel: (request) => { - const replay = replays.get(request.sessionId) - if (replay !== undefined) { - clearTimeout(replay.timer) - replay.finish(true) - } else { - setRunning(request.sessionId, false) - } - return sessionOk({ accepted: true as const }) - }, - } - - const controlBaseline = (): Extract => { - const queues: Record = {} - const jobs: Record = {} - const projections: Record = {} - for (const summary of sessions) { - queues[summary.sessionId] = [] - jobs[summary.sessionId] = [] - const log = logs.get(summary.sessionId) ?? [] - projections[summary.sessionId] = { - asOfSeq: log.length - 1, - values: projectionValuesOf(log), - } - } - return { - type: 'baseline', - value: { - queues, - jobs, - approvals: [], - questions: [], - projections, - }, - } - } - - const approvalInvocation = (): FixtureRemoteEventInvocationFrame => ({ - type: 'waterfall', - event: 'approval/request', - eventId: pendingApprovalEventId, - agentId: sid('fx-alpha'), - request: { - toolName: 'dangerous_tool', - reason: 'fixture 常驻审批(可答:批准/拒绝后消失)', - }, - }) - - const questionInvocation = (): FixtureRemoteEventInvocationFrame => ({ - type: 'waterfall', - event: 'user-questions/request', - eventId: pendingQuestionEventId, - agentId: sid('fx-alpha'), - request: { - questions: fixtureQuestions, - }, - }) - - async function* openControl(signal: AbortSignal): AsyncGenerator { - signal.throwIfAborted() - const conn = new FxInbox() - controlConns.add(conn) - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - try { - yield controlBaseline() - yield* conn.drain(signal) - } finally { - streamBreakers.delete(breakNow) - controlConns.delete(conn) - } - } - - async function* openWorkspace(signal: AbortSignal): AsyncGenerator { - signal.throwIfAborted() - const conn = new FxInbox() - workspaceConns.add(conn) - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - try { - yield workspaceBaseline() - yield* conn.drain(signal) - } finally { - streamBreakers.delete(breakNow) - workspaceConns.delete(conn) - } - } - - async function* openWorkspaceFileChanges(signal: AbortSignal): AsyncGenerator { - signal.throwIfAborted() - const conn = new FxInbox() - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - // Opt-in only: an unprompted frame would age every preview of demo.txt - // into its changed state on a timer the assembled snapshots cannot see. - const announce = options.fileChanges - ? setTimeout(() => { - conn.push({ kind: 'change', change: { absolutePath: `${WORKSPACE_FILES_ROOT}/notes/demo.txt`, version: 'fx-demo-v2' } }) - }, 1000) - : undefined - try { - yield { kind: 'ready' } - yield* conn.drain(signal) - } finally { - if (announce !== undefined) clearTimeout(announce) - streamBreakers.delete(breakNow) - } - } - - async function* openRemoteEvents( - signal: AbortSignal, - ): AsyncGenerator { - signal.throwIfAborted() - const clientId = randomUuid() - const conn = new FxInbox() - remoteEventConns.set(clientId, conn) - // Periodic material for the RPC-panel acceptance: flip fx-gamma every 5s. - // fx-gamma only; the conversation replay owns fx-alpha's running state. - const timer = setInterval(() => { - const gamma = summaryOf(sid('fx-gamma')) - /* v8 ignore next -- the fixture never removes fx-gamma. */ - if (gamma !== undefined) setRunning(gamma.sessionId, !gamma.running) - }, 5000) - try { - yield { type: 'ready', clientId, host: { home: FIXTURE_HOME } } - if (approvalPending) yield approvalInvocation() - if (questionPending) yield questionInvocation() - yield* conn.drain(signal) - } finally { - clearInterval(timer) - remoteEventConns.delete(clientId) - } - } - - async function* openFollow( - request: FixtureFollowRequest, - signal: AbortSignal, - ): AsyncGenerator { - signal.throwIfAborted() - const sessionId = request.address.kind === 'session' - ? request.address.sessionId - : request.address.childSessionId - if (summaryOf(sessionId) === undefined) throw new Error(`fixture: no session ${sessionId}`) - const conn = new FxInbox() - let conns = followConns.get(sessionId) - if (conns === undefined) { - conns = new Set() - followConns.set(sessionId, conns) - } - conns.add(conn) - const breakNow = (): void => { conn.breakNow() } - streamBreakers.add(breakNow) - const snapshot = [...logOf(sessionId)] - const cursor = snapshot.at(-1)?.seq ?? -1 - const summary = summaryOf(sessionId) - /* v8 ignore next -- existence was checked before the stream registered. */ - if (summary === undefined) throw new Error(`fixture: no session ${sessionId}`) - const initial = pageOf(snapshot, undefined, request.maxMessages ?? 50) - let nextSeq = cursor + 1 - try { - yield { - type: 'snapshot', - header: { - version: SESSION_FORMAT_VERSION, - id: sessionId, - createdAt: summary.updatedAt, - ...(summary.cwd === undefined ? {} : { cwd: summary.cwd }), - ...(summary.parentSessionId === undefined ? {} : { parentSession: summary.parentSessionId }), - isSeeded: summary.parentSessionId !== undefined, - ...(summary.origin === undefined ? {} : { origin: summary.origin }), - ...(summary.agentPreset === undefined ? {} : { agentPreset: summary.agentPreset }), - }, - cursor, - records: initial.records, - hasMore: initial.hasMore, - projections: { asOfSeq: cursor, values: projectionValuesOf(snapshot) }, - ...(request.assistantStream === true ? { - assistantStream: { - revision: assistantRevisions.get(sessionId) ?? 0, - ...(activeAttempts.get(sessionId) === undefined - ? {} - : { activeAttempt: { - attemptId: (activeAttempts.get(sessionId) as FixtureAttemptState).attemptId, - startedAfterSeq: (activeAttempts.get(sessionId) as FixtureAttemptState).startedAfterSeq, - turn: (activeAttempts.get(sessionId) as FixtureAttemptState).turn, - step: (activeAttempts.get(sessionId) as FixtureAttemptState).step, - nextIndex: (activeAttempts.get(sessionId) as FixtureAttemptState).index, - stream: (activeAttempts.get(sessionId) as FixtureAttemptState).stream.snapshot(), - } }), - }, - } : {}), - } - for await (const frame of conn.drain(signal)) { - if (frame.type === 'assistant-stream') { - yield frame - continue - } - if (frame.event.seq < nextSeq) continue - if (frame.event.seq !== nextSeq) { - throw new Error(`fixture: session event stream skipped seq ${String(nextSeq)}`) - } - nextSeq++ - yield frame - } - } finally { - streamBreakers.delete(breakNow) - conns.delete(conn) - if (conns.size === 0) followConns.delete(sessionId) - } - } - - const answerRemoteEvent = (result: FixtureRemoteEventResult): ConnectionRpcResult => { - if (!remoteEventConns.has(result.clientId)) { - return { - ok: false, - error: { - code: 'gateway/invocation-unavailable', - message: 'fixture Remote event result identifies no active event stream', - details: {}, - }, - } - } - if (result.eventId === pendingApprovalEventId) { - if (!approvalPending) return { ok: true, value: undefined } - approvalPending = false - } else if (result.eventId === pendingQuestionEventId) { - if (!questionPending) return { ok: true, value: undefined } - questionPending = false - } else { - return { ok: true, value: undefined } - } - emitRemoteFrame({ type: 'cancel', eventId: result.eventId }) - return { ok: true, value: undefined } - } - - const workspaceApi: FixtureWorkspaceApi = { - create: (request) => { - const existing = workspaces.find(workspace => workspace.path === request.path) - if (existing !== undefined) { - return sessionOk({ workspace: workspaceSnapshot(existing), created: false }) - } - const now = new Date().toISOString() - const created: FixtureWorkspace = { - workspaceId: wid(`fx-ws-${nextWorkspace++}`), - path: request.path, - title: request.path.split('/').filter(Boolean).at(-1) ?? request.path, - sessionIds: [], - createdAt: now, - updatedAt: now, - } - workspaces.unshift(created) - const workspace = workspaceSnapshot(created) - emitWorkspace({ type: 'upsert', workspace }) - return sessionOk({ workspace, created: true }) - }, - rename: (request) => { - const workspace = workspaces.find(candidate => candidate.workspaceId === request.workspaceId) - if (workspace === undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - const title = request.title.trim() - if (title === '') { - return sessionErr({ - code: 'gateway/bad-request', - message: 'Workspace rename requires a non-blank title', - details: {}, - }) - } - if (title !== workspace.title) { - if (workspaces.some(candidate => candidate.workspaceId !== request.workspaceId && candidate.title === title)) { - return sessionErr({ - code: 'workspace/name-conflict', - message: `workspace name '${title}' is already in use`, - details: { name: title }, - }) - } - workspace.title = title - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - return sessionOk({ workspace: workspaceSnapshot(workspace) }) - }, - delete: (request) => { - const index = workspaces.findIndex(workspace => workspace.workspaceId === request.workspaceId) - if (index === -1) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - workspaces.splice(index, 1) - emitWorkspace({ type: 'remove', workspaceId: request.workspaceId }) - return sessionOk({ deleted: true }) - }, - insertBefore: (request) => { - const source = workspaces.findIndex(workspace => workspace.workspaceId === request.workspaceId) - const anchor = request.beforeWorkspaceId === undefined - ? workspaces.length - : workspaces.findIndex(workspace => workspace.workspaceId === request.beforeWorkspaceId) - const missing = source === -1 - ? request.workspaceId - : anchor === -1 - ? request.beforeWorkspaceId - : undefined - if (missing !== undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${missing}`, - details: { workspaceId: missing }, - }) - } - if (request.beforeWorkspaceId !== request.workspaceId) { - const previousOrder = workspaces.map(workspace => workspace.workspaceId) - const [workspace] = workspaces.splice(source, 1) - /* v8 ignore next -- source was resolved from the same array immediately above. */ - if (workspace === undefined) throw new Error(`fixture lost workspace ${request.workspaceId}`) - const at = request.beforeWorkspaceId === undefined - ? workspaces.length - : workspaces.findIndex(candidate => candidate.workspaceId === request.beforeWorkspaceId) - workspaces.splice(at, 0, workspace) - if (workspaces.some((candidate, index) => candidate.workspaceId !== previousOrder[index])) { - emitWorkspace({ - type: 'order', - workspaceIds: workspaces.map(candidate => candidate.workspaceId), - }) - } - } - return sessionOk({ workspaceIds: workspaces.map(candidate => candidate.workspaceId) }) - }, - insertSessionBefore: (request) => { - const workspace = workspaces.find(candidate => candidate.workspaceId === request.workspaceId) - if (workspace === undefined) { - return sessionErr({ - code: 'workspace/not-found', - message: `no workspace ${request.workspaceId}`, - details: { workspaceId: request.workspaceId }, - }) - } - if (!workspace.sessionIds.includes(request.sessionId) - || (request.beforeSessionId !== undefined && !workspace.sessionIds.includes(request.beforeSessionId))) { - return sessionErr({ - code: 'workspace/move-invalid', - message: `session or anchor is not accounted by workspace ${request.workspaceId}`, - details: { - workspaceId: request.workspaceId, - sessionId: request.sessionId, - ...request.beforeSessionId === undefined ? {} : { beforeSessionId: request.beforeSessionId }, - }, - }) - } - const without = workspace.sessionIds.filter(id => id !== request.sessionId) - const at = request.beforeSessionId === undefined ? without.length : without.indexOf(request.beforeSessionId) - const sessionIds = [...without.slice(0, at), request.sessionId, ...without.slice(at)] - if (!sessionIds.every((id, index) => id === workspace.sessionIds[index])) { - workspace.sessionIds = sessionIds - workspace.updatedAt = new Date().toISOString() - emitWorkspace({ type: 'upsert', workspace: workspaceSnapshot(workspace) }) - } - return sessionOk({ workspace: workspaceSnapshot(workspace) }) - }, - archiveSession: (request) => { - if (summaryOf(request.sessionId) === undefined) { - return sessionErr({ - code: 'session/not-found', - message: `no session ${request.sessionId}`, - details: { sessionId: request.sessionId }, - }) - } - if (!archivedSessionIds.includes(request.sessionId)) { - archivedSessionIds.push(request.sessionId) - emitWorkspace({ type: 'archived', archivedSessionIds: [...archivedSessionIds] }) - } - return sessionOk({ archivedSessionIds: [...archivedSessionIds] }) - }, - unarchiveSession: (request) => { - if (archivedSessionIds.includes(request.sessionId)) { - archivedSessionIds.splice(archivedSessionIds.indexOf(request.sessionId), 1) - emitWorkspace({ type: 'archived', archivedSessionIds: [...archivedSessionIds] }) - } - return sessionOk({ archivedSessionIds: [...archivedSessionIds] }) - }, - } - - const rpc: ClientConnectionRpc = { - call(channel, endpoint, payload, signal) { - if (channel !== '/api') { - return Promise.reject(new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`)) - } - const args = (payload as { - args: Readonly<{ - agentId: SessionId - line?: string - query?: string - path?: string - range?: { offset?: number; limit?: number } - name?: string - images?: readonly unknown[] - // A goal ref and a credential reference name share this wire field name. - ref?: string | { id: string; revision: number } - refs?: readonly string[] - value?: string - ns?: string - settingsNs?: string - agentPreset?: string - from?: string - id?: string - request?: unknown - _request?: unknown - }> - }).args - const sessionId = args.agentId - const callSignal = signal ?? new AbortController().signal - const request = args.request - switch (endpoint) { - case 'commands/list': return Promise.resolve(commandRemotes.list(sessionId)) - case 'commands/execute': return Promise.resolve(commandRemotes.execute(sessionId, args.line as string, args.images ?? [])) - case 'fileReferences/list': return Promise.resolve(referenceRemotes.files(sessionId, args.query ?? '')) - case 'sessionReferenceResolver/candidates': return Promise.resolve(referenceRemotes.sessions(sessionId, args.query ?? '')) - case 'directoryPicker/pick': return Promise.resolve(directoryPickerRemotes.pick()) - case 'directoryPicker/list': return Promise.resolve(directoryPickerRemotes.list(args.path)) - case 'directoryPicker/createDirectory': - return Promise.resolve(directoryPickerRemotes.createDirectory(args.path ?? '', args.name ?? '')) - case 'goals/get': return Promise.resolve(goalRemotes.get(sessionId)) - case 'goals/create': return Promise.resolve(goalRemotes.create(sessionId, { - objective: (request as { objective?: string } | undefined)?.objective as string, - ...(request as { maxGoalRounds?: number } | undefined)?.maxGoalRounds === undefined - ? {} - : { maxGoalRounds: (request as { maxGoalRounds: number }).maxGoalRounds }, - })) - case 'goals/edit': return Promise.resolve(goalRemotes.edit( - sessionId, - args.ref as FxGoalRef, - request as { objective?: string; maxGoalRounds?: number }, - )) - case 'goals/pause': return Promise.resolve(goalRemotes.pause(sessionId, args.ref as FxGoalRef)) - case 'goals/resume': return Promise.resolve(goalRemotes.resume(sessionId, args.ref as FxGoalRef)) - case 'goals/complete': return Promise.resolve(goalRemotes.complete(sessionId, args.ref as FxGoalRef)) - case 'goals/clear': return Promise.resolve(goalRemotes.clear(sessionId, args.ref as FxGoalRef)) - case 'agentPresets/list': return Promise.resolve(presetRemotes.list()) - case 'agentPresets/select': return Promise.resolve(presetRemotes.select(sessionId, args.agentPreset as string)) - case 'agentPresets/read': return Promise.resolve(presetRemotes.read(args.agentPreset as string)) - case 'agentPresets/copy': return Promise.resolve(presetRemotes.copy(args.from as string, args.id as string)) - case 'agentPresets/deletePreset': return Promise.resolve(presetRemotes.deletePreset(args.id as string)) - case 'subagents/list': return Promise.resolve({ - ok: true, - value: { entries: [], parentAvailable: true }, - }) - case 'subagents/prompt': return Promise.resolve({ - ok: true, - value: { - messageId: `fixture-message-${(request as { childSessionId: SessionId }).childSessionId}`, - }, - }) - case 'subagents/interruptByParent': return Promise.resolve({ ok: true, value: { accepted: true } }) - case 'credentials/describe': return Promise.resolve(credentialRemotes.describe(args.refs ?? [])) - case 'credentials/set': return Promise.resolve(credentialRemotes.set(args.ref as string)) - case 'credentials/unset': return Promise.resolve(credentialRemotes.unset(args.ref as string)) - case 'settings/describe': return Promise.resolve(settingsRemotes.describe()) - case 'settings/canOpenAgentPresetDirectory': return Promise.resolve({ ok: true, value: true }) - case 'settings/openSettingsDocument': return Promise.resolve(settingsRemotes.openSettingsDocument()) - case 'settings/openAgentPresetDirectory': return Promise.resolve( - settingsRemotes.openAgentPresetDirectory(args.agentPreset as string), - ) - case 'skills/list': { - const skillRequest = request as { readonly sessionId: SessionId } - const missing = requireRemoteSession(skillRequest) - if (missing !== undefined) return missing - return sessionOk({ - skills: [ - { name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true }, - { name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false }, - ], - }) - } - case 'session/openWorkspacePath': { - return sessionOk({ opened: true as const }) - } - case 'workspaceFiles/read': { - return Promise.resolve(workspaceFileRemotes.read(args.path ?? '', args.range ?? {})) - } - case 'workspaceFiles/stat': { - return Promise.resolve(workspaceFileRemotes.stat(args.path ?? '')) - } - case 'workspaceFiles/list': { - return Promise.resolve(workspaceFileRemotes.list(args.path ?? '')) - } - case 'session/canOpenWorkspacePath': return Promise.resolve({ ok: true, value: true }) - case 'session/modelCatalog': return Promise.resolve({ - ok: true, - value: { - default: { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, - routableProviders: ['deepseek-official', 'openai', 'acme-gateway'], - groups: fixtureModelGroups(), - failures: [], - }, - }) - case 'llm/listProviders': return Promise.resolve({ - ok: true, - value: [ - { id: 'deepseek-official', name: 'DeepSeek' }, - { id: 'openai', name: 'openai' }, - { id: 'acme-gateway', name: 'Acme Gateway' }, - ], - }) - case 'llm/listConfigurableProviders': return Promise.resolve({ - ok: true, - value: [ - { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] }, - { provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], declared: false }, - { provider: 'anthropic', displayName: 'anthropic', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'anthropic'], declared: false }, - { provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], declared: true }, - ], - }) - // The fixture endpoint is imaginary, so interrogation answers the - // catalog it already serves without a network request. - case 'llm/discoverModels': return Promise.resolve({ - ok: true, - value: fixtureModelGroups().flatMap(group => group.models.map(model => ({ id: model.id, name: model.name }))), - }) - case 'settings/update': return Promise.resolve(settingsRemotes.update(args.ns as string)) - case 'settings/replace': return Promise.resolve(settingsRemotes.replace(args.ns as string)) - case 'settings/mutate': return Promise.resolve(settingsRemotes.mutate(args.ns as string)) - case 'session/list': return sessionApi.list( - args._request as Parameters[0], - ) - case 'session/search': return sessionApi.search( - request as Parameters[0], - callSignal, - ) - case 'session/create': return sessionApi.create( - request as Parameters[0], - ) - case 'session/selectModel': return sessionApi.selectModel( - request as Parameters[0], - ) - case 'session/rename': return sessionApi.rename( - request as Parameters[0], - ) - case 'session/fork': return sessionApi.fork( - request as Parameters[0], - ) - case 'session/prompt': return sessionApi.prompt( - request as Parameters[0], - ) - case 'session/attachment': return sessionApi.attachment( - request as Parameters[0], - ) - case 'session/updateQueue': return sessionApi.updateQueue( - request as Parameters[0], - ) - case 'session/cancel': return sessionApi.cancel( - request as Parameters[0], - ) - case 'session/page': { - const page = request as FixturePageRequest - const pageSessionId = page.address.kind === 'session' - ? page.address.sessionId - : page.address.childSessionId - return sessionApi.history({ - sessionId: pageSessionId, - throughSeq: page.throughSeq, - ...page.beforeSeq === undefined ? {} : { beforeSeq: page.beforeSeq }, - ...page.maxMessages === undefined ? {} : { maxMessages: page.maxMessages }, - }) - } - case '$events/result': return Promise.resolve(answerRemoteEvent(args as unknown as FixtureRemoteEventResult)) - case 'workspace/create': return workspaceApi.create(request as WorkspaceCreateRequest) - case 'workspace/rename': return workspaceApi.rename(request as WorkspaceRenameRequest) - case 'workspace/delete': return workspaceApi.delete(request as WorkspaceDeleteRequest) - case 'workspace/insertBefore': return workspaceApi.insertBefore(request as WorkspaceInsertBeforeRequest) - case 'workspace/insertSessionBefore': return workspaceApi.insertSessionBefore( - request as WorkspaceInsertSessionBeforeRequest, - ) - case 'workspace/archiveSession': return workspaceApi.archiveSession(request as WorkspaceArchiveSessionRequest) - case 'workspace/unarchiveSession': return workspaceApi.unarchiveSession( - request as WorkspaceUnarchiveSessionRequest, - ) - default: - return Promise.reject(new Error(`fixture connection RPC endpoint ${JSON.stringify(endpoint)} is unavailable`)) - } - }, - open(channel, endpoint, payload, signal) { - if (channel !== '/api') { - throw new Error(`fixture connection RPC channel ${JSON.stringify(channel)} is unavailable`) - } - const args = (payload as { args: Readonly<{ request?: unknown }> }).args - switch (endpoint) { - case '$events': return openRemoteEvents(signal) - case 'session/control': return openControl(signal) - case 'session/follow': return openFollow(args.request as FixtureFollowRequest, signal) - case 'workspace/follow': return openWorkspace(signal) - case 'workspaceFiles/changes': return openWorkspaceFileChanges(signal) - default: - throw new Error(`fixture connection stream endpoint ${JSON.stringify(endpoint)} is unavailable`) - } - }, - } - return { rpc } -} - -/** - * Build the browser fixture transport from the current page's query switches. - * @returns an in-memory Connection RPC transport. - */ -export function createFixtureConnectionRpc(): ClientConnectionRpc { - return createFixtureWorld(fixtureOptionsFromLocation()).rpc -} - -/** Browser query mapping; direct unit callers pass FixtureOptions explicitly. */ -function fixtureOptionsFromLocation(): FixtureOptions { - if (typeof location === 'undefined') return {} - const query = new URLSearchParams(location.search) - return { - empty: query.get('fixture') === 'empty', - rejectPrompt: query.get('fixturePrompt') === 'reject', - failWorkspaceAttach: query.get('fixtureAttach') === 'fail', - dropSessionCreateResponse: query.get('fixtureSessionCreate') === 'drop-response', - createFrameOrder: query.get('fixtureFrames') === 'workspace-first' ? 'workspace-first' : 'session-first', - fileChanges: query.get('fixtureFileChanges') === 'demo', - } -} diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index a134400044..4497ca05de 100644 --- a/packages/client/connection/src/client/index.ts +++ b/packages/client/connection/src/client/index.ts @@ -8,7 +8,6 @@ import { type ConnectionSinks, type ConnectionState, } from './connection.ts' -import { createFixtureConnectionRpc } from './fixture.ts' import { createWebConnectionRpc, type RpcFetch, type RpcStreamOpen } from './rpc.ts' import { isLoopbackHostname } from '../loopback-hostname.ts' import type { ClientConnectionRpc } from '../rpc.ts' @@ -111,10 +110,9 @@ interface ClientTransportGlobal { __DSH_CONNECTION_RECOVERY__?: unknown } -/** Browser location fields used to select fixture mode and loopback authority. */ +/** Browser location fields used to classify loopback authority. */ export interface ConnectionLocation { readonly hostname: string - readonly search: string } /** Instance-local inputs for installing a Connection service. */ @@ -204,11 +202,9 @@ function watchBrowserNetwork(controller: ConnectionController): () => void { */ export function installConnection(ctx: Context, options: ConnectionInstallOptions = {}): void { const pageLocation = options.location - const fixture = pageLocation !== undefined && new URLSearchParams(pageLocation.search).has('fixture') - const fixtureRpc = fixture ? createFixtureConnectionRpc() : undefined const transport = options.transport const recovery = options.recovery ?? {} - const rpc = fixtureRpc ?? transport?.rpc ?? createWebConnectionRpc(transport?.fetch, transport?.openStream) + const rpc = transport?.rpc ?? createWebConnectionRpc(transport?.fetch, transport?.openStream) let generationSource: ConnectionGenerationSource | undefined let owner: ConnectionOwner | undefined let generationId = 0 diff --git a/packages/client/connection/tests/client-apply.client.spec.ts b/packages/client/connection/tests/client-apply.client.spec.ts index 3025767fd5..084f037135 100644 --- a/packages/client/connection/tests/client-apply.client.spec.ts +++ b/packages/client/connection/tests/client-apply.client.spec.ts @@ -1,6 +1,6 @@ /** - * Connection plugin browser-half apply: ctx.connection handle mounting, mode - * selection off the page URL, and single-consumer connection-loop ownership. + * Connection plugin browser-half apply: ctx.connection handle mounting, + * explicit carrier selection, and single-consumer connection-loop ownership. */ import { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -15,7 +15,7 @@ import { } from '../src/client/index.ts' type Win = { - location?: { hostname: string; search: string; origin?: string } + location?: { hostname: string; origin?: string } __DSH_TRANSPORT__?: ClientTransportHooks } @@ -124,25 +124,18 @@ describe('connection client apply', () => { }) it('mounts ctx.connection and identifies a loopback page', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() expect(handle.isLoopback).toBe(true) }) - it('selects the fixture RPC transport under ?fixture', async () => { - ;(globalThis as Win).location = { hostname: '127.0.0.1', search: '?fixture' } - const handle = await mount() - await expect(handle.rpc.call('/api', 'settings/describe', { args: {} })) - .resolves.toMatchObject({ ok: true }) - }) - it('reports non-loopback page authority through the connection handle', async () => { - ;(globalThis as Win).location = { hostname: '192.0.2.20', search: '' } + ;(globalThis as Win).location = { hostname: '192.0.2.20' } expect((await mount()).isLoopback).toBe(false) }) it('requires one generation source and ignores a stale source disposer', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const first = new GenerationProbe() const second = new GenerationProbe() @@ -165,7 +158,7 @@ describe('connection client apply', () => { }) it('start() hands out one loop, rejects a second consumer, and stop() aborts the generation', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) @@ -192,7 +185,7 @@ describe('connection client apply', () => { }) it('does not notify state subscribers when a pre-ready loop stops', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() handle.registerGenerationSource(signal => new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) @@ -209,7 +202,7 @@ describe('connection client apply', () => { }) it('allows a replacement owner and ignores the previous owner handle', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) @@ -232,7 +225,7 @@ describe('connection client apply', () => { }) it('lets the connection service force only its current owner to reconnect', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const requested = vi.fn() @@ -259,7 +252,7 @@ describe('connection client apply', () => { it('ignores a non-browser window shim without navigator state', async () => { vi.stubGlobal('window', new EventTarget()) - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const loop = handle.start({}) @@ -276,7 +269,7 @@ describe('connection client apply', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const browser = new BrowserNetworkProbe() vi.stubGlobal('window', browser) - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() let calls = 0 const source: ConnectionGenerationSource = (signal, ready) => new Promise((resolve) => { @@ -320,7 +313,7 @@ describe('connection client apply', () => { }) it('does not announce a generation synchronously stopped by a generation subscriber', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() installGeneration(handle) const owner: { loop?: ReturnType } = {} @@ -344,7 +337,7 @@ describe('connection client apply', () => { }) it('retracts the generation while connecting and publishes the next generation', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) const generations: Array = [] @@ -377,7 +370,7 @@ describe('connection client apply', () => { }) it('publishes connection state directly on the service and isolates subscribers', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) const snapshots: Array = [] @@ -415,7 +408,7 @@ describe('connection client apply', () => { }) it('does not announce disconnection after a generation subscriber stops the loop', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as Win).location = { hostname: 'localhost' } const handle = await mount() const generation = installGeneration(handle) const owner: { loop?: ReturnType } = {} @@ -448,7 +441,7 @@ describe('connection client apply', () => { }) it('carries RPC calls without requiring secure-context randomUUID', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '' } + ;(globalThis as Win).location = { hostname: 'localhost' } vi.stubGlobal('crypto', { getRandomValues(bytes: Uint8Array) { return bytes.fill(0) @@ -486,7 +479,7 @@ describe('connection client apply', () => { }) it('uses an already decoded rpc carrier from the transport hooks instead of the HTTP caller', async () => { - ;(globalThis as Win).location = { hostname: 'preview.example', search: '' } + ;(globalThis as Win).location = { hostname: 'preview.example' } const rpc: ClientConnectionRpc = { call: vi.fn(async (_channel: string, endpoint: string, payload: unknown) => ({ ok: true as const, value: { endpoint, payload } })), open: vi.fn((_channel: string, endpoint: string) => (async function *(): AsyncGenerator { yield endpoint })()), @@ -500,7 +493,7 @@ describe('connection client apply', () => { }) it('exposes a worker-local Gateway stream through connection.rpc.open', async () => { - ;(globalThis as Win).location = { hostname: 'preview.example', search: '' } + ;(globalThis as Win).location = { hostname: 'preview.example' } const openStream = vi.fn>( (endpoint, payload, signal) => (async function *(): AsyncGenerator { signal.throwIfAborted() @@ -538,7 +531,7 @@ describe('connection client apply', () => { it('validates generic RPC transport failures, correlation, and targets', async () => { ;(globalThis as Win).location = { - hostname: 'harness.example', search: '', origin: 'https://harness.example', + hostname: 'harness.example', origin: 'https://harness.example', } const handle = await mount() const original = globalThis.fetch @@ -552,7 +545,7 @@ describe('connection client apply', () => { expect.objectContaining({ signal: abort.signal }), ) - ;(globalThis as Win).location = { hostname: 'localhost', search: '', origin: 'null' } + ;(globalThis as Win).location = { hostname: 'localhost', origin: 'null' } globalThis.fetch = vi.fn().mockResolvedValue(Response.json({ type: 'server-response', rpcId: 'different-rpc', @@ -624,37 +617,4 @@ describe('connection client apply', () => { } }) - it('carries Goal Remotes over the client-only fixture state', async () => { - ;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' } - const handle = await mount() - const created = await handle.rpc.call('/api', 'goals/create', { - args: { agentId: 'fx-alpha', request: { objective: 'fixture remote' } }, - }) - expect(created).toMatchObject({ ok: true, value: { ref: { revision: 1 } } }) - if (!created.ok) throw new Error('fixture Goal create failed') - const ref = (created.value as { ref: { id: string; revision: number } }).ref - const edited = await handle.rpc.call('/api', 'goals/edit', { - args: { agentId: 'fx-alpha', ref, request: { objective: 'edited fixture remote' } }, - }) - expect(edited).toMatchObject({ ok: true, value: { objective: 'edited fixture remote', revision: 2 } }) - const editedRef = { id: ref.id, revision: 2 } - const paused = await handle.rpc.call('/api', 'goals/pause', { - args: { agentId: 'fx-alpha', ref: editedRef }, - }) - expect(paused).toMatchObject({ ok: true, value: { phase: 'paused', activation: 'disarmed', revision: 3 } }) - const resumed = await handle.rpc.call('/api', 'goals/resume', { - args: { agentId: 'fx-alpha', ref: { id: ref.id, revision: 3 } }, - }) - expect(resumed).toMatchObject({ ok: true, value: { phase: 'active', activation: 'armed', revision: 4 } }) - const completed = await handle.rpc.call('/api', 'goals/complete', { - args: { agentId: 'fx-alpha', ref: { id: ref.id, revision: 4 } }, - }) - expect(completed).toMatchObject({ ok: true, value: { phase: 'complete', activation: 'disarmed', revision: 5 } }) - await expect(handle.rpc.call('/api', 'goals/clear', { - args: { agentId: 'fx-alpha', ref: { id: ref.id, revision: 5 } }, - })).resolves.toEqual({ ok: true, value: { id: ref.id, revision: 6 } }) - await expect(handle.rpc.call('/other', 'goals/create', {})).rejects.toThrow(/channel.*unavailable/) - await expect(handle.rpc.call('/api', 'unknown/read', { args: { agentId: 'fx-alpha' } })) - .rejects.toThrow(/endpoint.*unavailable/) - }) }) diff --git a/packages/client/connection/tests/fixture-commands.client.spec.ts b/packages/client/connection/tests/fixture-commands.client.spec.ts deleted file mode 100644 index 103a9d9266..0000000000 --- a/packages/client/connection/tests/fixture-commands.client.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Fixture commands/skills domains: session-addressed catalogs, execute - * parse/dispatch and its logged lifecycle pair, and skills/list Session resolution. - */ -import { describe, expect, it } from 'vitest' -import type { SessionId } from '../src/client/api.ts' -import { createFixtureFaces } from '../src/client/fixture.ts' - -/** Drive one commands Remote endpoint against the fixture state graph. */ -async function callRemote( - rpc: ReturnType['rpc'], - endpoint: string, - args: Record, -): Promise { - const result = await rpc.call('/api', endpoint, { args }) - if (!result.ok) throw new Error(`${endpoint} failed: ${result.error.code}`) - return result.value as T -} - -const sid = (id: string): SessionId => id as SessionId - -describe('createFixtureApi commands/skills', () => { - it('serves the addressed session catalog', async () => { - const { rpc } = createFixtureFaces() - const commands = await callRemote<{ name: string; input?: { hint: string; attachments?: boolean } }[]>( - rpc, 'commands/list', { agentId: sid('fx-alpha') }) - expect(commands.map(c => c.name)).toEqual(['compact', 'echo', 'goal', 'permission', 'plan']) - // input hint rides only the commands declaring it. - const echo = commands.find(c => c.name === 'echo') - expect(echo?.input?.hint).toBeTruthy() - expect(commands.find(c => c.name === 'compact')?.input).toBeUndefined() - // Image acceptance is declared per descriptor; only goal and plan carry it. - expect(commands.filter(c => c.input?.attachments === true).map(c => c.name)).toEqual(['goal', 'plan']) - }) - - it('rejects a catalog request for an unknown session', async () => { - const { rpc } = createFixtureFaces() - const result = await rpc.call('/api', 'commands/list', { args: { agentId: sid('fx-nope') } }) - expect(result).toMatchObject({ ok: false, error: { code: 'session/not-found' } }) - }) - - it('executes a known command line: pure admission plus a followed lifecycle pair', async () => { - const { rpc } = createFixtureFaces() - const frames: unknown[] = [] - const abort = new AbortController() - const stream = rpc.open?.('/api', 'session/follow', { - args: { request: { address: { kind: 'session', sessionId: sid('fx-alpha') } } }, - }, abort.signal) - if (stream === undefined) throw new Error('fixture session follow stream is unavailable') - const pump = (async () => { - for await (const frame of stream) { - frames.push(frame) - if (frames.filter(f => (f as { type: string }).type === 'event').length >= 2) abort.abort() - } - })() - const execution = await callRemote<{ commandId: string } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/echo hello world' }) - expect(execution?.commandId).toBeTruthy() - await pump - const events = frames - .filter((f): f is { type: string; event: { type: string; data: Record } } => (f as { type: string }).type === 'event') - .map(f => f.event) - expect(events).toMatchObject([ - { type: 'command/run', data: { name: 'echo', args: ' hello world', source: { kind: 'user' } } }, - { type: 'command/done', data: { kind: 'success', text: 'hello world' } }, - ]) - expect(events[0]?.data.commandId).toBe(events[1]?.data.commandId) - }) - - it('addresses execute to the session; an unknown session errs', async () => { - const { rpc } = createFixtureFaces() - const hit = await callRemote<{ commandId: string } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/goal ship' }) - expect(hit?.commandId).toBeTruthy() - - const missing = await rpc.call('/api', 'commands/execute', { - args: { agentId: sid('fx-nope'), line: '/goal ship' }, - }) - expect(missing).toMatchObject({ ok: false, error: { code: 'session/not-found' } }) - }) - - it('refuses an image-carrying execute for a non-declaring command with a logged error pair', async () => { - const { rpc } = createFixtureFaces() - const frames: unknown[] = [] - const abort = new AbortController() - const stream = rpc.open?.('/api', 'session/follow', { - args: { request: { address: { kind: 'session', sessionId: sid('fx-alpha') } } }, - }, abort.signal) - if (stream === undefined) throw new Error('fixture session follow stream is unavailable') - const pump = (async () => { - for await (const frame of stream) { - frames.push(frame) - if (frames.filter(f => (f as { type: string }).type === 'event').length >= 2) abort.abort() - } - })() - const png = { mediaType: 'image/png', data: 'AA==' } - const refused = await callRemote<{ commandId: string; result: { kind: string; text?: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/echo hi', images: [png] }) - expect(refused?.commandId).toBeTruthy() - expect(refused?.result).toEqual({ kind: 'error', text: '/echo does not accept attachments' }) - await pump - const events = frames - .filter((f): f is { type: string; event: { type: string; data: Record } } => (f as { type: string }).type === 'event') - .map(f => f.event) - expect(events).toMatchObject([ - { type: 'command/run', data: { name: 'echo', args: ' hi', source: { kind: 'user' } } }, - { type: 'command/done', data: { kind: 'error', text: '/echo does not accept attachments' } }, - ]) - }) - - it('a declaring command accepts an image-carrying execute', async () => { - const { rpc } = createFixtureFaces() - const png = { mediaType: 'image/png', data: 'AA==' } - const accepted = await callRemote<{ result: { kind: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/goal ship it', images: [png] }) - expect(accepted?.result.kind).toBe('success') - const planMessage = await callRemote<{ result: { kind: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/plan sketch the layout', images: [png] }) - expect(planMessage?.result.kind).toBe('success') - const imageOnlyPlan = await callRemote<{ result: { kind: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/plan', images: [png] }) - expect(imageOnlyPlan?.result.kind).toBe('success') - }) - - it('mirrors the producer grammar rejections for control-only declaring lines', async () => { - const { rpc } = createFixtureFaces() - const png = { mediaType: 'image/png', data: 'AA==' } - const bareGoal = await callRemote<{ result: { kind: string; text?: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/goal', images: [png] }) - expect(bareGoal?.result).toEqual({ - kind: 'error', - text: 'Attachments only accompany a goal objective: /goal or /goal edit .', - }) - const refused = await callRemote<{ result: { kind: string; text?: string } } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/plan off', images: [png] }) - expect(refused?.result).toEqual({ - kind: 'error', - text: 'Attachments cannot accompany /plan off.', - }) - }) - - it('answers no execution for an unknown name even when images accompany it', async () => { - const { rpc } = createFixtureFaces() - const png = { mediaType: 'image/png', data: 'AA==' } - expect(await callRemote(rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/nope', images: [png] })) - .toBeUndefined() - }) - - it('answers no execution for unknown names and non-command lines', async () => { - const { rpc } = createFixtureFaces() - for (const line of ['/nope', 'plain text', '/']) { - // Absence is the whole answer: nothing matched, so no lifecycle id exists. - expect(await callRemote(rpc, 'commands/execute', { agentId: sid('fx-alpha'), line })) - .toBeUndefined() - } - }) - - it('serves the skill catalog for the addressed session and rejects unknown sessions', async () => { - const { rpc } = createFixtureFaces() - const skills = await callRemote<{ skills: Array<{ name: string }> }>( - rpc, 'skills/list', { request: { sessionId: sid('fx-alpha') } }, - ) - expect(skills.skills[0]?.name).toBe('fixture-demo') - - const missingSession = await rpc.call('/api', 'skills/list', { - args: { request: { sessionId: sid('fx-nope') } }, - }) - expect(missingSession).toMatchObject({ ok: false, error: { code: 'session/not-found' } }) - }) -}) - -describe('fixture Connection command/skill dispatch', () => { - it('routes the Remote command and skill rows through one state graph', async () => { - const { rpc } = createFixtureFaces() - const commands = await callRemote<{ name: string }[]>(rpc, 'commands/list', { agentId: sid('fx-alpha') }) - expect(commands.length).toBeGreaterThan(0) - const executed = await callRemote<{ commandId: string } | undefined>( - rpc, 'commands/execute', { agentId: sid('fx-alpha'), line: '/compact' }) - expect(executed?.commandId).toBeTruthy() - const skills = await callRemote<{ skills: unknown[] }>( - rpc, 'skills/list', { request: { sessionId: sid('fx-alpha') } }, - ) - expect(skills.skills.length).toBeGreaterThan(0) - }) -}) diff --git a/packages/client/connection/tests/fixture.client.spec.ts b/packages/client/connection/tests/fixture.client.spec.ts deleted file mode 100644 index efaa35cc16..0000000000 --- a/packages/client/connection/tests/fixture.client.spec.ts +++ /dev/null @@ -1,1825 +0,0 @@ -import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' -import type { - RpcRequest, - RpcResponse, - RpcResult, - SessionEvent, - SessionId, - StreamChunk, -} from '../src/client/api.ts' -import { RpcId } from '../src/client/api.ts' -import { - createFixtureConnectionRpc, - createFixtureFaces, - type FixtureAssistantStreamFrame, - type FixtureOptions, -} from '../src/client/fixture.ts' -import type { - ClientConnectionRpc, ConnectionRpcResult, -} from '../src/rpc.ts' -import type { DirectoryListing } from '@deepseek-ai/dsh-host-directory-picker/types' -import type { - ModelCatalog, ModelSelection, SessionAssistantStreamFrame, -} from '@deepseek-ai/dsh-api-session-controller/types' - -const sid = (id: string): SessionId => id as SessionId -type WorkspaceId = string & { readonly __fixtureWorkspaceId: 'WorkspaceId' } -const req =

(payload: P): RpcRequest

=> ({ rpcId: RpcId(`t-${Math.abs(Math.sin(reqCount++)).toString(36).slice(2, 10)}`), payload }) -let reqCount = 0 - -interface FixtureSessionSummary { - sessionId: SessionId - updatedAt: number - running: boolean - blank: boolean - parentSessionId?: SessionId - origin?: 'subagent' - cwd?: string - agentPreset?: string -} - -interface FixtureHistoryEntry { - readonly type: 'event' - readonly event: SessionEvent -} - -type FixtureHistoryRecord = FixtureHistoryEntry - -interface FixturePage { - readonly records: readonly FixtureHistoryRecord[] - readonly hasMore: boolean -} - -function historyEvents(records: readonly FixtureHistoryRecord[]): SessionEvent[] { - return records.map(record => record.event) -} - -function isReasoningDeltaChunk( - value: unknown, -): value is Extract { - if (typeof value !== 'object' || value === null) return false - const record = value as { readonly type?: unknown; readonly text?: unknown } - return record.type === 'reasoning-delta' && typeof record.text === 'string' -} - -type FixtureFollowFrame = - | { - readonly type: 'snapshot' - readonly cursor: number - readonly records: readonly FixtureHistoryRecord[] - readonly hasMore: boolean - readonly projections: { - readonly asOfSeq: number - readonly values: Readonly> - } - readonly assistantStream?: { - readonly revision: number - readonly activeAttempt?: { - readonly attemptId: string - readonly startedAfterSeq: number - readonly turn: number - readonly step: number - readonly nextIndex: number - readonly stream: readonly unknown[] - } - } - } - | FixtureHistoryEntry - | { readonly type: 'assistant-stream'; readonly frame: FixtureAssistantStreamFrame } - -type FixtureControlFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly queues: Readonly> - readonly jobs: Readonly> - readonly approvals: readonly unknown[] - readonly questions: readonly unknown[] - readonly projections: Readonly> - }>> - } - } - | { - readonly type: 'projection' - readonly sessionId: SessionId - readonly key: string - readonly value: unknown - readonly seq: number - } - -interface FixtureSessionRequests { - list: { readonly cursor?: string } - search: { readonly query: string } - create: { - readonly workspaceId?: WorkspaceId - readonly cwd?: string - readonly sessionId?: SessionId - readonly agentPreset?: string - } - history: { - readonly sessionId: SessionId - readonly beforeSeq?: number - readonly maxMessages?: number - } - selectModel: { - readonly sessionId: SessionId - readonly provider: string - readonly model: string - readonly reasoningEffort?: string - } - prompt: { - readonly sessionId: SessionId - readonly mode: 'queue' | 'steer' - readonly content: readonly ({ readonly type: 'text'; readonly text: string } | { - readonly type: 'image' - readonly mediaType: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif' - readonly data: string - readonly name?: string - })[] - } - cancel: { readonly sessionId: SessionId } - rename: { readonly sessionId: SessionId; readonly title: string } -} - -interface FixtureSessionValues { - list: { readonly items: FixtureSessionSummary[] } - search: { readonly items: readonly { readonly sessionId: SessionId; readonly snippet: string }[]; readonly hasMore: boolean } - create: { readonly sessionId: SessionId } - history: FixturePage - selectModel: { readonly selected: ModelSelection } - prompt: { readonly accepted: true } - cancel: Record - rename: { readonly title: string; readonly seq: number } -} - -type FixtureSessionApi = { - [K in keyof FixtureSessionRequests]: ( - request: RpcRequest, - signal?: AbortSignal, - ) => Promise> -} - -type FixtureSessionClient = { - [K in keyof FixtureSessionRequests]: ( - request: FixtureSessionRequests[K], - signal?: AbortSignal, - ) => Promise> -} - -interface FixtureSessionRemote { - modelCatalog(): Promise> - follow(sessionId: SessionId, signal: AbortSignal): AsyncIterable - control(signal: AbortSignal): AsyncIterable -} - -interface FixtureWorkspaceView { - readonly workspaceId: WorkspaceId - readonly path: string - readonly title: string - readonly sessionIds: readonly SessionId[] - readonly createdAt: string - readonly updatedAt: string -} - -interface FixtureWorkspaceRequests { - create: { readonly path: string } - rename: { readonly workspaceId: WorkspaceId; readonly title: string } - delete: { readonly workspaceId: WorkspaceId } - insertBefore: { readonly workspaceId: WorkspaceId; readonly beforeWorkspaceId?: WorkspaceId } - insertSessionBefore: { - readonly workspaceId: WorkspaceId - readonly sessionId: SessionId - readonly beforeSessionId?: SessionId - } - archiveSession: { readonly sessionId: SessionId } - unarchiveSession: { readonly sessionId: SessionId } -} - -interface FixtureWorkspaceValues { - create: { readonly workspace: FixtureWorkspaceView; readonly created: boolean } - rename: { readonly workspace: FixtureWorkspaceView } - delete: { readonly deleted: true } - insertBefore: { readonly workspaceIds: readonly WorkspaceId[] } - insertSessionBefore: { readonly workspace: FixtureWorkspaceView } - archiveSession: { readonly archivedSessionIds: readonly SessionId[] } - unarchiveSession: { readonly archivedSessionIds: readonly SessionId[] } -} - -type FixtureWorkspaceApi = { - [K in keyof FixtureWorkspaceRequests]: ( - request: RpcRequest, - signal?: AbortSignal, - ) => Promise> -} - -type FixtureWorkspaceClient = { - [K in keyof FixtureWorkspaceRequests]: ( - request: FixtureWorkspaceRequests[K], - signal?: AbortSignal, - ) => Promise> -} - -type FixtureWorkspaceFrame = - | { - readonly type: 'baseline' - readonly value: { - readonly items: readonly FixtureWorkspaceView[] - readonly archivedSessionIds: readonly SessionId[] - } - } - | { readonly type: 'upsert'; readonly workspace: FixtureWorkspaceView } - | { readonly type: 'remove'; readonly workspaceId: WorkspaceId } - | { readonly type: 'order'; readonly workspaceIds: readonly WorkspaceId[] } - | { readonly type: 'archived'; readonly archivedSessionIds: readonly SessionId[] } - -interface FixtureWorkspaceRemote { - follow(signal: AbortSignal): AsyncIterable -} - -interface FixtureRemoteEventNotificationFrame { - readonly type: 'emit' - readonly event: string - readonly args: readonly unknown[] -} - -interface FixtureRemoteEventRequestFrame { - readonly type: 'waterfall' - readonly event: string - readonly eventId: string - readonly agentId: SessionId - readonly request: Readonly> -} - -interface FixtureRemoteEventCancellationFrame { - readonly type: 'cancel' - readonly eventId: string -} - -type FixtureRemoteEventFrame = - | FixtureRemoteEventNotificationFrame - | FixtureRemoteEventRequestFrame - | FixtureRemoteEventCancellationFrame - -interface FixtureRemoteEventResult { - readonly clientId: string - readonly eventId: string - readonly outcome: - | { readonly kind: 'next' } - | { readonly kind: 'result'; readonly value?: unknown } - | { - readonly kind: 'rejected' - readonly error: { - readonly name: string - readonly message: string - readonly code?: string - readonly details?: unknown - } - } -} - -interface FixtureRemoteEventStream extends AsyncIterable { - readonly clientId: Promise -} - -type FixtureTestApi = { - /** The directory-picking Remote namespace as the fixture serves it. */ - readonly directoryPickerRemote: { - pick: () => Promise> - list: (path?: string) => Promise> - createDirectory: (path: string, name: string) => Promise> - } - readonly sessions: FixtureSessionApi - readonly sessionRemote: FixtureSessionRemote - readonly workspace: FixtureWorkspaceApi - readonly workspaceRemote: FixtureWorkspaceRemote - readonly credentialRemote: FixtureCredentialRemote - readonly settingsRemote: FixtureSettingsRemote - readonly remoteEvents: (signal: AbortSignal) => FixtureRemoteEventStream - readonly answerRemoteEvent: (result: FixtureRemoteEventResult) => Promise -} - -/** Keep existing fixture assertions compact while driving only the new Session Remote endpoints. */ -function createFixtureApi(options: FixtureOptions = {}): FixtureTestApi { - const { rpc } = createFixtureFaces(options) - return { - directoryPickerRemote: { - pick: () => rpc.call('/api', 'directoryPicker/pick', { args: {} }) as - Promise>, - list: (path?: string) => rpc.call('/api', 'directoryPicker/list', { args: { path } }) as - Promise>, - createDirectory: (path: string, name: string) => - rpc.call('/api', 'directoryPicker/createDirectory', { args: { path, name } }) as - Promise>, - }, - sessions: createSessionApi(rpc), - sessionRemote: createSessionRemote(rpc), - workspace: createWorkspaceApi(rpc), - workspaceRemote: createWorkspaceRemote(rpc), - credentialRemote: createCredentialRemote(rpc), - settingsRemote: createSettingsRemote(rpc), - remoteEvents: (signal: AbortSignal) => openFixtureRemoteEvents(rpc, signal), - answerRemoteEvent: (result: FixtureRemoteEventResult) => - rpc.call('/api', '$events/result', { args: result }), - } -} - -/** The fixture's Credentials Remote endpoints over the shared RPC carrier. */ -interface FixtureCredentialRemote { - describe(refs: readonly string[]): Promise> - set(ref: string, value: string): Promise> - unset(ref: string): Promise> -} - -/** The settings Remote reads the fixture serves, addressed like the credential half. */ -interface FixtureSettingsRemote { - describe(): Promise> - update(ns: string, patch: unknown, expectedRevision?: number): Promise> - replace(ns: string, section: unknown, expectedRevision?: number): Promise> -} - -function createSettingsRemote(rpc: ClientConnectionRpc): FixtureSettingsRemote { - return { - describe: () => rpc.call('/api', 'settings/describe', { args: {} }), - update: (ns, patch, expectedRevision) => rpc.call('/api', 'settings/update', { - args: { ns, patch, expectedRevision }, - }), - replace: (ns, section, expectedRevision) => rpc.call('/api', 'settings/replace', { - args: { ns, section, expectedRevision }, - }), - } -} - -function createCredentialRemote(rpc: ClientConnectionRpc): FixtureCredentialRemote { - return { - describe: refs => rpc.call('/api', 'credentials/describe', { args: { refs } }), - set: (ref, value) => rpc.call('/api', 'credentials/set', { args: { ref, value } }), - unset: ref => rpc.call('/api', 'credentials/unset', { args: { ref } }), - } -} - -function openFixtureRemoteEvents( - rpc: ClientConnectionRpc, - signal: AbortSignal, -): FixtureRemoteEventStream { - const ready = Promise.withResolvers() - const source = (async function* (): AsyncGenerator { - const stream = rpc.open?.('/api', '$events', { args: {} }, signal) - if (stream === undefined) throw new Error('fixture forwarded-event stream is unavailable') - let opened = false - for await (const value of stream) { - if (!opened) { - expect(value).toMatchObject({ type: 'ready' }) - const clientId: unknown = Reflect.get(value as object, 'clientId') - if (typeof clientId !== 'string') throw new Error('fixture forwarded-event stream omitted its Client id') - ready.resolve(clientId) - opened = true - continue - } - yield value as FixtureRemoteEventFrame - } - })() - return Object.assign(source, { clientId: ready.promise }) -} - -function createSessionApi(rpc: ClientConnectionRpc): FixtureSessionApi { - const call = async ( - endpoint: K, - request: RpcRequest, - signal?: AbortSignal, - ): Promise> => { - const page = endpoint === 'history' - ? request.payload as FixtureSessionRequests['history'] - : undefined - const args = endpoint === 'list' - ? { _request: request.payload } - : endpoint === 'history' - ? { - request: { - address: { kind: 'session', sessionId: page?.sessionId }, - ...page?.beforeSeq === undefined ? {} : { beforeSeq: page.beforeSeq }, - ...page?.maxMessages === undefined ? {} : { maxMessages: page.maxMessages }, - }, - } - : { request: request.payload } - const remoteEndpoint = endpoint === 'history' ? 'page' : endpoint - const result = await rpc.call('/api', `session/${remoteEndpoint}`, { args }, signal) - return { - rpcId: request.rpcId, - result: result as unknown as RpcResult, - } - } - return { - list: (request, signal) => call('list', request, signal), - search: (request, signal) => call('search', request, signal), - create: (request, signal) => call('create', request, signal), - history: (request, signal) => call('history', request, signal), - selectModel: (request, signal) => call('selectModel', request, signal), - prompt: (request, signal) => call('prompt', request, signal), - cancel: (request, signal) => call('cancel', request, signal), - rename: (request, signal) => call('rename', request, signal), - } -} - -function createSessionClient(rpc: ClientConnectionRpc): FixtureSessionClient { - const api = createSessionApi(rpc) - return { - list: (request, signal) => api.list(req(request), signal), - search: (request, signal) => api.search(req(request), signal), - create: (request, signal) => api.create(req(request), signal), - history: (request, signal) => api.history(req(request), signal), - selectModel: (request, signal) => api.selectModel(req(request), signal), - prompt: (request, signal) => api.prompt(req(request), signal), - cancel: (request, signal) => api.cancel(req(request), signal), - rename: (request, signal) => api.rename(req(request), signal), - } -} - -function createSessionRemote(rpc: ClientConnectionRpc): FixtureSessionRemote { - const open = (endpoint: string, args: object, signal: AbortSignal): AsyncIterable => { - const stream = rpc.open?.('/api', endpoint, { args }, signal) - if (stream === undefined) throw new Error(`fixture ${endpoint} stream is unavailable`) - return stream as AsyncIterable - } - return { - modelCatalog: () => rpc.call('/api', 'session/modelCatalog', { args: {} }) as - Promise>, - follow: (sessionId, signal) => open('session/follow', { - request: { address: { kind: 'session', sessionId }, assistantStream: true }, - }, signal), - control: signal => open('session/control', {}, signal), - } -} - -function createWorkspaceApi(rpc: ClientConnectionRpc): FixtureWorkspaceApi { - const call = async ( - endpoint: K, - request: RpcRequest, - signal?: AbortSignal, - ): Promise> => { - const result = await rpc.call('/api', `workspace/${endpoint}`, { - args: { request: request.payload }, - }, signal) - return { - rpcId: request.rpcId, - result: result as unknown as RpcResult, - } - } - return { - create: (request, signal) => call('create', request, signal), - rename: (request, signal) => call('rename', request, signal), - delete: (request, signal) => call('delete', request, signal), - insertBefore: (request, signal) => call('insertBefore', request, signal), - insertSessionBefore: (request, signal) => call('insertSessionBefore', request, signal), - archiveSession: (request, signal) => call('archiveSession', request, signal), - unarchiveSession: (request, signal) => call('unarchiveSession', request, signal), - } -} - -function createWorkspaceClient(rpc: ClientConnectionRpc): FixtureWorkspaceClient { - const api = createWorkspaceApi(rpc) - return { - create: (request, signal) => api.create(req(request), signal), - rename: (request, signal) => api.rename(req(request), signal), - delete: (request, signal) => api.delete(req(request), signal), - insertBefore: (request, signal) => api.insertBefore(req(request), signal), - insertSessionBefore: (request, signal) => api.insertSessionBefore(req(request), signal), - archiveSession: (request, signal) => api.archiveSession(req(request), signal), - unarchiveSession: (request, signal) => api.unarchiveSession(req(request), signal), - } -} - -function createWorkspaceRemote(rpc: ClientConnectionRpc): FixtureWorkspaceRemote { - return { - follow(signal) { - const stream = rpc.open?.('/api', 'workspace/follow', { args: {} }, signal) - if (stream === undefined) throw new Error('fixture workspace/follow stream is unavailable') - return stream as AsyncIterable - }, - } -} - -interface TimingHooks { - setHistoryDelay(ms: number): void - failNextHistory(): void - appendUser(id: string, msg: string): void - appendTitle(id: string, title: string): void - disarmOnlyGoal(): void - startReasoningChunkStorm(id: string, chunkCount: number, chunksPerInterval: number, intervalMs: number): string - reasoningChunkStormState(): { - sessionId: string - chunkCount: number - chunksPerInterval: number - intervalMs: number - emitted: number - marker: string - emitting: boolean - } | null - beginModelRetry(id: string): void - scheduleModelRetry(id: string, retry?: number, delayMs?: number): void - cancelModelRetryDuringBackoff(id: string, delayMs?: number): void - completeModelRetry(id: string): void - appendSilent(id: string, msg: string): void - breakStreams(): void -} -const timing = (): TimingHooks => (globalThis as Record).__fxTiming as TimingHooks - -/** Collect value-stream frames until the predicate or a soft cap; abort ends the stream. */ -async function collectValues(stream: AsyncIterable, abort: AbortController, done: (frames: F[]) => boolean): Promise { - const frames: F[] = [] - for await (const frame of stream) { - frames.push(frame) - if (done(frames) || frames.length > 500) { - abort.abort() - break - } - } - return frames -} - -async function readControlBaseline(remote: FixtureSessionRemote): Promise> { - const abort = new AbortController() - for await (const frame of remote.control(abort.signal)) { - if (frame.type !== 'baseline') continue - abort.abort() - return frame - } - throw new Error('fixture control baseline missing') -} - -function isRemoteEventRequest(frame: FixtureRemoteEventFrame): frame is FixtureRemoteEventRequestFrame { - return frame.type === 'waterfall' -} - -function isRemoteEventCancellation(frame: FixtureRemoteEventFrame): frame is FixtureRemoteEventCancellationFrame { - return frame.type === 'cancel' -} - -async function readResidentRemoteEvents( - api: FixtureTestApi, - count: number, -): Promise { - const abort = new AbortController() - const frames = await collectValues( - api.remoteEvents(abort.signal), - abort, - seen => seen.filter(isRemoteEventRequest).length >= count, - ) - return frames.filter(isRemoteEventRequest) -} - -async function nextRemoteEvent( - iterator: AsyncIterator, - predicate: (frame: FixtureRemoteEventFrame) => boolean, -): Promise { - for (;;) { - const item = await iterator.next() - if (item.done) throw new Error('fixture Remote Event stream ended before the expected frame') - if (predicate(item.value)) return item.value - } -} - -async function readOpeningCursor(remote: FixtureSessionRemote, sessionId: SessionId): Promise { - const abort = new AbortController() - for await (const frame of remote.follow(sessionId, abort.signal)) { - if (frame.type !== 'snapshot') continue - abort.abort() - return frame.cursor - } - throw new Error('fixture follow opening cursor missing') -} - -async function readWorkspaceBaseline( - remote: FixtureWorkspaceRemote, -): Promise['value']> { - const abort = new AbortController() - for await (const frame of remote.follow(abort.signal)) { - if (frame.type !== 'baseline') continue - abort.abort() - return frame.value - } - throw new Error('fixture Workspace baseline missing') -} - -describe('createFixtureApi', () => { - it('keeps its Assistant frame vocabulary identical to the controller wire', () => { - expectTypeOf().toEqualTypeOf() - }) - - it('serves the session list sorted by updatedAt desc and echoes rpcIds on every unary', async () => { - const api = createFixtureApi() - const request = req({}) - const response = await api.sessions.list(request) - expect(response.rpcId).toBe(request.rpcId) - if (!response.result.ok) throw new Error('list failed') - expect(response.result.value.items.map(s => s.sessionId)).toEqual(['fx-alpha', 'fx-beta', 'fx-gamma']) - expect(response.result.value.items[1]?.parentSessionId).toBe('fx-alpha') // lineage material - }) - - it('returns an empty Assistant baseline when Session follow opts in', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const iterator = api.sessionRemote.follow(sid('fx-alpha'), abort.signal)[Symbol.asyncIterator]() - try { - const opening = await iterator.next() - if (opening.done || opening.value.type !== 'snapshot') throw new Error('follow opening snapshot missing') - expect(opening.value.assistantStream).toEqual({ revision: 0 }) - } finally { - abort.abort() - await iterator.return?.() - } - }) - - it('searches current message text with literal unicode61-style token phrases', async () => { - const api = createFixtureApi() - const signal = new AbortController().signal - const phrase = await api.sessions.search(req({ query: 'FIXTURE 历史消息' }), signal) - expect(phrase.result).toMatchObject({ - ok: true, - value: { - items: [{ sessionId: 'fx-alpha' }], - hasMore: false, - }, - }) - if (!phrase.result.ok) throw new Error('search failed') - expect(phrase.result.value.items[0]?.snippet).toContain('fixture 历史消息') - - timing().appendUser( - 'fx-alpha', - `${'leading context '.repeat(20)}late café token${' trailing context'.repeat(20)}`, - ) - const late = await api.sessions.search(req({ query: 'LATE CAFE TOKEN' }), signal) - if (!late.result.ok) throw new Error('late search failed') - const lateSnippet = late.result.value.items[0]?.snippet ?? '' - expect(lateSnippet).toContain('late café token') - expect(lateSnippet.startsWith('…')).toBe(true) - expect(lateSnippet.endsWith('…')).toBe(true) - expect(Array.from(lateSnippet).length).toBeLessThanOrEqual(120) - - timing().appendUser('fx-alpha', 'Greek final sigma: ος') - const finalSigma = await api.sessions.search(req({ query: 'ΟΣ' }), signal) - if (!finalSigma.result.ok) throw new Error('final sigma search failed') - expect(finalSigma.result.value.items[0]?.snippet).toContain('ος') - - const substring = await api.sessions.search(req({ query: 'ixtur' }), signal) - expect(substring.result).toEqual({ - ok: true, - value: { items: [], hasMore: false }, - }) - const punctuationOnly = await api.sessions.search(req({ query: '*' }), signal) - expect(punctuationOnly.result).toEqual({ - ok: true, - value: { items: [], hasMore: false }, - }) - const reasoningOnly = await api.sessions.search(req({ query: '思考过程' }), signal) - expect(reasoningOnly.result).toEqual({ - ok: true, - value: { items: [], hasMore: false }, - }) - - const aborted = new AbortController() - aborted.abort() - await expect(api.sessions.search(req({ query: 'fixture' }), aborted.signal)) - .resolves.toMatchObject({ result: { ok: false, error: { code: 'gateway/cancelled' } } }) - }) - - it('pages history backwards on message-boundary cuts with seq-contiguous stitching', async () => { - const api = createFixtureApi() - const tail = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 10 })) - if (!tail.result.ok) throw new Error('history failed') - const tailPage = tail.result.value - expect(tailPage.hasMore).toBe(true) - const tailEvents = historyEvents(tailPage.records) - expect(tailEvents[0]?.type).toBe('turn/start') // cut lands on a turn boundary - const boundary = tailEvents[0]?.seq ?? 0 - expect(boundary).toBeGreaterThan(0) - const older = await api.sessions.history(req({ sessionId: sid('fx-alpha'), beforeSeq: boundary, maxMessages: 10 })) - if (!older.result.ok) throw new Error('older failed') - const olderTail = historyEvents(older.result.value.records).at(-1) - expect((olderTail?.seq ?? -1) + 1).toBe(boundary) // pages stitch with no hole/overlap - // Out-of-range beforeSeq clamps instead of exploding. - const clamped = await api.sessions.history(req({ sessionId: sid('fx-alpha'), beforeSeq: -5, maxMessages: 10 })) - if (!clamped.result.ok) throw new Error('clamped failed') - expect(clamped.result.value.records).toEqual([]) - // Unknown session: empty page, not an error (history of a bare id). - const empty = await api.sessions.history(req({ sessionId: sid('no-such'), maxMessages: 10 })) - if (!empty.result.ok) throw new Error('empty failed') - expect(empty.result.value).toEqual({ records: [], hasMore: false }) - }) - - it('serves raw history entries with replayable tool-result metadata', async () => { - const api = createFixtureApi() - const response = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 200 })) - if (!response.result.ok) throw new Error('history failed') - - const records = response.result.value.records - const results = historyEvents(records) - .filter(event => event.type === 'tool/result') - - expect(results.find(event => event.data.turn === 64)).toMatchObject({ - data: { - meta: { - diffs: [ - { path: 'src/config.ts', oldText: 'const timeout = 30', newText: 'const timeout = 60' }, - { path: 'src/config.ts', oldText: 'retries: 1', newText: 'retries: 3' }, - ], - }, - }, - }) - expect(results.find(event => event.data.turn === 67)).toMatchObject({ - data: { meta: { shape: 'matches', truncated: true, total: 42 } }, - }) - expect(results.find(event => event.data.turn === 69)).toMatchObject({ - data: { meta: { path: 'packages/client/ui-primitives/src/ReadBlock.tsx', offset: 41, totalLines: 180 } }, - }) - const webSearch = results.find(event => event.data.turn === 70) - expect(webSearch).toHaveProperty('data.meta.truncated', true) - expect(webSearch).toHaveProperty('data.meta.sources', expect.arrayContaining([ - expect.objectContaining({ url: 'https://github.com/deepseek-ai/deepseek-harness' }), - ])) - expect(results.find(event => event.data.turn === 71)).toMatchObject({ - data: { meta: { url: 'https://www.deepseek.com/blog/harness-architecture', statusCode: 200 } }, - }) - const terminal = results.find(event => event.data.turn === 66) - expect(terminal).toHaveProperty('data.message.content.0.content.0.type', 'text') - expect(terminal).toHaveProperty( - 'data.message.content.0.content.0.text', - expect.stringContaining('\n[exit code: 1]'), - ) - }) - - it('serves grouped models and keeps a selection for later history and fixture requests', async () => { - const api = createFixtureApi() - const sessionId = sid('fx-alpha') - const catalog = await api.sessionRemote.modelCatalog() - if (!catalog.ok) throw new Error('models failed') - expect(catalog.value.groups.map(group => group.name)).toEqual(['DeepSeek', 'OpenAI']) - expect(catalog.value.groups[0]?.models.map(model => model.id)) - .toEqual(['deepseek-v4-flash', 'deepseek-v4-pro']) - - const selected = await api.sessions.selectModel(req({ - sessionId, - provider: 'openai', - model: 'gpt-5', - })) - if (!selected.result.ok) throw new Error('selection failed') - expect(selected.result.value.selected).toEqual({ provider: 'openai', model: 'gpt-5' }) - const history = await api.sessions.history(req({ sessionId })) - if (!history.result.ok) throw new Error('history failed') - - const prompt = await api.sessions.prompt(req({ - sessionId, - mode: 'queue', - content: [{ type: 'text', text: 'report model' }], - })) - expect(prompt.result.ok).toBe(true) - await new Promise(resolve => setTimeout(resolve, 600)) - const after = await api.sessions.history(req({ sessionId })) - if (!after.result.ok) throw new Error('history failed') - expect(JSON.stringify(after.result.value.records)).toContain('openai/gpt-5') - }) - - it('serves configured DeepSeek readiness and keeps credential values write-only', async () => { - const api = createFixtureApi() - const settings = await api.settingsRemote.describe() - if (!settings.ok) throw new Error('settings describe failed') - expect((settings.value as { namespaces: unknown[] }).namespaces).toMatchObject([{ - ns: 'llm-deepseek', - value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, - secrets: [{ path: ['apiKey'], set: false }], - }]) - for (const result of [ - await api.settingsRemote.update('llm-deepseek', {}, undefined), - await api.settingsRemote.replace('llm-deepseek', {}, undefined), - ]) { - expect(result).toMatchObject({ - ok: false, - error: { code: 'settings/rejected', message: 'fixture: the minimal readiness settings descriptor is read-only' }, - }) - } - - const describe = async (refs: readonly string[]): Promise> => { - const result = await api.credentialRemote.describe(refs) - if (!result.ok) throw new Error('credential describe failed') - return result.value as Record - } - expect(await describe(['DEEPSEEK_API_KEY', 'TEST_API_KEY'])).toEqual({ - DEEPSEEK_API_KEY: { configured: true, source: 'file', writable: true }, - TEST_API_KEY: { configured: false, writable: true }, - }) - await api.credentialRemote.set('TEST_API_KEY', 'write-only-fixture-secret') - expect((await describe(['TEST_API_KEY'])).TEST_API_KEY).toEqual({ - configured: true, - source: 'file', - writable: true, - }) - await api.credentialRemote.unset('TEST_API_KEY') - expect((await describe(['TEST_API_KEY'])).TEST_API_KEY).toEqual({ configured: false, writable: true }) - }) - - it('emits the todo/write snapshot at the real tool boundary: between tool/call and tool/result, timestamps monotonic', async () => { - const api = createFixtureApi() - const tail = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 10 })) - if (!tail.result.ok) throw new Error('history failed') - const events = historyEvents(tail.result.value.records) - const todoAt = events.findIndex(e => e.type === 'todo/write') - expect(todoAt).toBeGreaterThan(0) - // Production ordering (the tool appends mid-execution): call → snapshot → result. - expect(events[todoAt - 1]?.type).toBe('tool/call') - expect(events[todoAt + 1]?.type).toBe('tool/result') - const times = events.slice(todoAt - 1, todoAt + 2).map(e => e.time) - expect(times[0]).toBeLessThanOrEqual(times[1] ?? 0) - expect(times[1]).toBeLessThanOrEqual(times[2] ?? 0) - // The sample is a parallel plan: this fixture chooses the parallel policy, - // so the surfaces fed from here face more than one active item. - const snapshot = events[todoAt] as { data: { todos: { status: string }[] } } - expect(snapshot.data.todos.filter(t => t.status === 'in_progress')).toHaveLength(2) - }) - - it('create adds a session and announces it through the Host Remote event stream', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const seen: FixtureRemoteEventNotificationFrame[] = [] - const consuming = (async () => { - for await (const frame of api.remoteEvents(abort.signal)) { - if (frame.type !== 'emit' || frame.event !== 'api-session/added') continue - seen.push(frame) - abort.abort() - break - } - })() - await new Promise(resolve => setTimeout(resolve, 10)) // let the stream register - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - await consuming - if (!created.result.ok) throw new Error('create failed') - const createdId = created.result.value.sessionId - expect(seen).toHaveLength(1) - const added = seen[0] - expect(added).toMatchObject({ - event: 'api-session/added', - args: [{ sessionId: createdId, blank: true, cwd: '/tmp/fixture' }], - }) - const list = await api.sessions.list(req({})) - if (!list.result.ok) throw new Error('list failed') - expect(list.result.value.items.some(s => s.sessionId === createdId)).toBe(true) - }) - - it('prompt replays a full streamed turn and cancel mid-replay freezes with (已中断)', async () => { - const api = createFixtureApi() - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - const followAbort = new AbortController() - const controlAbort = new AbortController() - const controlFrames: FixtureControlFrame[] = [] - const followPromise = collectValues( - api.sessionRemote.follow(id, followAbort.signal), - followAbort, - frames => frames.some(frame => frame.type === 'event' && frame.event.type === 'turn/end'), - ) - const controlPromise = (async () => { - for await (const frame of api.sessionRemote.control(controlAbort.signal)) controlFrames.push(frame) - })() - await new Promise(resolve => setTimeout(resolve, 10)) - // Unknown session → session/not-found with the id echoed in details. - const missing = await api.sessions.prompt(req({ sessionId: sid('ghost'), mode: 'queue' as const, content: [{ type: 'text' as const, text: 'x' }] })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'session/not-found', details: { sessionId: 'ghost' } } }) - // Real prompt: replay starts (running flips true), cancel freezes it. - const accepted = await api.sessions.prompt(req({ sessionId: id, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'render markdown' }] })) - expect(accepted.result).toMatchObject({ ok: true, value: { accepted: true } }) - await new Promise(resolve => setTimeout(resolve, 120)) // a couple of typewriter ticks - await api.sessions.cancel(req({ sessionId: id })) - const frames = await followPromise - const types = frames.flatMap(frame => frame.type === 'event' ? [frame.event.type] : []) - const assistantFrames = frames.flatMap(frame => frame.type === 'assistant-stream' ? [frame.frame] : []) - expect(types).toContain('turn/start') - expect(types).toContain('user/message') - expect(types).toContain('assistant/message') - expect(assistantFrames.some(frame => frame.type === 'chunk')).toBe(true) - const committed = assistantFrames.find(frame => frame.type === 'end' - && frame.outcome.kind === 'committed' - && frame.outcome.eventType === 'assistant/message') - expect(committed).toMatchObject({ - type: 'end', - index: assistantFrames.filter(frame => frame.type === 'chunk' - && frame.attemptId === committed?.attemptId).length, - }) - expect(types.at(-1)).toBe('turn/end') - // Capacity is durable log state, not a transient frame: the prompt path - // records request/context and the projection carries it to the client. - expect(types).toContain('request/context') - await vi.waitFor(() => { - expect(controlFrames.some(frame => - frame.type === 'projection' - && frame.key === 'contextBreakdown' - && (frame.value as { messageTokens?: number }).messageTokens! > 0)).toBe(true) - }) - expect(controlFrames.some(frame => - frame.type === 'projection' - && frame.key === 'tokenUsage' - && (frame.value as { outputTokens?: number }).outputTokens === 8)).toBe(true) - expect(controlFrames.some(frame => - frame.type === 'projection' - && frame.key === 'contextPressure' - && (frame.value as { contextWindow?: number }).contextWindow === 128_000)).toBe(true) - const finalize = frames.find(frame => frame.type === 'event' && frame.event.type === 'assistant/message') - if (finalize?.type !== 'event') throw new Error('assistant final event missing') - if (finalize.event.type !== 'assistant/message') throw new Error('assistant final event has wrong type') - expect(finalize.event.data.interrupted).toBe(true) - expect(finalize.event.data.stream.length).toBeGreaterThan(0) - controlAbort.abort() - await controlPromise - // Idle cancel: no replay in flight, must not explode; running flips false. - const idleCancel = await api.sessions.cancel(req({ sessionId: id })) - expect(idleCancel.result).toMatchObject({ ok: true }) - }) - - it('steer during a replay lands a user/message inside the current turn and the replay continues', async () => { - const api = createFixtureApi() - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - const abort = new AbortController() - const framesPromise = collectValues(api.sessionRemote.follow(id, abort.signal), abort, - frames => frames.some(frame => frame.type === 'event' && frame.event.type === 'turn/end')) - await new Promise(resolve => setTimeout(resolve, 10)) - await api.sessions.prompt(req({ sessionId: id, mode: 'queue' as const, content: [{ type: 'text' as const, text: '短' }] })) - await api.sessions.prompt(req({ sessionId: id, mode: 'steer' as const, content: [{ type: 'text' as const, text: '插话' }] })) - const frames = await framesPromise - const types = frames.flatMap(frame => frame.type === 'event' ? [frame.event.type] : []) - expect(JSON.stringify(frames)).toContain('插话') - expect(types.at(-1)).toBe('turn/end') // steer did not restart the turn - }) - - it('control replays projections while resident Remote Events retain ids across reconnects', async () => { - const api = createFixtureApi() - const first = await readControlBaseline(api.sessionRemote) - const second = await readControlBaseline(api.sessionRemote) - expect(first.value.approvals).toEqual([]) - expect(first.value.questions).toEqual([]) - const alpha = first.value.projections['fx-alpha'] - expect(alpha?.asOfSeq).toBeGreaterThan(0) - expect(alpha?.values).toMatchObject({ - title: 'Fixture 历史会话', - plan: { active: false, pending: false }, - goal: null, - imageLimits: { maxImagesPerMessage: 20, maxImageBytes: 5 * 1024 * 1024 }, - }) - const breakdown = alpha?.values['contextBreakdown'] as { systemTokens: number; messageTokens: number } - expect(breakdown.messageTokens).toBeGreaterThan(0) - // The seeded system/message at surface node 0 prices the system figure. - expect(breakdown.systemTokens).toBeGreaterThan(0) - expect((alpha?.values['sessionStats'] as { steps: number }).steps).toBeGreaterThan(0) - expect(second.value.projections['fx-alpha']).toEqual(alpha) - - const firstEvents = await readResidentRemoteEvents(api, 2) - const secondEvents = await readResidentRemoteEvents(api, 2) - const firstApproval = firstEvents.find(frame => frame.event === 'approval/request') - const firstQuestion = firstEvents.find(frame => frame.event === 'user-questions/request') - const secondApproval = secondEvents.find(frame => frame.event === 'approval/request') - const secondQuestion = secondEvents.find(frame => frame.event === 'user-questions/request') - expect(firstApproval).toMatchObject({ - type: 'waterfall', - request: { toolName: 'dangerous_tool' }, - agentId: 'fx-alpha', - }) - expect(firstQuestion).toMatchObject({ - type: 'waterfall', - agentId: 'fx-alpha', - }) - expect(Array.isArray(firstQuestion?.request.questions)).toBe(true) - expect(secondApproval?.eventId).toBe(firstApproval?.eventId) - expect(secondQuestion?.eventId).toBe(firstQuestion?.eventId) - expect(await readOpeningCursor(api.sessionRemote, sid('fx-alpha'))).toBeGreaterThan(0) - }) - - it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => { - const api = createFixtureApi() - const created = await api.sessions.create(req({})) - if (!created.result.ok) throw new Error('create failed') - const abort = new AbortController() - const framesPromise = collectValues( - api.sessionRemote.follow(created.result.value.sessionId, abort.signal), - abort, - frames => frames.some(frame => frame.type === 'event' && frame.event.type === 'turn/end'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - // steer while idle + a non-text content block (covers the '' arm of the text join). - await api.sessions.prompt(req({ - sessionId: created.result.value.sessionId, mode: 'steer' as const, - content: [{ type: 'text' as const, text: '短' }, { type: 'image', data: 'x' } as never], - })) - const frames = await framesPromise - const types = frames.flatMap(frame => frame.type === 'event' ? [frame.event.type] : []) - expect(types[0]).toBe('turn/start') // idle steer degraded to a queued turn, not an in-turn insert - }) - - it('gamma interval flip emits a Remote status event and its empty follow source opens at -1', async () => { - vi.useFakeTimers() - try { - const api = createFixtureApi() - const abort = new AbortController() - const hostSeen: FixtureRemoteEventFrame[] = [] - const consuming = (async () => { - for await (const frame of api.remoteEvents(abort.signal)) hostSeen.push(frame) - })() - await vi.advanceTimersByTimeAsync(5001) // interval fires: fx-gamma flips running=true (no log exists) - expect(hostSeen).toContainEqual({ - type: 'emit', - event: 'api-session/status', - args: [sid('fx-gamma'), true], - }) - expect(await readOpeningCursor(api.sessionRemote, sid('fx-gamma'))).toBe(-1) - abort.abort() - await vi.advanceTimersByTimeAsync(10) - await consuming - } finally { - vi.useRealTimers() - } - }) - - it('answers a resident question through its Remote Event id and stops replaying it', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const stream = api.remoteEvents(abort.signal) - const iterator = stream[Symbol.asyncIterator]() - const question = await nextRemoteEvent( - iterator, - frame => isRemoteEventRequest(frame) && frame.event === 'user-questions/request', - ) - if (!isRemoteEventRequest(question)) throw new Error('fixture question Remote Event missing') - const clientId = await stream.clientId - await expect(api.answerRemoteEvent({ - clientId, - eventId: 'unrelated', - outcome: { kind: 'result', value: {} }, - })).resolves.toEqual({ ok: true, value: undefined }) - await expect(api.answerRemoteEvent({ - clientId, - eventId: question.eventId, - outcome: { kind: 'result', value: { answers: {} } }, - })).resolves.toEqual({ ok: true, value: undefined }) - const cancelled = await nextRemoteEvent( - iterator, - frame => isRemoteEventCancellation(frame) && frame.eventId === question.eventId, - ) - expect(cancelled).toEqual({ type: 'cancel', eventId: question.eventId }) - abort.abort() - await iterator.return?.() - - await expect(api.answerRemoteEvent({ - clientId, - eventId: question.eventId, - outcome: { kind: 'result', value: { answers: {} } }, - })).resolves.toMatchObject({ ok: false, error: { code: 'gateway/invocation-unavailable' } }) - const remaining = await readResidentRemoteEvents(api, 1) - expect(remaining.map(frame => frame.event)).toEqual(['approval/request']) - - const cancelledApi = createFixtureApi() - const cancelAbort = new AbortController() - const cancelStream = cancelledApi.remoteEvents(cancelAbort.signal) - const cancelIterator = cancelStream[Symbol.asyncIterator]() - const cancelQuestion = await nextRemoteEvent( - cancelIterator, - frame => isRemoteEventRequest(frame) && frame.event === 'user-questions/request', - ) - if (!isRemoteEventRequest(cancelQuestion)) throw new Error('fixture cancellation question missing') - await expect(cancelledApi.answerRemoteEvent({ - clientId: await cancelStream.clientId, - eventId: cancelQuestion.eventId, - outcome: { - kind: 'rejected', - error: { name: 'UserQuestionError', message: 'skip', code: 'ASK_CANCELLED' }, - }, - })).resolves.toEqual({ ok: true, value: undefined }) - cancelAbort.abort() - await cancelIterator.return?.() - const afterCancellation = await readResidentRemoteEvents(cancelledApi, 1) - expect(afterCancellation.map(frame => frame.event)).toEqual(['approval/request']) - }) - - it('answers a resident approval and broadcasts cancellation to its active delivery', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const stream = api.remoteEvents(abort.signal) - const iterator = stream[Symbol.asyncIterator]() - const approval = await nextRemoteEvent( - iterator, - frame => isRemoteEventRequest(frame) && frame.event === 'approval/request', - ) - if (!isRemoteEventRequest(approval)) throw new Error('fixture approval Remote Event missing') - - await expect(api.answerRemoteEvent({ - clientId: await stream.clientId, - eventId: approval.eventId, - outcome: { kind: 'result', value: 'allowed-once' }, - })).resolves.toEqual({ ok: true, value: undefined }) - const cancelled = await nextRemoteEvent( - iterator, - frame => isRemoteEventCancellation(frame) && frame.eventId === approval.eventId, - ) - expect(cancelled).toEqual({ type: 'cancel', eventId: approval.eventId }) - abort.abort() - await iterator.return?.() - - await expect(api.answerRemoteEvent({ - clientId: await stream.clientId, - eventId: approval.eventId, - outcome: { kind: 'next' }, - })).resolves.toMatchObject({ ok: false, error: { code: 'gateway/invocation-unavailable' } }) - const remaining = await readResidentRemoteEvents(api, 1) - expect(remaining.map(frame => frame.event)).toEqual(['user-questions/request']) - }) - - it('createDirectory under the root mints /name whose listing and crumbs share the identity', async () => { - const api = createFixtureApi() - const created = await api.directoryPickerRemote.createDirectory('/', 'srv') - if (!created.ok) throw new Error('create failed') - expect(created.value).toBe('/srv') - const listed = await api.directoryPickerRemote.list('/srv') - if (!listed.ok) throw new Error('list failed') - expect(listed.value.crumbs).toEqual([ - { name: '/', path: '/', hidden: false }, - { name: 'srv', path: '/srv', hidden: false }, - ]) - const root = await api.directoryPickerRemote.list('/') - if (!root.ok) throw new Error('root list failed') - expect(root.value.entries).toContainEqual({ name: 'srv', path: '/srv', hidden: false }) - }) - - it('workspace/follow serves the resident baseline and create reuses on path collision', async () => { - const api = createFixtureApi() - const baseline = await readWorkspaceBaseline(api.workspaceRemote) - expect(baseline.items).toEqual([ - expect.objectContaining({ - workspaceId: 'fx-ws-fixture', path: '/tmp/fixture', title: 'fixture', - sessionIds: ['fx-alpha', 'fx-beta', 'fx-gamma'], - }), - expect.objectContaining({ - workspaceId: 'fx-ws-home', path: '/home/fixture/Documents/project', title: 'project', - sessionIds: [], - }), - ]) - // path collision → the existing entity comes back, created:false, no frame. - const reused = await api.workspace.create(req({ path: '/tmp/fixture' })) - if (!reused.result.ok) throw new Error('reuse failed') - expect(reused.result.value).toMatchObject({ created: false, workspace: { workspaceId: 'fx-ws-fixture' } }) - }) - - it('workspace.create on a fresh path mints a new entity and pushes an upsert', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const consuming = collectValues( - api.workspaceRemote.follow(abort.signal), - abort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.path === '/tmp/fixture-workspaces/nova'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const created = await api.workspace.create(req({ path: '/tmp/fixture-workspaces/nova' })) - if (!created.result.ok) throw new Error('create failed') - expect(created.result.value.created).toBe(true) - expect(created.result.value.workspace).toMatchObject({ - path: '/tmp/fixture-workspaces/nova', title: 'nova', sessionIds: [], - }) - const frames = await consuming - expect(frames.at(-1)).toEqual({ type: 'upsert', workspace: created.result.value.workspace }) - // A basename-less path serves as its own title. - const rootPath = await api.workspace.create(req({ path: '/' })) - if (!rootPath.result.ok) throw new Error('rootPath failed') - expect(rootPath.result.value.workspace.title).toBe('/') - }) - - it('workspace.rename covers not-found, conflict, no-op, and the changed frame', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const consuming = collectValues( - api.workspaceRemote.follow(abort.signal), - abort, - frames => frames.filter(frame => frame.type === 'upsert').length >= 2, - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const wsid = 'fx-ws-fixture' as WorkspaceId - const missing = await api.workspace.rename(req({ workspaceId: 'fx-ws-void' as WorkspaceId, title: 'x' })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found', details: { workspaceId: 'fx-ws-void' } } }) - - await api.workspace.create(req({ path: '/tmp/fixture-workspaces/occupied' })) - const conflict = await api.workspace.rename(req({ workspaceId: wsid, title: ' occupied ' })) - expect(conflict.result).toMatchObject({ ok: false, error: { code: 'workspace/name-conflict', details: { name: 'occupied' } } }) - - const noop = await api.workspace.rename(req({ workspaceId: wsid, title: ' fixture ' })) - if (!noop.result.ok) throw new Error('no-op rename failed') - expect(noop.result.value.workspace.title).toBe('fixture') - - const renamed = await api.workspace.rename(req({ workspaceId: wsid, title: 'renamed' })) - if (!renamed.result.ok) throw new Error('rename failed') - expect(renamed.result.value.workspace.title).toBe('renamed') - const frames = await consuming - // Only the create and the effective rename emit frames; the no-op stays silent. - const upserts = frames.filter(frame => frame.type === 'upsert') - expect(upserts).toHaveLength(2) - expect(upserts[1]).toMatchObject({ workspace: { workspaceId: wsid, title: 'renamed' } }) - }) - - it('session.rename covers not-found, blank title, and the accepted append + title frame', async () => { - const api = createFixtureApi() - const followAbort = new AbortController() - const controlAbort = new AbortController() - const followPromise = collectValues( - api.sessionRemote.follow(sid('fx-alpha'), followAbort.signal), - followAbort, - frames => frames.some(frame => frame.type === 'event' - && (frame.event as { type: string }).type === 'session/title'), - ) - const controlPromise = collectValues( - api.sessionRemote.control(controlAbort.signal), - controlAbort, - frames => frames.some(frame => frame.type === 'projection' && frame.key === 'title' && frame.value === '重命名'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - - const missing = await api.sessions.rename(req({ sessionId: sid('fx-void'), title: 'x' })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'session/not-found', details: { sessionId: 'fx-void' } } }) - - const blank = await api.sessions.rename(req({ sessionId: sid('fx-alpha'), title: ' ' })) - expect(blank.result).toMatchObject({ ok: false, error: { code: 'session/title-invalid', details: { sessionId: 'fx-alpha' } } }) - - const renamed = await api.sessions.rename(req({ sessionId: sid('fx-alpha'), title: ' 重命名 ' })) - if (!renamed.result.ok) throw new Error('rename failed') - expect(renamed.result.value.title).toBe('重命名') - const acceptedSeq = renamed.result.value.seq - // The response seq addresses the appended title event (the client plane - // has no session/title in its event union — titles ride the projection — - // so the event is located by seq and its payload checked structurally). - const history = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 100 })) - if (!history.result.ok) throw new Error('history failed') - const appended = historyEvents(history.result.value.records).find(event => event.seq === acceptedSeq) - expect(appended).toMatchObject({ - type: 'session/title', - data: { title: '重命名', messageSeqs: [], source: { kind: 'user' } }, - }) - const followed = await followPromise - expect(followed.some(frame => frame.type === 'event' - && frame.event.seq === acceptedSeq - && (frame.event as { readonly type: string }).type === 'session/title')).toBe(true) - const frames = await controlPromise - const titleFrames = frames.filter(frame => - frame.type === 'projection' - && frame.key === 'title' - && frame.sessionId === sid('fx-alpha') - && frame.value === '重命名') - expect(titleFrames).toHaveLength(1) - expect(titleFrames[0]).toMatchObject({ seq: acceptedSeq }) - }) - - it('workspace.insertSessionBefore moves, appends, no-ops, and rejects invalid ids', async () => { - const api = createFixtureApi() - const wsid = 'fx-ws-fixture' as WorkspaceId - const missing = await api.workspace.insertSessionBefore(req({ workspaceId: 'fx-ws-void' as WorkspaceId, sessionId: sid('fx-alpha') })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found' } }) - const ghost = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-ghost') })) - expect(ghost.result).toMatchObject({ ok: false, error: { code: 'workspace/move-invalid', details: { sessionId: 'fx-ghost' } } }) - const badAnchor = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-alpha'), beforeSessionId: sid('fx-ghost') })) - expect(badAnchor.result).toMatchObject({ ok: false, error: { code: 'workspace/move-invalid', details: { beforeSessionId: 'fx-ghost' } } }) - - const moved = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-gamma'), beforeSessionId: sid('fx-beta') })) - if (!moved.result.ok) throw new Error('move failed') - expect(moved.result.value.workspace.sessionIds).toEqual(['fx-alpha', 'fx-gamma', 'fx-beta']) - const appended = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-alpha') })) - if (!appended.result.ok) throw new Error('append failed') - expect(appended.result.value.workspace.sessionIds).toEqual(['fx-gamma', 'fx-beta', 'fx-alpha']) - const before = appended.result.value.workspace.updatedAt - const noop = await api.workspace.insertSessionBefore(req({ workspaceId: wsid, sessionId: sid('fx-alpha') })) - if (!noop.result.ok) throw new Error('no-op move failed') - expect(noop.result.value.workspace.sessionIds).toEqual(['fx-gamma', 'fx-beta', 'fx-alpha']) - expect(noop.result.value.workspace.updatedAt).toBe(before) - }) - - it('workspace.delete removes only the Workspace row and emits the removal frame', async () => { - const api = createFixtureApi() - const abort = new AbortController() - const consuming = collectValues( - api.workspaceRemote.follow(abort.signal), - abort, - frames => frames.some(frame => frame.type === 'remove'), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const missing = await api.workspace.delete(req({ workspaceId: 'fx-ws-void' as WorkspaceId })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found' } }) - const deleted = await api.workspace.delete(req({ workspaceId: 'fx-ws-fixture' as WorkspaceId })) - expect(deleted.result).toEqual({ ok: true, value: { deleted: true } }) - const frames = await consuming - expect(frames.at(-1)).toEqual({ type: 'remove', workspaceId: 'fx-ws-fixture' }) - const baseline = await readWorkspaceBaseline(api.workspaceRemote) - expect(baseline.items.some(workspace => workspace.workspaceId === 'fx-ws-fixture')).toBe(false) - const sessions = await api.sessions.list(req({})) - if (!sessions.result.ok) throw new Error('session list failed') - expect(sessions.result.value.items.map(session => session.sessionId)).toContain('fx-alpha') - }) - - it('session.create({workspaceId}) lands on the account and unknown ids error', async () => { - const api = createFixtureApi() - const hostAbort = new AbortController() - const workspaceAbort = new AbortController() - const seen: FixtureRemoteEventNotificationFrame[] = [] - const consuming = (async () => { - for await (const frame of api.remoteEvents(hostAbort.signal)) { - if (frame.type !== 'emit' || frame.event !== 'api-session/added') continue - seen.push(frame) - hostAbort.abort() - break - } - })() - const workspaceFrames = collectValues( - api.workspaceRemote.follow(workspaceAbort.signal), - workspaceAbort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.sessionIds.length === 4), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const missing = await api.sessions.create(req({ workspaceId: 'fx-ws-void' as WorkspaceId })) - expect(missing.result).toMatchObject({ ok: false, error: { code: 'workspace/not-found', details: { workspaceId: 'fx-ws-void' } } }) - const created = await api.sessions.create(req({ workspaceId: 'fx-ws-fixture' as WorkspaceId })) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - await consuming - const added = seen[0] - expect(added).toMatchObject({ - event: 'api-session/added', - args: [{ sessionId: id, blank: true, cwd: '/tmp/fixture' }], - }) - expect((await workspaceFrames).at(-1)).toMatchObject({ - type: 'upsert', - workspace: { - workspaceId: 'fx-ws-fixture', - sessionIds: [id, 'fx-alpha', 'fx-beta', 'fx-gamma'], - }, - }) - }) - - it('supports an empty baseline, preallocated ids, independent streams, and idempotent retry', async () => { - const api = createFixtureApi({ empty: true, createFrameOrder: 'workspace-first' }) - const initialSessions = await api.sessions.list(req({})) - expect(initialSessions.result).toMatchObject({ ok: true, value: { items: [] } }) - expect(await readWorkspaceBaseline(api.workspaceRemote)).toEqual({ - items: [], - archivedSessionIds: [], - }) - - const made = await api.workspace.create(req({ path: '/tmp/fixture-workspaces/nova' })) - if (!made.result.ok) throw new Error('workspace create failed') - const hostAbort = new AbortController() - const workspaceAbort = new AbortController() - const hostFrames = collectValues( - api.remoteEvents(hostAbort.signal), - hostAbort, - frames => frames.length === 1, - ) - const workspaceFrames = collectValues( - api.workspaceRemote.follow(workspaceAbort.signal), - workspaceAbort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.sessionIds.includes(sid('fx-preallocated'))), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const preallocated = sid('fx-preallocated') - const created = await api.sessions.create(req({ - workspaceId: made.result.value.workspace.workspaceId, - sessionId: preallocated, - })) - expect(created.result).toEqual({ ok: true, value: { sessionId: preallocated } }) - expect((await workspaceFrames).at(-1)).toMatchObject({ - type: 'upsert', workspace: { sessionIds: [preallocated] }, - }) - const added = (await hostFrames)[0] - expect(added).toMatchObject({ - event: 'api-session/added', - args: [{ - sessionId: preallocated, - blank: true, - cwd: made.result.value.workspace.path, - }], - }) - - const retried = await api.sessions.create(req({ - workspaceId: made.result.value.workspace.workspaceId, - sessionId: preallocated, - })) - expect(retried.result).toEqual({ ok: true, value: { sessionId: preallocated } }) - const listed = await api.sessions.list(req({})) - if (!listed.result.ok) throw new Error('session list failed') - expect(listed.result.value.items.filter(item => item.sessionId === preallocated)).toHaveLength(1) - - const conflict = await api.sessions.create(req({ sessionId: preallocated, cwd: '/elsewhere' })) - expect(conflict.result).toMatchObject({ - ok: false, - error: { code: 'session/conflict', details: { sessionId: preallocated, requestedCwd: '/elsewhere' } }, - }) - }) - - it('attaches an existing ungrouped Session to a matching Workspace', async () => { - const api = createFixtureApi() - const sessionId = sid('fx-existing-ungrouped') - await expect(api.sessions.create(req({ sessionId, cwd: '/tmp/fixture' }))).resolves.toMatchObject({ - result: { ok: true, value: { sessionId } }, - }) - - await expect(api.sessions.create(req({ - sessionId, - workspaceId: 'fx-ws-fixture' as WorkspaceId, - }))).resolves.toMatchObject({ result: { ok: true, value: { sessionId } } }) - - const workspaces = await readWorkspaceBaseline(api.workspaceRemote) - expect(workspaces.items[0]?.sessionIds).toContain(sessionId) - }) - - it('reports a conflict without an existing cwd detail for an unrecorded cwd', async () => { - const api = createFixtureApi() - const listed = await api.sessions.list(req({})) - if (!listed.result.ok) throw new Error('session list failed') - const existing = listed.result.value.items.find(item => item.sessionId === sid('fx-alpha')) - if (existing === undefined) throw new Error('fixture Session missing') - delete existing.cwd - - const conflict = await api.sessions.create(req({ sessionId: existing.sessionId })) - expect(conflict.result).toEqual({ - ok: false, - error: { - code: 'session/conflict', - message: `session ${existing.sessionId} already uses no cwd`, - details: { sessionId: existing.sessionId, requestedCwd: '/tmp/fixture' }, - }, - }) - }) - - it('publishes an ungrouped Session when Workspace attachment fails', async () => { - const api = createFixtureApi({ failWorkspaceAttach: true }) - const sessionId = sid('fx-partial') - const created = await api.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - })) - expect(created.result).toMatchObject({ - ok: false, - error: { code: 'session/workspace-attach-failed', details: { sessionId, workspaceId: 'fx-ws-fixture' } }, - }) - const listed = await api.sessions.list(req({})) - const workspaces = await readWorkspaceBaseline(api.workspaceRemote) - if (!listed.result.ok) throw new Error('list failed') - expect(listed.result.value.items.filter(item => item.sessionId === sessionId)).toHaveLength(1) - expect(workspaces.items[0]?.sessionIds).not.toContain(sessionId) - - const retried = await api.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - })) - expect(retried.result).toMatchObject({ ok: false, error: { code: 'session/workspace-attach-failed' } }) - const afterRetry = await api.sessions.list(req({})) - if (!afterRetry.result.ok) throw new Error('list failed') - expect(afterRetry.result.value.items.filter(item => item.sessionId === sessionId)).toHaveLength(1) - }) - - it('reconciles a dropped create response and can reject a prompt before acceptance', async () => { - const sessionId = sid('fx-lost-response') - const dropped = createFixtureApi({ dropSessionCreateResponse: true }) - await expect(Promise.resolve().then(() => dropped.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - })))).rejects.toThrow(/dropped session\.create response/) - const listed = await dropped.sessions.list(req({})) - const workspaces = await readWorkspaceBaseline(dropped.workspaceRemote) - if (!listed.result.ok) throw new Error('list failed') - expect(listed.result.value.items.some(item => item.sessionId === sessionId)).toBe(true) - expect(workspaces.items[0]?.sessionIds).toContain(sessionId) - await expect(dropped.sessions.create(req({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId, - }))).resolves.toMatchObject({ result: { ok: true, value: { sessionId } } }) - - const rejecting = createFixtureApi({ empty: true, rejectPrompt: true }) - const real = await rejecting.sessions.create(req({ sessionId: sid('fx-rejected') })) - if (!real.result.ok) throw new Error('session create failed') - const prompt = await rejecting.sessions.prompt(req({ - sessionId: real.result.value.sessionId, - mode: 'queue' as const, - content: [{ type: 'text' as const, text: 'keep me' }], - })) - expect(prompt.result).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } }) - const imagePrompt = await rejecting.sessions.prompt(req({ - sessionId: real.result.value.sessionId, - mode: 'queue' as const, - content: [{ type: 'image' as const, mediaType: 'image/png' as const, data: 'iVBORw0KGgo=' }], - })) - expect(imagePrompt.result).toMatchObject({ - ok: false, - error: { code: 'session/attachment-invalid', details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' } }, - }) - }) - - it('timing hooks: history delay + one-shot failure, silent append, and breakStreams end open generators', async () => { - const api = createFixtureApi() - const hooks = timing() - // One-shot transport failure after transit delay. - hooks.setHistoryDelay(5) - hooks.failNextHistory() - await expect(api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 }))).rejects.toThrow(/simulated history transport failure/) - hooks.setHistoryDelay(0) - // The failure was one-shot: the next call succeeds. - const ok = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 })) - expect(ok.result.ok).toBe(true) - // A durable append without a live frame creates a detectable seq gap. - const gapAbort = new AbortController() - const gapIterator = api.sessionRemote.follow(sid('fx-alpha'), gapAbort.signal)[Symbol.asyncIterator]() - const opening = await gapIterator.next() - if (opening.done || opening.value.type !== 'snapshot') throw new Error('follow opening snapshot missing') - hooks.appendSilent('fx-alpha', '静默丢帧') - hooks.appendUser('fx-alpha', '正常直播') - await expect(gapIterator.next()).rejects.toThrow(/stream skipped seq/) - - // Reopening replaces the window with a complete snapshot containing both durable events. - const followAbort = new AbortController() - const controlAbort = new AbortController() - const followed: FixtureFollowFrame[] = [] - const controlled: FixtureControlFrame[] = [] - const following = (async () => { - for await (const frame of api.sessionRemote.follow(sid('fx-alpha'), followAbort.signal)) { - followed.push(frame) - } - })() - const controlling = (async () => { - for await (const frame of api.sessionRemote.control(controlAbort.signal)) controlled.push(frame) - })() - await new Promise(resolve => setTimeout(resolve, 10)) - await vi.waitFor(() => { - const snapshot = followed.find(frame => frame.type === 'snapshot') - const events = snapshot === undefined ? [] : historyEvents(snapshot.records) - expect(events.some(event => JSON.stringify(event.data).includes('静默丢帧'))).toBe(true) - expect(events.some(event => JSON.stringify(event.data).includes('正常直播'))).toBe(true) - }) - hooks.appendTitle('fx-alpha', 'Fixture 修订标题') - hooks.beginModelRetry('fx-alpha') - hooks.scheduleModelRetry('fx-alpha') - hooks.completeModelRetry('fx-alpha') - hooks.beginModelRetry('fx-alpha') - hooks.cancelModelRetryDuringBackoff('fx-alpha') - await vi.waitFor(() => { - expect(followed.some(frame => frame.type === 'event' && (frame.event as { type: string }).type === 'llm/retry')).toBe(true) - expect(followed.some(frame => frame.type === 'event' && JSON.stringify(frame.event.data).includes('重试后的完整回复'))).toBe(true) - expect(followed.some(frame => frame.type === 'event' - && frame.event.type === 'turn/end' - && frame.event.data.reason.kind === 'aborted')).toBe(true) - expect(controlled.some(frame => frame.type === 'projection' - && frame.key === 'title' - && frame.value === 'Fixture 修订标题')).toBe(true) - }) - expect(followed.some(frame => frame.type === 'event' && (frame.event as { type: string }).type === 'session/title')).toBe(true) - // Paging and resumed follow agree on the recovered durable event. - const repull = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 5 })) - if (!repull.result.ok) throw new Error('repull failed') - expect(JSON.stringify(repull.result.value.records)).toContain('静默丢帧') - // breakStreams force-ends follow and control without client aborts. - await new Promise(resolve => setTimeout(resolve, 10)) - hooks.breakStreams() - await following - await controlling - expect(followAbort.signal.aborted).toBe(false) - expect(controlAbort.signal.aborted).toBe(false) - }) - - it('paces the opt-in reasoning stress hook from an external interval', async () => { - vi.useFakeTimers() - vi.setSystemTime(0) - const api = createFixtureApi() - const hooks = timing() - expect(hooks.reasoningChunkStormState()).toBeNull() - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 0, 1, 16)).toThrow(/chunk count/) - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 1, 0, 16)).toThrow(/chunks per interval/) - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 1, 1, 0)).toThrow(/reasoning interval/) - const abort = new AbortController() - try { - const streamed = collectValues(api.sessionRemote.follow(sid('fx-alpha'), abort.signal), abort, frames => frames.some(frame => ( - frame.type === 'assistant-stream' - && frame.frame.type === 'chunk' - && isReasoningDeltaChunk(frame.frame.chunk) - && frame.frame.chunk.text.includes('REASONING_STRESS_COMPLETE') - ))) - const marker = hooks.startReasoningChunkStorm('fx-alpha', 3, 2, 16) - expect(() => hooks.startReasoningChunkStorm('fx-alpha', 1, 1, 16)).toThrow(/already running/) - expect(hooks.reasoningChunkStormState()).toMatchObject({ emitted: 0, emitting: true, marker }) - - await vi.advanceTimersByTimeAsync(0) - expect(hooks.reasoningChunkStormState()).toMatchObject({ emitted: 2, emitting: true }) - await vi.advanceTimersByTimeAsync(16) - expect(hooks.reasoningChunkStormState()).toEqual({ - sessionId: 'fx-alpha', chunkCount: 3, chunksPerInterval: 2, intervalMs: 16, - emitted: 3, marker, emitting: false, - }) - - const frames = await streamed - const deltas = frames.flatMap(frame => ( - frame.type === 'assistant-stream' - && frame.frame.type === 'chunk' - && isReasoningDeltaChunk(frame.frame.chunk) - ? [frame.frame.chunk.text] - : [] - )) - expect(deltas).toEqual(['推理', '推理', `\n${marker}`]) - } finally { - abort.abort() - vi.useRealTimers() - } - }) -}) - -describe('fixture Connection RPC', () => { - afterEach(() => { - vi.restoreAllMocks() - vi.unstubAllGlobals() - }) - - it.each(['turn-end', 'omitted', 'past-end'] as const)('forks through the completed turn for a %s anchor', async (anchor) => { - vi.stubGlobal('__fxTiming', (globalThis as Record).__fxTiming) - const { rpc } = createFixtureFaces() - const sessions = createSessionClient(rpc) - const sourceId = sid('fx-alpha') - const initial = await sessions.history({ sessionId: sourceId, maxMessages: 1000 }) - if (!initial.result.ok) throw new Error('source history failed') - const events = historyEvents(initial.result.value.records) - const boundary = events.findLast(event => event.type === 'turn/end') - if (boundary === undefined) throw new Error('source has no completed turn') - expect(initial.result.value.hasMore).toBe(false) - expect((await sessions.rename({ sessionId: sourceId, title: 'Title after the selected turn' })).result.ok).toBe(true) - const source = await sessions.history({ sessionId: sourceId, maxMessages: 1000 }) - if (!source.result.ok) throw new Error('source history after rename failed') - const fork = await rpc.call('/api', 'session/fork', { - args: { request: { - sessionId: sourceId, - ...(anchor === 'omitted' ? {} : { atSeq: anchor === 'turn-end' ? boundary.seq : events.length + 100 }), - } }, - }) - if (!fork.ok) throw new Error('fork failed') - const childId = (fork.value as { sessionId: SessionId }).sessionId - const child = await sessions.history({ sessionId: childId, maxMessages: 1000 }) - if (!child.result.ok) throw new Error('child history failed') - expect(child.result.value.hasMore).toBe(false) - expect(historyEvents(child.result.value.records)).toEqual(events.slice(0, boundary.seq + 1)) - expect(await sessions.history({ sessionId: sourceId, maxMessages: 1000 })).toMatchObject({ result: source.result }) - }) - - it('covers the migrated Remote dispatch table', async () => { - const rpc = createFixtureConnectionRpc() - const sessions = createSessionClient(rpc) - const workspaces = createWorkspaceClient(rpc) - expect((await sessions.search( - { query: 'fixture' }, - new AbortController().signal, - )).result.ok).toBe(true) - const created = await sessions.create({}) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - expect((await sessions.history({ sessionId: id })).result.ok).toBe(true) - expect((await sessions.prompt({ sessionId: id, mode: 'queue', content: [{ type: 'text', text: '嗨' }] })).result.ok).toBe(true) - expect((await sessions.cancel({ sessionId: id })).result.ok).toBe(true) - expect((await readWorkspaceBaseline(createWorkspaceRemote(rpc))).items).not.toHaveLength(0) - const workspace = await workspaces.create({ path: '/tmp/fixture-workspaces/via-client' }) - if (!workspace.result.ok) throw new Error('workspace create failed') - expect(workspace.result.value.workspace.title).toBe('via-client') - const wsid = workspace.result.value.workspace.workspaceId - const renamed = await workspaces.rename({ workspaceId: wsid, title: 'via-client-2' }) - if (!renamed.result.ok) throw new Error('workspace rename failed') - expect(renamed.result.value.workspace.title).toBe('via-client-2') - const attached = await sessions.create({ workspaceId: wsid }) - if (!attached.result.ok) throw new Error('attached create failed') - const moved = await workspaces.insertSessionBefore({ workspaceId: wsid, sessionId: attached.result.value.sessionId }) - if (!moved.result.ok) throw new Error('workspace move failed') - expect(moved.result.value.workspace.sessionIds).toEqual([attached.result.value.sessionId]) - const archived = await workspaces.archiveSession({ sessionId: attached.result.value.sessionId }) - if (!archived.result.ok) throw new Error('workspace archive failed') - expect(archived.result.value.archivedSessionIds).toEqual([attached.result.value.sessionId]) - const unarchived = await workspaces.unarchiveSession({ sessionId: attached.result.value.sessionId }) - if (!unarchived.result.ok) throw new Error('workspace unarchive failed') - expect(unarchived.result.value.archivedSessionIds).toEqual([]) - // A second unarchive of the same id leaves the set unchanged. - const repeated = await workspaces.unarchiveSession({ sessionId: attached.result.value.sessionId }) - if (!repeated.result.ok) throw new Error('repeated workspace unarchive failed') - expect(repeated.result.value.archivedSessionIds).toEqual([]) - }) - - it('folds the goal lifecycle over the Goal Remotes', async () => { - const rpc = createFixtureConnectionRpc() - const sessions = createSessionClient(rpc) - const created = await sessions.create({}) - if (!created.result.ok) throw new Error('create failed') - const id = created.result.value.sessionId - const goal = (endpoint: string, args: Record) => - rpc.call('/api', endpoint, { args: { agentId: id, ...args } }) - - // create → edit → pause → resume → complete → clear; each mutation advances the CAS - // revision by one (state rides the projection frames). - const goalCreated = await goal('goals/create', { request: { objective: 'ship it' } }) - if (!goalCreated.ok) throw new Error('goal create failed') - const { id: goalId, revision } = (goalCreated.value as { ref: { id: string; revision: number } }).ref - expect(revision).toBe(1) - const ref = (at: number) => ({ id: goalId, revision: at }) - expect((await goal('goals/edit', { ref: ref(1), request: { objective: 'ship it v2' } })).ok).toBe(true) - expect(await goal('goals/get', {})).toMatchObject({ - ok: true, value: { objective: 'ship it v2', revision: 2, activation: 'armed' }, - }) - timing().disarmOnlyGoal() - expect(await goal('goals/get', {})).toMatchObject({ - ok: true, value: { objective: 'ship it v2', revision: 2, activation: 'disarmed' }, - }) - expect((await goal('goals/pause', { ref: ref(2) })).ok).toBe(true) - expect((await goal('goals/resume', { ref: ref(3) })).ok).toBe(true) - // A stale ref loses the CAS check. - expect((await goal('goals/pause', { ref: ref(1) })).ok).toBe(false) - expect((await goal('goals/complete', { ref: ref(4) })).ok).toBe(true) - // complete → complete is an invalid transition. - expect((await goal('goals/complete', { ref: ref(5) })).ok).toBe(false) - expect(await goal('goals/clear', { ref: ref(5) })).toEqual({ ok: true, value: ref(6) }) - - const goalHistory = await sessions.history({ sessionId: id }) - if (!goalHistory.result.ok) throw new Error('goal history failed') - const goalEvents = historyEvents(goalHistory.result.value.records).map(event => event as unknown as { - type: string - data: { - operation?: string - source?: { kind?: string; round?: number } - } - }) - const goalChanges = goalEvents.filter(event => event.type === 'goal/change') - expect(goalChanges.map(event => event.data.operation)) - .toEqual(['create', 'edit', 'pause', 'resume', 'complete', 'clear']) - expect(goalEvents.some(event => event.type === 'user/message' - && event.data.source?.kind === 'goal' && event.data.source.round === 0)).toBe(false) - }) - - it('maps empty, prompt-reject, and workspace-first query scenarios', async () => { - vi.stubGlobal('location', { - search: '?fixture=empty&fixturePrompt=reject&fixtureFrames=workspace-first', - }) - const rpc = createFixtureConnectionRpc() - const sessions = createSessionClient(rpc) - const workspaces = createWorkspaceClient(rpc) - const workspaceRemote = createWorkspaceRemote(rpc) - await expect(sessions.list({})).resolves.toMatchObject({ result: { ok: true, value: { items: [] } } }) - const made = await workspaces.create({ path: '/tmp/fixture-workspaces/query-workspace' }) - if (!made.result.ok) throw new Error('workspace create failed') - const hostAbort = new AbortController() - const workspaceAbort = new AbortController() - const hostFrames = collectValues( - openFixtureRemoteEvents(rpc, hostAbort.signal), - hostAbort, - frames => frames.length === 1, - ) - const workspaceFrames = collectValues( - workspaceRemote.follow(workspaceAbort.signal), - workspaceAbort, - frames => frames.some(frame => frame.type === 'upsert' - && frame.workspace.sessionIds.includes(sid('fx-query-session'))), - ) - await new Promise(resolve => setTimeout(resolve, 10)) - const sessionId = sid('fx-query-session') - const created = await sessions.create({ - workspaceId: made.result.value.workspace.workspaceId, - sessionId, - }) - expect(created.result).toMatchObject({ ok: true, value: { sessionId } }) - expect((await workspaceFrames).at(-1)).toMatchObject({ - type: 'upsert', - workspace: { sessionIds: [sessionId] }, - }) - expect((await hostFrames)[0]).toMatchObject({ - event: 'api-session/added', - }) - const rejected = await sessions.prompt({ - sessionId, - mode: 'queue', - content: [{ type: 'text', text: 'retain' }], - }) - expect(rejected.result).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } }) - }) - - it('maps attach-failure and dropped-response query scenarios', async () => { - vi.stubGlobal('location', { search: '?fixture&fixtureAttach=fail' }) - const partial = createFixtureConnectionRpc() - const partialResult = await createSessionClient(partial).create({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId: sid('fx-query-partial'), - }) - expect(partialResult.result).toMatchObject({ - ok: false, - error: { code: 'session/workspace-attach-failed', details: { sessionId: 'fx-query-partial' } }, - }) - - vi.stubGlobal('location', { search: '?fixture&fixtureSessionCreate=drop-response' }) - const dropped = createFixtureConnectionRpc() - await expect(createSessionClient(dropped).create({ - workspaceId: 'fx-ws-fixture' as WorkspaceId, - sessionId: sid('fx-query-dropped'), - })).rejects.toThrow(/dropped session\.create response/) - }) - -}) diff --git a/packages/client/connection/tests/generation.client.spec.ts b/packages/client/connection/tests/generation.client.spec.ts index d2b365de4b..48a6dc5e7b 100644 --- a/packages/client/connection/tests/generation.client.spec.ts +++ b/packages/client/connection/tests/generation.client.spec.ts @@ -7,7 +7,7 @@ import { } from '../src/client/index.ts' type BrowserGlobal = { - location?: { hostname: string; search: string } + location?: { hostname: string } } const contexts = new Set() @@ -20,12 +20,12 @@ afterEach(async () => { }) async function mount(): Promise { - ;(globalThis as BrowserGlobal).location = { hostname: 'localhost', search: '?fixture' } + ;(globalThis as BrowserGlobal).location = { hostname: 'localhost' } const ctx = new Context() contexts.add(ctx) await ctx.plugin({ apply, inject: [] }) const connection = ctx.get('connection') as ConnectionHandle | undefined - if (connection === undefined) throw new Error('fixture did not provide Connection') + if (connection === undefined) throw new Error('plugin did not provide Connection') return connection } diff --git a/packages/client/connection/tsconfig.client.json b/packages/client/connection/tsconfig.client.json index 4266a1defc..ffc18472c2 100644 --- a/packages/client/connection/tsconfig.client.json +++ b/packages/client/connection/tsconfig.client.json @@ -9,7 +9,6 @@ "src/api-path.ts", "src/client/api.ts", "src/client/connection.ts", - "src/client/fixture.ts", "src/client/index.ts", "src/client/random-uuid.ts", "src/client/rpc.ts", @@ -30,24 +29,9 @@ { "path": "../../core/session" }, - { - "path": "../../todo/tool-todo" - }, - { - "path": "../../core/tools" - }, { "path": "../../credentials/credentials" }, - { - "path": "../../settings/settings" - }, - { - "path": "../../host/directory-picker" - }, - { - "path": "../../interaction/commands" - }, { "path": "../../llm/llm" }, diff --git a/packages/client/file-upload/README.i18n.yaml b/packages/client/file-upload/README.i18n.yaml index e6b4cb5fcb..4422bb94d6 100644 --- a/packages/client/file-upload/README.i18n.yaml +++ b/packages/client/file-upload/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/file-upload/README.md -README.md: 2c19e1a564d7b58226e6ccd3cb5b7c81e1b1c14e -README.zh.md: d1553d40d953dd6378a954c43a9b7a36b7334953 +README.md: e0c0d7637da0b426ebb5b4a0c6cb47d12784e0ed +README.zh.md: abdf965694f7ae62dd05a0192ffbc16e8678ec7d diff --git a/packages/client/file-upload/README.md b/packages/client/file-upload/README.md index 2c19e1a564..e0c0d7637d 100644 --- a/packages/client/file-upload/README.md +++ b/packages/client/file-upload/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package lets browser features store a `Blob`, exact bytes, or a `ReadableStream` for one Session and receive an opaque receipt for a later prompt. Served pages send Blob and stream bodies without aggregating their bytes on the page thread; pages whose Host runs in another execution context supply a Fetch-shaped carrier before Cordis boots. Callers can observe consumed bytes and cancel an active operation. A stream body is consumed once and transfers ownership when it crosses a Worker boundary. The standalone `?fixture` page uses the generated Remote for replayable Blob and exact-byte inputs. +This package lets browser features store a `Blob`, exact bytes, or a `ReadableStream` for one Session and receive an opaque receipt for a later prompt. Served pages send Blob and stream bodies without aggregating their bytes on the page thread; pages whose Host runs in another execution context supply a Fetch-shaped carrier before Cordis boots. Callers can observe consumed bytes and cancel an active operation. A stream body is consumed once and transfers ownership when it crosses a Worker boundary. Exact bytes use the generated Remote. ## Table of Contents @@ -32,7 +32,7 @@ Mount the package before a consumer that injects `fileUpload`, then call `ctx.fi name: '@deepseek-ai/dsh-client-file-upload' ``` -The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest inside a dedicated Worker so the service can report browser upload progress, including the total when the browser provides it. A `ReadableStream` transfers to that Worker and feeds Fetch incrementally; progress reports consumed bytes without a total. An `AbortSignal` terminates the dedicated Worker or reaches a page-owned carrier. Exact bytes and fixture Blob inputs use the generated Remote. +The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest inside a dedicated Worker so the service can report browser upload progress, including the total when the browser provides it. A `ReadableStream` transfers to that Worker and feeds Fetch incrementally; progress reports consumed bytes without a total. An `AbortSignal` terminates the dedicated Worker or reaches a page-owned carrier. Exact bytes use the generated Remote. ----- @@ -42,7 +42,7 @@ The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest ins

Implementation internals — click to expand -The Client plugin provides `ctx.fileUpload`. Its `upload()` method receives the owning Session identity, assembles the raw route request, and invokes the generated Remote fallback for replayable inputs. The provider reads the optional pre-Cordis `__DSH_FILE_UPLOAD__` hook once. Without a hook, each non-fixture raw request owns a short-lived Worker and releases it after completion, failure, or cancellation. With the hook, the service sends the body through the page-owned Fetch carrier; the Web Worker runtime transfers stream bodies through its request frame and exposes them to the Host HTTP bridge as backpressured chunks. +The Client plugin provides `ctx.fileUpload`. Its `upload()` method receives the owning Session identity, assembles the raw route request, and invokes the generated Remote for exact bytes. The provider reads the optional pre-Cordis `__DSH_FILE_UPLOAD__` hook once. Without a hook, each Blob or stream request owns a short-lived Worker and releases it after completion, failure, or cancellation. With the hook, the service sends the body through the page-owned Fetch carrier; the Web Worker runtime transfers stream bodies through its request frame and exposes them to the Host HTTP bridge as backpressured chunks. The Host plugin provides `ctx.fileUploads`. It owns the authenticated streaming route, encoded Remote fallback, command receipt resolver, and staged-receipt lifecycle; encoded admission, attachment-error recognition, and byte storage stay behind `ctx.attachments`. Receipt tables use the receiving Agent's Session object as their key. The Session Controller registers the resolver that can resume a cold ordinary Agent and consumes receipts during prompt admission. Prompt delivery holds each receipt binding in a disposable transaction: disposal restores the previous binding until successful delivery commits it, and queue or history observation then retires the committed receipt. diff --git a/packages/client/file-upload/README.zh.md b/packages/client/file-upload/README.zh.md index d1553d40d9..abdf965694 100644 --- a/packages/client/file-upload/README.zh.md +++ b/packages/client/file-upload/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包让浏览器功能为一个 Session 存储 `Blob`、精确字节或 `ReadableStream`,并取得供后续 prompt 使用的不透明凭证。普通服务页面发送 Blob 和 stream 请求体时,不会在页面线程聚合全部字节;Host 位于其他执行上下文中的页面会在 Cordis 启动前提供 Fetch 形式的载体。调用方可以观察已消费字节并取消活动操作。stream 请求体只能消费一次,跨 Worker 边界时会转移所有权。独立的 `?fixture` 页面通过生成的 Remote 处理可重放的 Blob 与精确字节输入。 +本包让浏览器功能为一个 Session 存储 `Blob`、精确字节或 `ReadableStream`,并取得供后续 prompt 使用的不透明凭证。普通服务页面发送 Blob 和 stream 请求体时,不会在页面线程聚合全部字节;Host 位于其他执行上下文中的页面会在 Cordis 启动前提供 Fetch 形式的载体。调用方可以观察已消费字节并取消活动操作。stream 请求体只能消费一次,跨 Worker 边界时会转移所有权。精确字节使用生成的 Remote。 ## 目录 @@ -32,7 +32,7 @@ kind: "package-reference" name: '@deepseek-ai/dsh-client-file-upload' ``` -本包没有 Cordis 配置字段。`Blob` 在专用 Worker 内通过 XMLHttpRequest 发送,因此服务可以报告浏览器上传进度,并在浏览器提供总量时一并报告。`ReadableStream` 会转移给该 Worker,再增量传入 Fetch;进度只报告已消费字节,不包含总量。`AbortSignal` 会终止专用 Worker,或传递给页面自己提供的载体。精确字节与 fixture Blob 输入使用生成的 Remote。 +本包没有 Cordis 配置字段。`Blob` 在专用 Worker 内通过 XMLHttpRequest 发送,因此服务可以报告浏览器上传进度,并在浏览器提供总量时一并报告。`ReadableStream` 会转移给该 Worker,再增量传入 Fetch;进度只报告已消费字节,不包含总量。`AbortSignal` 会终止专用 Worker,或传递给页面自己提供的载体。精确字节使用生成的 Remote。 ----- @@ -42,7 +42,7 @@ kind: "package-reference"
实现细节——点击展开 -Client 插件提供 `ctx.fileUpload`。其 `upload()` 方法接收所属 Session 标识,组装原始路由请求,并为可重放输入调用生成的 Remote 兜底。提供方只读取一次可选的 Cordis 启动前 `__DSH_FILE_UPLOAD__` 钩子。没有该钩子时,每个非 fixture 原始请求拥有一个短期 Worker,并在完成、失败或取消后释放。存在该钩子时,服务通过页面自己提供的 Fetch 载体发送请求体;Web Worker runtime 会通过请求帧转移 stream 请求体,再以带背压的分片形式交给 Host HTTP bridge。 +Client 插件提供 `ctx.fileUpload`。其 `upload()` 方法接收所属 Session 标识,组装原始路由请求,并为精确字节调用生成的 Remote。提供方只读取一次可选的 Cordis 启动前 `__DSH_FILE_UPLOAD__` 钩子。没有该钩子时,每个 Blob 或 stream 请求拥有一个短期 Worker,并在完成、失败或取消后释放。存在该钩子时,服务通过页面自己提供的 Fetch 载体发送请求体;Web Worker runtime 会通过请求帧转移 stream 请求体,再以带背压的分片形式交给 Host HTTP bridge。 Host 插件提供 `ctx.fileUploads`。它拥有经过认证的流式路由、编码 Remote 兜底、命令凭证解析器与暂存凭证生命周期;编码准入、附件错误识别与字节存储仍由 `ctx.attachments` 提供。凭证表以接收方 Agent 的 Session 对象为键。Session Controller 注册可恢复休眠普通 Agent 的解析器,并在 prompt 准入时消费凭证。Prompt 投递通过可释放事务持有每个凭证绑定。成功投递提交事务前,释放会恢复原绑定;提交后,队列或历史观察会退休该凭证。 diff --git a/packages/client/file-upload/src/client/contract.ts b/packages/client/file-upload/src/client/contract.ts index 998a71b2c3..dfe2245ff2 100644 --- a/packages/client/file-upload/src/client/contract.ts +++ b/packages/client/file-upload/src/client/contract.ts @@ -17,7 +17,7 @@ export interface FileUploadService { readonly available: boolean /** * Store one file for a Session. Blob and stream bodies use - * the background carrier; exact bytes and fixture fallbacks use Remote. + * the background carrier; exact bytes use Remote. * @param sessionId - Session that owns the staged receipt. * @param data - browser Blob, exact bytes, or a one-shot byte stream. * @param name - optional display name. diff --git a/packages/client/file-upload/src/client/runtime.ts b/packages/client/file-upload/src/client/runtime.ts index a54e31121c..90076cbedd 100644 --- a/packages/client/file-upload/src/client/runtime.ts +++ b/packages/client/file-upload/src/client/runtime.ts @@ -160,14 +160,13 @@ interface FileUploadTransport { /** Cordis service that owns one background carrier per upload operation. */ export class FileUploadRuntime extends Service implements FileUploadService { - readonly available: boolean + readonly available = true private readonly transport: FileUploadTransport /** @param ctx - providing Client context. */ constructor(ctx: Context) { super(ctx, 'fileUpload') const hook = (globalThis as ClientFileUploadGlobal).__DSH_FILE_UPLOAD__ - this.available = hook !== undefined || !isFixturePage() this.transport = hook === undefined ? workerTransport() : customTransport(hook.fetch) } @@ -177,7 +176,6 @@ export class FileUploadRuntime extends Service implements FileUploadService { * @returns the response status and text body. */ post(request: FileUploadRequest): Promise { - if (!this.available) return Promise.reject(new Error('background upload is unavailable in fixture mode')) return this.transport.post(request) } @@ -197,7 +195,7 @@ export class FileUploadRuntime extends Service implements FileUploadService { signal?: AbortSignal, onProgress?: (progress: { readonly loaded: number; readonly total?: number }) => void, ): Promise> { - if (!(data instanceof Uint8Array) && this.available) { + if (!(data instanceof Uint8Array)) { const query = new URLSearchParams({ sessionId }) if (name !== undefined) query.set('name', name) const response = await this.post({ @@ -212,14 +210,10 @@ export class FileUploadRuntime extends Service implements FileUploadService { } return parseFileUploadResult(response.body) } - if (!(data instanceof Uint8Array) && !(data instanceof Blob)) { - throw new Error('stream file upload requires a background carrier') - } - const bytes = data instanceof Uint8Array ? data : new Uint8Array(await data.arrayBuffer()) return (this.ctx as FileUploadRemoteContext).remote.fileUploads.upload( sessionId, { - data: bytesToBase64(bytes), + data: bytesToBase64(data), ...(name === undefined ? {} : { name }), }, signal, @@ -311,13 +305,6 @@ function resolveUrl(path: string): URL { return new URL(path, origin === undefined || origin === 'null' ? 'http://dsh.internal' : origin) } -function isFixturePage(): boolean { - const pageLocation = Reflect.get(globalThis, 'location') as unknown - return typeof pageLocation === 'object' && pageLocation !== null - && 'search' in pageLocation && typeof pageLocation.search === 'string' - && new URLSearchParams(pageLocation.search).has('fixture') -} - function parseFileUploadResult(body: string): RemoteResult { const value = JSON.parse(body) as unknown if (!isRecord(value) || typeof value.ok !== 'boolean') { diff --git a/packages/client/file-upload/tests/file-upload.client.spec.ts b/packages/client/file-upload/tests/file-upload.client.spec.ts index 66b27b102a..c6e28d54c0 100644 --- a/packages/client/file-upload/tests/file-upload.client.spec.ts +++ b/packages/client/file-upload/tests/file-upload.client.spec.ts @@ -252,17 +252,6 @@ describe('file upload service', () => { await fiber.dispose() }) - it('leaves the fixture on its generated Remote fallback', async () => { - vi.stubGlobal('location', { origin: 'https://fixture.test', search: '?fixture' }) - const ctx = new Context() - const fiber = ctx.plugin(FileUploadRuntime) - await fiber - expect(ctx.fileUpload.available).toBe(false) - await expect((ctx.fileUpload as FileUploadRuntime).post({ path: '/upload', body: new Blob() })) - .rejects.toThrow('background upload is unavailable in fixture mode') - await fiber.dispose() - }) - it('fails loud when a served browser has no Worker implementation', async () => { vi.stubGlobal('Worker', undefined) const ctx = new Context() @@ -438,8 +427,7 @@ describe('Session-addressed file upload', () => { await fiber.dispose() }) - it('uses the direct Remote fallback for exact bytes and fixture Blob bodies', async () => { - vi.stubGlobal('location', { origin: 'https://fixture.test', search: '?fixture' }) + it('uses the direct Remote fallback for exact bytes', async () => { const remote = vi.fn(() => Promise.resolve({ ok: true, value: { @@ -450,23 +438,13 @@ describe('Session-addressed file upload', () => { const { fiber, service } = await scopedService({ remote }) await expect(service.upload(SESSION_ID, Uint8Array.of(0, 0, 0), 'bytes.bin')) .resolves.toMatchObject({ ok: true }) - await expect(service.upload(SESSION_ID, new Blob([Uint8Array.of(1)]))) - .resolves.toMatchObject({ ok: true }) expect(remote.mock.calls).toEqual([ [SESSION_ID, { data: 'AAAA', name: 'bytes.bin' }, undefined], - [SESSION_ID, { data: 'AQ==' }, undefined], ]) await fiber.dispose() }) - it('rejects an unavailable stream and malformed background results', async () => { - vi.stubGlobal('location', { origin: 'https://fixture.test', search: '?fixture' }) - const fixture = await scopedService() - const stream = new ReadableStream({ start(controller) { controller.close() } }) - await expect(fixture.service.upload(SESSION_ID, stream)) - .rejects.toThrow('stream file upload requires a background carrier') - await fixture.fiber.dispose() - + it('rejects malformed background results', async () => { vi.stubGlobal('location', { origin: 'https://preview.test' }) ;(globalThis as UploadGlobal).__DSH_FILE_UPLOAD__ = { fetch: () => Promise.resolve(new Response(null, { status: 413 })), diff --git a/packages/host/frontend-static/tests/frontend-static.spec.ts b/packages/host/frontend-static/tests/frontend-static.spec.ts index 26034e7d6f..2a38750c06 100644 --- a/packages/host/frontend-static/tests/frontend-static.spec.ts +++ b/packages/host/frontend-static/tests/frontend-static.spec.ts @@ -141,7 +141,7 @@ describe('real Loader composition', () => { // Only the root and index path render index.html through registered taps. const untap = server.tapIndex(html => html.replace('', '')) - for (const path of ['/', '/index.html', '/?fixture']) { + for (const path of ['/', '/index.html', '/?view=test']) { const got = await request(port, path, authenticated()) expect(got.status).toBe(200) expect(got.type).toBe('text/html; charset=utf-8') diff --git a/packages/session-query/session-query-sqlite/src/index.ts b/packages/session-query/session-query-sqlite/src/index.ts index e0b24b3c39..7c95afa28d 100644 --- a/packages/session-query/session-query-sqlite/src/index.ts +++ b/packages/session-query/session-query-sqlite/src/index.ts @@ -662,8 +662,6 @@ export class SqliteSessionQueryEngine extends SessionQueryEngine { offset, ] assertPortableBindingCount(bindings.length) - // The browser fixture mirrors these rank keys in - // `packages/client/connection/src/client/fixture.ts`; update both together. return this._requireDb().prepare(` ${selected.sql}, filtered AS ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a5e7b7eb..037ce8b38b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -604,6 +604,9 @@ importers: '@deepseek-ai/dsh-client-store': specifier: workspace:^ version: link:../../packages/client/store + '@deepseek-ai/dsh-client-test-runtime': + specifier: workspace:^ + version: link:../../packages/test-support/client-runtime '@deepseek-ai/dsh-client-ui-dockkit': specifier: workspace:^ version: link:../../packages/client/ui-dockkit @@ -634,6 +637,9 @@ importers: '@deepseek-ai/dsh-pwsh-local': specifier: workspace:^ version: link:../../packages/shell/pwsh-local + '@deepseek-ai/dsh-remote-mock': + specifier: workspace:^ + version: link:../../packages/test-support/remote-mock '@types/node': specifier: ^22.0.0 version: 22.20.0 @@ -878,6 +884,9 @@ importers: '@deepseek-ai/dsh-host-webserver': specifier: workspace:^ version: link:../../host/webserver + '@deepseek-ai/dsh-remote-mock': + specifier: workspace:^ + version: link:../../test-support/remote-mock '@deepseek-ai/dsh-typert-registry': specifier: workspace:^ version: link:../../typert/registry @@ -2056,21 +2065,12 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis - '@deepseek-ai/dsh-api-session-controller': - specifier: workspace:^ - version: link:../../api/session-controller '@deepseek-ai/dsh-attachment': specifier: workspace:^ version: link:../../attachment/attachment '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand - '@deepseek-ai/dsh-commands': - specifier: workspace:^ - version: link:../../interaction/commands - '@deepseek-ai/dsh-host-directory-picker': - specifier: workspace:^ - version: link:../../host/directory-picker '@deepseek-ai/dsh-host-webserver': specifier: workspace:^ version: link:../../host/webserver @@ -2080,15 +2080,6 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session - '@deepseek-ai/dsh-settings': - specifier: workspace:^ - version: link:../../settings/settings - '@deepseek-ai/dsh-tool-todo': - specifier: workspace:^ - version: link:../../todo/tool-todo - '@deepseek-ai/dsh-util-values': - specifier: workspace:^ - version: link:../../util/values packages/client/file-upload: dependencies: diff --git a/vitest.config.ts b/vitest.config.ts index 20c6e149ad..cd6752424c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -305,7 +305,6 @@ export default defineConfig({ 'packages/experimental/client-ui-agent-team/src/client/index.ts', // Slash/command/input round: per-file gaps deferred with the same // client-lane debt. TODO(gui): cover and remove with the lane above. - 'packages/client/connection/src/client/fixture.ts', 'packages/client/ui-commands/src/index.ts', 'packages/client/ui-skill/src/index.ts', 'packages/client/ui-input-trigger/src/index.ts',