test(llm-deepseek): cover all-deferred requests and separate tool updates

This commit is contained in:
Yixiang Tu committed 2026-11-02 20:30:57 +08:00
1 parent 9d53909bcf
commit 523e741a31
8 files changed
+128 -10

No files matched your search

+16 -10
View File
@@ -1,6 +1,6 @@
import * as Protocol from '@deepseek-ai/dsh-llm-deepseek'
/**
* Real Messages round trips use the official root and require credentials.
* Real Messages round trips require credentials; tool replacement also accepts DEEPSEEK_BASE_URL.
* System-update checks additionally require DEEPSEEK_IN_HISTORY_MODEL.
*/
import { randomUUID } from 'node:crypto'
@@ -33,7 +33,7 @@ afterEach(async () => {
vi.unstubAllEnvs()
vi.unstubAllGlobals()
})
async function boot(models?: Messages.Options['models']) {
async function boot(models?: Messages.Options['models'], baseURL = Protocol.PUBLIC_BASE_URL) {
const home = await mkdtemp(join(tmpdir(), 'dsh-messages-e2e-'))
cleanups.push(() => rm(home, { recursive: true, force: true }))
vi.stubEnv('DSH_HOME', home)
@@ -41,7 +41,7 @@ async function boot(models?: Messages.Options['models']) {
cleanups.push(() => ctx.fiber.dispose())
await ctx.plugin(LlmRuntime)
await ctx.plugin(Messages, {
baseURL: Protocol.PUBLIC_BASE_URL,
baseURL,
maxTokens: 4096,
...models === undefined ? {} : { models },
})
@@ -50,17 +50,23 @@ async function boot(models?: Messages.Options['models']) {
const tool = { name: 'lookup_value', description: 'Read the requested value. Always call this tool to obtain a value.', parameters: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] } }
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('DeepSeek Messages real API', () => {
it('calls a replacement tool after every baseline tool has been removed', async () => {
it.each([false, true])('calls a replacement tool after every baseline tool has been removed, separate updates=%s', async (separate) => {
const model = 'deepseek-flash'
const ctx = await boot([{ id: model, toolUpdate: 'addition-only' }])
const update = createDeveloperMessage({ source: { kind: 'tool-registry' }, content: [
{ type: 'tool-removal', toolName: 'old_lookup' }, { type: 'tool-addition', toolName: tool.name },
] })
const ctx = await boot([{ id: model, toolUpdate: 'addition-only' }], process.env.DEEPSEEK_BASE_URL ?? Protocol.PUBLIC_BASE_URL)
const updates = [createDeveloperMessage({ source: { kind: 'tool-registry' }, content: [
{ type: 'tool-removal', toolName: 'old_lookup' },
...separate ? [] : [{ type: 'tool-addition' as const, toolName: tool.name }],
] })]
if (separate) updates.push(createDeveloperMessage({ source: { kind: 'tool-registry' }, content: [
{ type: 'tool-addition', toolName: tool.name },
] }))
const response = await assemble(ctx.llm.stream(options({
model, reasoningEffort: ReasoningEffortId('high'),
messages: [user('Call lookup_value with key "answer" to obtain the value. Do not guess.'), update],
messages: [user('Call lookup_value with key "answer" to obtain the value. Do not guess.'), ...updates],
tools: [tool],
toolHistory: { tools: [{ ...tool, name: 'old_lookup' }], updates: [{ messageId: update.id, additions: [tool] }] },
toolHistory: { tools: [{ ...tool, name: 'old_lookup' }], updates: updates.map(update => ({
messageId: update.id, additions: update.content.some(block => block.type === 'tool-addition') ? [tool] : [],
})) },
})), model)
expect(response.assembler.finish).toEqual({ kind: 'tool-calls' })
expect(response.message.content.filter(block => block.type === 'tool-call').map(call => call.name)).toEqual([tool.name])
@@ -0,0 +1,38 @@
/** Replay explicitly deferred declarations without activating them. */
import assert from 'node:assert/strict'
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-agent'
import { projectToolUpdates } from '@deepseek-ai/dsh-llm'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit'
import { resolveAdapterOptions } from '../../src/config.ts'
import { serialize } from '../../src/serialize.ts'
export const name = 'messages-all-deferred-tools-snapshot'
export const inject = ['tools', 'llm']
/**
* Keep one real tool deferred and inspect the derived Messages request.
* @param ctx - Snapshot composition context.
*/
export function apply(ctx: Context): void {
ctx.on('agent/created', ({ agent }) => {
agent.ctx.tools.restrict({ allow: [] })
agent.ctx.tools.register(defineContentToolFixture({
name: 'snapshot_pending', description: 'Wait for activation.', parameters: {}, deferLoading: true,
async execute() { assert.fail('An unactivated tool must not be called') },
}))
})
const connection = resolveAdapterOptions({})
ctx.on('llm/stream', (options, next) => {
const projected = projectToolUpdates(options.messages, options.tools, 'addition-only', options.toolHistory)
assert.ok(projected.tools)
assert.deepEqual(projected.tools, options.tools)
const request = serialize({ ...options, tools: [...projected.tools] }, connection, projected.messages, new Map(), () => undefined)
assert.deepEqual(request.tools, [
{ name: 'DeferredToolPlaceholder', description: 'Reserved placeholder that keeps deferred tool loading active; never call this tool.', input_schema: { type: 'object', properties: {} } },
{ name: 'snapshot_pending', description: 'Wait for activation.', input_schema: { type: 'object', properties: {} }, defer_loading: true },
])
assert.deepEqual(request.messages.filter(message => message.role === 'system'), [])
return next()
})
}
@@ -0,0 +1,18 @@
- id: llm-deepseek
disabled: true
- insert:
- id: llm-replay
name: '@deepseek-ai/dsh-llm-replay'
config:
providers:
- id: deepseek-official
name: DeepSeek
models:
- id: deepseek-v4-flash
toolUpdate: addition-only
defaultMaxTokens: 256000
reasoningEfforts: [off, low, high, max]
defaultReasoningEffort: high
- id: messages-all-deferred-tools-snapshot
name: '../../../packages/llm/llm-deepseek/tests/fixtures/all-deferred-tools.ts'
@@ -0,0 +1,3 @@
- insert:
- id: messages-all-deferred-tools-snapshot
name: '../../../packages/llm/llm-deepseek/tests/fixtures/all-deferred-tools.ts'
@@ -0,0 +1,17 @@
{"type":"session","version":4,"id":"{{session:1}}","createdAt":0,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
{"type":"approval/policy","data":{"policy":"never"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply DONE without calling any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply DONE without calling any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Reply DONE without calling any","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"stream":[{"type":"chunk","time":1792661643324,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792661643324,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1792661643324,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1792661643324,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -0,0 +1,8 @@
version: 1
scenario: deepseek-all-deferred-tools
profile: headless
composition: deepseek-all-deferred-tools
recording: authored
header:
class: deepseek-all-deferred-tools
pin: true
@@ -0,0 +1,14 @@
You are an AI agent powered by DeepSeek Harness.
You are a coding assistant powered by the deepseek-v4-flash model. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug.
Verify your work by running the code or tests. Keep answers brief and factual.
Check the [exit code: N] marker on every bash result; investigate failures before moving on.
Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.
create_goal may infer goal intent from a direct human request in any language. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked.
Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.
@@ -0,0 +1,14 @@
{
"initial": [
{
"name": "snapshot_pending",
"description": "Wait for activation.",
"parameters": {
"type": "object",
"properties": {}
},
"deferLoading": true
}
],
"changes": []
}