Files
waggle-os/packages/agent/tests/workflow-tools-harness.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

247 lines
8.0 KiB
TypeScript

/**
* Tests for the `run_harness` agent tool — specifically the session
* scoping refactor that replaced the process-global harness_id keyed
* Map with explicit run_id handles.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
createWorkflowTools,
__resetActiveHarnessRunsForTests,
} from '../src/workflow-tools.js';
import type { ToolDefinition } from '../src/tools.js';
function makeConfig(): Parameters<typeof createWorkflowTools>[0] {
return {
availableTools: [],
// The run_harness tool does not spin up a sub-agent — these fields
// are required for the factory signature but unused in these tests.
maxDepth: 0,
maxWorkers: 0,
aiClient: { generateText: async () => ({ text: '', tokens: { input: 0, output: 0 } }) },
workerFactory: async () => ({
async run() {
return {
content: '',
toolsUsed: [],
usage: { inputTokens: 0, outputTokens: 0 },
};
},
}),
} as unknown as Parameters<typeof createWorkflowTools>[0];
}
function findTool(tools: ToolDefinition[], name: string): ToolDefinition {
const t = tools.find(x => x.name === name);
if (!t) throw new Error(`tool "${name}" not found`);
return t;
}
// ── Fixtures ────────────────────────────────────────────────────
describe('run_harness tool — session scoping', () => {
beforeEach(() => {
__resetActiveHarnessRunsForTests();
});
it('first call returns a run_id and the first phase instruction', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const result = await run.execute({ harness_id: 'research-verify' }) as string;
expect(result).toContain('Harness Started: Research & Verify');
expect(result).toMatch(/\*\*run_id:\*\*\s*`research-verify-/);
expect(result).toContain('Gather');
});
it('subsequent call without run_id starts a BRAND NEW run instead of resuming', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const first = await run.execute({ harness_id: 'research-verify' }) as string;
const runId1 = extractRunId(first);
const second = await run.execute({ harness_id: 'research-verify' }) as string;
const runId2 = extractRunId(second);
expect(runId1).not.toBe(runId2);
});
it('parallel sessions with same harness_id but different run_ids do not collide', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const aStart = await run.execute({ harness_id: 'research-verify' }) as string;
const bStart = await run.execute({ harness_id: 'research-verify' }) as string;
const aRun = extractRunId(aStart);
const bRun = extractRunId(bStart);
expect(aRun).not.toBe(bRun);
// Advance only run A with a phase-satisfying output.
const aAdvanced = await run.execute({
harness_id: 'research-verify',
run_id: aRun,
phase_output: {
content: 'I searched and found two results.',
tool_calls: [
{ tool: 'search_memory', args: { q: 'x' }, result: 'a' },
{ tool: 'recall_memory', args: { q: 'y' }, result: 'b' },
],
artifacts: [],
},
}) as string;
// Run A should have progressed to Synthesize (phase 2).
expect(aAdvanced).toContain('Synthesize');
// Run B should still be at Gather (phase 1) — advancing A did not bleed
// into B's state.
const bPeek = await run.execute({
harness_id: 'research-verify',
run_id: bRun,
}) as string;
expect(bPeek).toContain('Gather');
});
it('unknown run_id returns an error message', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const result = await run.execute({
harness_id: 'research-verify',
run_id: 'nonexistent-id',
}) as string;
expect(result).toMatch(/Unknown run_id/i);
});
it('run_id from a different harness is rejected', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const start = await run.execute({ harness_id: 'research-verify' }) as string;
const runId = extractRunId(start);
const result = await run.execute({
harness_id: 'code-review-fix',
run_id: runId,
}) as string;
expect(result).toMatch(/Unknown run_id/i);
});
it('captures real duration_ms and tokens when provided in phase_output', async () => {
// Use research-verify; the Verify phase is auto-skipped because
// WAGGLE_AUTO_VERIFY is not set, so the run summary fires after
// the Synthesize phase passes its gates.
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const start = await run.execute({ harness_id: 'research-verify' }) as string;
const runId = extractRunId(start);
// Phase 1: Gather — feed in real tokens + duration.
await run.execute({
harness_id: 'research-verify',
run_id: runId,
phase_output: {
content: 'results',
tool_calls: [
{ tool: 'search_memory', args: { q: 'x' }, result: 'a' },
{ tool: 'recall_memory', args: { q: 'y' }, result: 'b' },
],
artifacts: [],
duration_ms: 2500,
tokens: { input: 100, output: 50 },
},
});
// Phase 2: Synthesize — this satisfies the 3-sections gate and
// triggers the auto-skip of the Verify phase, completing the run.
const finalOutput = await run.execute({
harness_id: 'research-verify',
run_id: runId,
phase_output: {
content: '## Section 1\n## Section 2\n## Section 3',
tool_calls: [],
artifacts: [],
duration_ms: 3000,
tokens: { input: 200, output: 120 },
},
}) as string;
// Summary includes accumulated token totals — 100+200 in, 50+120 out.
expect(finalOutput).toContain('Tokens:');
expect(finalOutput).toContain('300 input');
expect(finalOutput).toContain('170 output');
});
it('unknown harness_id returns a list of available harnesses', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const result = await run.execute({ harness_id: 'does-not-exist' }) as string;
expect(result).toContain('not found');
expect(result).toContain('research-verify');
expect(result).toContain('code-review-fix');
expect(result).toContain('document-draft');
});
it('advancing past the final phase deletes the tracked run (cleanup)', async () => {
const tools = createWorkflowTools(makeConfig());
const run = findTool(tools, 'run_harness');
const start = await run.execute({ harness_id: 'research-verify' }) as string;
const runId = extractRunId(start);
// Drive all three phases.
await run.execute({
harness_id: 'research-verify',
run_id: runId,
phase_output: {
content: 'ok',
tool_calls: [
{ tool: 'search_memory', args: {}, result: 'a' },
{ tool: 'recall_memory', args: {}, result: 'b' },
],
artifacts: [],
},
});
await run.execute({
harness_id: 'research-verify',
run_id: runId,
phase_output: {
content: '## A\n## B\n## C',
tool_calls: [],
artifacts: [],
},
});
await run.execute({
harness_id: 'research-verify',
run_id: runId,
phase_output: {
content: 'VERDICT: PASS',
tool_calls: [],
artifacts: [],
},
});
// Run should be gone. Attempting to reuse the runId errors cleanly.
const reuse = await run.execute({
harness_id: 'research-verify',
run_id: runId,
}) as string;
expect(reuse).toMatch(/Unknown run_id/i);
});
});
function extractRunId(output: string): string {
const m = output.match(/\*\*run_id:\*\*\s*`([^`]+)`/);
if (!m) throw new Error(`could not find run_id in:\n${output}`);
return m[1];
}