workflow: harden the seam-contract tests ahead of the engine swap

Three engine-agnostic pins, landed BEFORE the worker-thread port so the
port commit demonstrates contract preservation against them:

- tool-workflow: the tool:<toolName> prompt-section registration was
  entirely unasserted — assemble() now pins the section present under
  the CONFIGURED name and gone after fiber dispose (the packages
  AGENTS.md dispose-and-assert-cleanup rule; tool-bash is the template).
- tool-workflow: drop the dead `??` re-defaulting of already-
  schemastery-resolved config (the hidden-fallback shape AGENTS.md
  bans) and the direct-apply test that existed only to cover those
  branches; both engines' `config as ResolvedConfig` is the pattern.
- workflow-vm: workflow/end was asserted only on completed runs — the
  cancelled path and the grace force-settle path now pin the event and
  its stopReason/error/agentsStarted payload (an observer's only death
  signal on those paths).
This commit is contained in:
imccyu
2026-07-09 18:16:51 +08:00
parent 4a15c8a479
commit fbc9eb313c
3 changed files with 24 additions and 14 deletions
@@ -5,7 +5,7 @@ import { AgentId } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubagentCapabilities, SubagentProvider, SubagentResult, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import type { WorkflowResult, WorkflowRunInfo } from '@deepseek-ai/dsh-workflow'
import type { WorkflowResult, WorkflowResultInfo, WorkflowRunInfo } from '@deepseek-ai/dsh-workflow'
import * as vmEngineModule from '../src/index.ts'
import VmWorkflowEngine, { type Config } from '../src/index.ts'
@@ -534,6 +534,8 @@ describe('dsh-workflow-vm', () => {
it('cancel() aborts in-flight children and settles the run cancelled', async () => {
const { ctx, parent, provider } = await setup({ manual: true })
const ends: WorkflowResultInfo[] = []
ctx.on('workflow/end', (_info, result) => { ends.push(result) })
const handle = ctx.workflows.start({ script: script("return await agent('long job')"), parent })
await vi.waitFor(() => { expect(provider.runs.length).toBe(1) })
handle.cancel('user stopped it')
@@ -541,6 +543,9 @@ describe('dsh-workflow-vm', () => {
expect(result.stopReason).toBe('cancelled')
expect(result.error).toContain('user stopped it')
expect(provider.runs[0]!.disposed).toBe(true)
// workflow/end is an observer's only death signal: it fires for a
// cancelled run too, mirroring the settled outcome data.
expect(ends).toEqual([{ stopReason: 'cancelled', error: result.error, agentsStarted: 1 }])
await handle.dispose()
})
@@ -787,6 +792,8 @@ describe('dsh-workflow-vm', () => {
it('cancel() force-settles the result of a script parked on a promise no hook owns', async () => {
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: 30 } })
const ends: WorkflowResultInfo[] = []
ctx.on('workflow/end', (_info, result) => { ends.push(result) })
const handle = ctx.workflows.start({
// No hooks involved: an unsettleable await cancellation cannot reject
// — the abandon grace is the only thing that can settle this run.
@@ -797,6 +804,9 @@ describe('dsh-workflow-vm', () => {
const result = await handle.result
expect(result.stopReason).toBe('cancelled')
expect(result.error).toContain('user aborted')
// The grace force-settle fires workflow/end exactly like an ordinary
// settlement — an abandoned script's death still reaches observers.
expect(ends).toEqual([{ stopReason: 'cancelled', error: result.error, agentsStarted: 0 }])
await handle.dispose()
})