Files
deepseek-harness/packages/workflow/workflow-vm/tests/workflow.e2e.ts
T
Tianyi Cui 4641f1a851 chore: absorb the no-nudge seam into the workflow tree
The parent branch dropped structuredNudgeRetries; the two workflow-side test
setups stop passing it, and the RFC's foundation paragraph now describes the
current design (final-ASSEMBLY enforcement logged via request/header, the
post-capture pre-execute deny, the start() schema snapshot, no re-prompt)
instead of the retired agent/request + nudge shape.
2026-07-07 09:26:47 +08:00

132 lines
5.3 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn'
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
import { CallId } from '@deepseek-ai/dsh-llm'
import VmWorkflowEngine from '../src/index.ts'
/**
* With-key e2e for the workflow engine: a REAL script drives REAL spawn
* children against the live DeepSeek API — one plain child and one schema'd
* child through the real structured-output runtime — and the run's value,
* events, and child sessions are asserted from the outside (never the
* script's self-report alone). Key-gated (self-skips without
* DEEPSEEK_API_KEY).
*/
let ctx: Context | undefined
afterEach(async () => {
await ctx?.fiber.dispose()
ctx = undefined
})
async function harness(): Promise<Context> {
const built = new Context()
await built.plugin(LlmService)
await built.plugin(SessionStore)
await built.plugin(SystemPrompt)
await built.plugin(ToolRegistry)
await built.plugin(AgentRegistry)
await built.plugin(AgentLoop, { agents: [] })
await built.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
await built.plugin(SubagentService)
await built.plugin(Spawn, { providerName: 'spawn' })
await built.plugin(VmWorkflowEngine, { provider: 'spawn' })
await built.plugin(ToolWorkflow, {})
return built
}
const SCRIPT = `export const meta = {
name: 'e2e-arithmetic',
description: 'two real children: one prose, one structured',
phases: [{ title: 'Ask' }, { title: 'Judge' }],
}
phase('Ask')
log('asking the prose child')
const prose = await agent('Reply with exactly one short sentence: what is 2 + 2?')
phase('Judge')
const judged = await agent(
'Here is an answer to the question "what is 2+2": ' + prose
+ ' — report whether it contains the number 4 and your confidence between 0 and 1.',
{ schema: { type: 'object', properties: { containsFour: { type: 'boolean' }, confidence: { type: 'number' } }, required: ['containsFour'] } },
)
return { prose, containsFour: judged === null ? null : judged.containsFour }`
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workflow engine with-key e2e', () => {
it('runs a two-phase script over real children, one through the structured runtime', async () => {
ctx = await harness()
const parentHandle = ctx.agents.create({
agentId: AgentId('wf-e2e-parent'),
sessionId: 'wf-e2e-session' as never,
agentOptions: { model: 'deepseek-v4-flash' },
})
const events: string[] = []
const childIds: string[] = []
for (const name of ['workflow/start', 'workflow/phase', 'workflow/log', 'workflow/agent-start', 'workflow/agent-end', 'workflow/end'] as const) {
ctx.on(name, (...payload: unknown[]) => {
events.push(name)
if (name === 'workflow/agent-start') childIds.push((payload[1] as { childId: string }).childId)
})
}
const run = ctx.workflows.start({ script: SCRIPT, parent: parentHandle.agent })
const result = await run.result
await run.dispose()
expect(result.stopReason).toBe('completed')
expect(result.agentsStarted).toBe(2)
const value = result.value as { prose: string; containsFour: boolean | null }
// World checks: the prose child really answered (a real completion), and
// the structured child judged it against the REAL schema-forced tool.
expect(value.prose.length).toBeGreaterThan(0)
expect(value.containsFour).toBe(true)
expect(events[0]).toBe('workflow/start')
expect(events.at(-1)).toBe('workflow/end')
expect(events.filter(name => name === 'workflow/phase').length).toBe(2)
expect(events.filter(name => name === 'workflow/agent-start').length).toBe(2)
expect(childIds.length).toBe(2)
// The children were disposed to quiescence after collection.
for (const childId of childIds) {
expect(ctx.agents.get(AgentId(childId))).toBeUndefined()
}
await parentHandle.dispose()
}, 240_000)
it('the workflow TOOL runs the same path through the real registry pipeline', async () => {
ctx = await harness()
const parentHandle = ctx.agents.create({
agentId: AgentId('wf-e2e-tool-parent'),
sessionId: 'wf-e2e-tool-session' as never,
agentOptions: { model: 'deepseek-v4-flash' },
})
const result = await ctx.tools.execute({
callId: CallId('wf-e2e-call'),
name: 'workflow',
arguments: {
script: `export const meta = { name: 'e2e-tool', description: 'one real child via the tool' }
const answer = await agent('Reply with exactly one word: the capital of France.')
return { answer }`,
},
agent: parentHandle.agent,
})
expect(result.isError).toBe(false)
const text = (result.content[0] as { text: string }).text
expect(text).toContain('workflow "e2e-tool" completed (1 agent)')
expect(text.toLowerCase()).toContain('paris')
await parentHandle.dispose()
}, 240_000)
})