diff --git a/packages/cordis/tool-cordis/tests/mount.spec.ts b/packages/cordis/tool-cordis/tests/mount.spec.ts index 6bc2d5970d..a46f37a721 100644 --- a/packages/cordis/tool-cordis/tests/mount.spec.ts +++ b/packages/cordis/tool-cordis/tests/mount.spec.ts @@ -433,6 +433,7 @@ describe('cordis_mount', () => { ['__proto__']: { type: 'string', required: true }, value: { type: 'json', default: { ['__proto__']: { safe: true } } }, }, + ${CONTENT_OUTPUT_CODE} async execute() { return [] }, })) }, diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts index 65c2258d0b..eb1a456c7a 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -274,6 +274,6 @@ describe('structured tool error propagation (the runtime-validation Agent Note, const toolResult = agent.session.events.find(e => e.type === 'tool/result') expect(toolResult?.type === 'tool/result' && toolResult.data.isError).toBe(true) expect(toolResult?.type === 'tool/result' && toolResult.data.error) - .toEqual({ message: 'exploded', info: { name: 'HarnessError', code: 'BOOM' } }) + .toEqual({ name: 'HarnessError', code: 'BOOM' }) }) }) diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index aa3a35bc36..3c89eda70c 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -253,7 +253,7 @@ describe('agent loop', () => { ['BigInt', { n: 1n }], ['Map', new Map([['key', 'value']])], ['class instance', new (class ResultMeta { x = 1 })()], - ])('normalizes non-JSON presentation metadata (%s) before the durable result commit', async (_kind, meta) => { + ])('rejects non-JSON presentation metadata (%s) before the durable result commit', async (_kind, meta) => { const adapter = new MockAdapter([ toolCallResponse('bad-meta-call', 'bad-meta', {}, 'calling'), textResponse('recovered'), @@ -281,15 +281,16 @@ describe('agent loop', () => { expect(result.data.callId).toBe('bad-meta-call') expect(result.data.isError).toBe(true) expect(result.data.meta).toBeUndefined() + expect(result.data.error).toEqual({ name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' }) expect(result.data.content).toEqual([{ type: 'text', - text: 'Error: tool result must be losslessly JSON-serializable', + text: 'Error: tool "bad-meta" returned invalid output: output.presentationMeta returned non-lossless JSON', }]) } // The normalized failure was durably logged and fed back to the model; the // turn continued normally instead of failing after an apparent success. expect(adapter.requests).toHaveLength(2) - expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('losslessly JSON-serializable') + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('output.presentationMeta returned non-lossless JSON') }) it('omits the system field when a system-prompt/assemble veto empties the assembly', async () => { diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index d237430dda..874500ed3f 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { Context } from 'cordis' -import { CallId, HarnessError } from '@deepseek-ai/dsh-llm' +import { CallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import type { Agent } from '@deepseek-ai/dsh-agent' import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval' @@ -217,6 +217,35 @@ describe('ToolRegistry', () => { expect('value' in result).toBe(false) }) + it.each(['render', 'presentationMeta'] as const)('contains a throwing output.%s snapshot as one failed call', async (projector) => { + const ctx = await setup() + const hostile = Object.defineProperty({}, 'value', { + enumerable: true, + get: () => { throw new Error('snapshot getter exploded') }, + }) + ctx.tools.register(defineTool({ + name: `hostile-${projector}`, + description: projector, + parameters: {}, + output: { + schema: { type: 'string' }, + render: () => projector === 'render' + ? hostile as unknown as ContentBlock[] + : [{ type: 'text', text: 'ok' }], + presentationMeta: () => projector === 'presentationMeta' + ? hostile as unknown as JsonValue + : null, + }, + execute: async () => 'ok', + })) + + const result = await ctx.tools.execute({ + callId: CallId(`hostile-${projector}`), name: `hostile-${projector}`, arguments: {}, + }) + expect(result.error?.message).toContain('snapshot getter exploded') + expect(result.error?.info).toEqual({ name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' }) + }) + it('keeps value/meta through content replacement and recomputes both projections after value replacement', async () => { const ctx = await setup() ctx.tools.register(defineTool({ diff --git a/packages/fs/tool-fs/src/read-render.ts b/packages/fs/tool-fs/src/read-render.ts index e30dad7bcd..943ff98f61 100644 --- a/packages/fs/tool-fs/src/read-render.ts +++ b/packages/fs/tool-fs/src/read-render.ts @@ -77,7 +77,6 @@ function lineByteSize(line: string, currentLineCount: number): number { function consumeLine(acc: WindowAccumulator, rawLine: string, request: ReadWindow): void { acc.totalLines += 1 - if (acc.done) return if (acc.totalLines < request.offset || acc.lines.length >= request.limit) return const text = truncateLine(rawLine, request.maxLineLength) diff --git a/packages/mcp/mcp-client/tests/apply.spec.ts b/packages/mcp/mcp-client/tests/apply.spec.ts index 4b43411346..e36e091478 100644 --- a/packages/mcp/mcp-client/tests/apply.spec.ts +++ b/packages/mcp/mcp-client/tests/apply.spec.ts @@ -15,14 +15,26 @@ import type { Config } from '@deepseek-ai/dsh-mcp-client' const { mockConnect, mockClose, mockListTools, mockCallTool, mockSetNotificationHandler, MockClient } = vi.hoisted(() => { const mockConnect = vi.fn<() => Promise>() const mockClose = vi.fn<() => Promise>() - const mockListTools = vi.fn() - const mockCallTool = vi.fn() + const mockListTools = vi.fn<(_params?: Record) => Promise>() + const mockCallTool = vi.fn<( + _params?: Record, _compatibilitySchema?: unknown, _options?: unknown, + ) => Promise>() const mockSetNotificationHandler = vi.fn() + const mockRequest = vi.fn(async ( + request: { method: string; params?: Record }, + _schema: unknown, + options?: unknown, + ): Promise => { + if (request.method === 'tools/list') return await mockListTools(request.params) + if (request.method === 'tools/call') return await mockCallTool(request.params, undefined, options) + throw new Error(`unexpected MCP request: ${request.method}`) + }) class MockClient { connect = mockConnect close = mockClose listTools = mockListTools callTool = mockCallTool + request = mockRequest setNotificationHandler = mockSetNotificationHandler } return { mockConnect, mockClose, mockListTools, mockCallTool, mockSetNotificationHandler, MockClient }