Files
msd-core/tests/claude-orchestration-command-router.test.cjs
Tom Boucher e3262d94d3 feat(capabilities): add claude-orchestration capability (Workflow backend) (#1143)
Default-off, BETA, claude-only capability adopting Claude Code's Workflow tool
(/effort ultracode, Agent SDK >= v0.3.149) as an optional parallel-execution
backend for the GSD loop. Restores the wave parallelism + plan-checker + verifier
that #853 forces inline on Claude Code, and folds gsd-ultraplan-phase under one
runtime gate.

- Pure fail-closed core (src/claude-orchestration.cts): detectWorkflowBackend
  (gate ladder: enabled -> Claude -> backend != inline -> nested+background host
  -> valid Agent SDK -> SDK >= floor; every miss degrades to inline) and
  emitWorkflowScript (waves -> parallel() barriers, plans -> gsd-executor +
  worktree, files_modified overlap -> separate stages, resumeFromRunId, budget).
  All interpolated identifiers validated script-safe; briefs JSON-quoted.
- claude-orchestration command family (gsd-tools claude-orchestration
  detect-backend|emit-workflow) for orchestrator invocation.
- Two gated loop contributions at wired points (execute:wave:post, plan:post);
  federated config keys (enabled/execution_backend/min_agent_sdk_version).
- ADR-1143 implementation amendment; CONTEXT.md glossary entry; explanation doc.

On any runtime lacking the Workflow tool, behaviour is byte-identical to today.

closes #1143
2026-07-06 15:18:23 -04:00

170 lines
6.3 KiB
JavaScript

'use strict';
/**
* claude-orchestration-command-router.test.cjs — end-to-end tests for the
* `gsd-tools claude-orchestration` command surface (#1143).
*
* Exercises the full dispatch path: gsd-tools → dispatchCapabilityCommand →
* routeClaudeOrchestrationCommand → the pure detect/emitter functions.
*
* NOTE: runGsdTools() returns { success, output, exitCode, error } — it does
* NOT throw on a non-zero exit (helpers.cjs). Tests assert `.success` and parse
* `.output`, and check `.error` on the failure path.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── Fixtures ─────────────────────────────────────────────────────────────────
const WAVES_MANIFEST = {
waves: [
{
id: 'w1',
plans: [
{ id: 'p1', brief: 'Implement the foo module', files_modified: ['src/foo.cts'] },
{ id: 'p2', brief: 'Wire the bar seam', files_modified: ['src/bar.cts'] },
],
},
],
};
function writeManifest(tmpDir) {
const manifestPath = path.join(tmpDir, 'waves.json');
fs.writeFileSync(manifestPath, JSON.stringify(WAVES_MANIFEST), 'utf8');
return manifestPath;
}
/** Run the command and assert it succeeded, returning the parsed JSON output. */
function runAndParse(args, cwd) {
const res = runGsdTools(args, cwd);
assert.strictEqual(res.success, true, 'command should succeed; stderr: ' + (res.error || ''));
assert.ok(res.output.length > 0, 'command should emit output');
return JSON.parse(res.output);
}
// ─── emit-workflow ────────────────────────────────────────────────────────────
describe('claude-orchestration emit-workflow (CLI)', () => {
test('emits a Workflow script with parallel barriers, gsd-executor + worktree, and resumeFromRunId', () => {
const tmp = createTempProject('claw-emit-');
try {
const manifestPath = writeManifest(tmp);
const parsed = runAndParse([
'claude-orchestration', 'emit-workflow',
'--waves', manifestPath,
'--run-id', 'run-cli-1143',
'--phase-dir', '.planning/phases/01-foo',
], tmp);
assert.ok(typeof parsed.script === 'string' && parsed.script.length > 0);
assert.ok(parsed.script.includes('parallel('), 'parallel() barrier emitted');
assert.ok(parsed.script.includes('gsd-executor'), 'gsd-executor agentType');
assert.ok(parsed.script.includes('worktree'), 'worktree isolation');
assert.ok(parsed.script.includes('resumeFromRunId'), 'resumeFromRunId wired');
assert.ok(parsed.script.includes('run-cli-1143'), 'carries the run id');
assert.strictEqual(parsed.summary.resumeRunId, 'run-cli-1143');
assert.strictEqual(parsed.summary.waves, 1);
assert.strictEqual(parsed.summary.plans, 2);
} finally {
cleanup(tmp);
}
});
test('budget flag threads a shared token pool into the script', () => {
const tmp = createTempProject('claw-budget-');
try {
const manifestPath = writeManifest(tmp);
const parsed = runAndParse([
'claude-orchestration', 'emit-workflow',
'--waves', manifestPath,
'--run-id', 'r',
'--budget', '750000',
], tmp);
assert.ok(parsed.script.includes('budget('), 'budget() pool emitted');
assert.ok(parsed.script.includes('750000'));
} finally {
cleanup(tmp);
}
});
test('missing --waves -> non-zero exit with a diagnostic', () => {
const tmp = createTempProject('claw-noargs-');
try {
const res = runGsdTools([
'claude-orchestration', 'emit-workflow', '--run-id', 'r',
], tmp);
assert.strictEqual(res.success, false, 'missing --waves must fail');
assert.ok(res.exitCode !== 0, 'non-zero exit');
assert.match(res.error || '', /--waves/);
} finally {
cleanup(tmp);
}
});
});
// ─── detect-backend ───────────────────────────────────────────────────────────
describe('claude-orchestration detect-backend (CLI)', () => {
test('default config (capability off) -> inline, even on Claude', () => {
const tmp = createTempProject('claw-detect-');
try {
const parsed = runAndParse([
'claude-orchestration', 'detect-backend',
'--runtime', 'claude',
'--agent-sdk-version', '1.0.0',
], tmp);
assert.strictEqual(parsed.backend, 'inline');
assert.strictEqual(parsed.available, false);
assert.match(parsed.reason, /disabled/);
} finally {
cleanup(tmp);
}
});
test('enabled + claude + capable + new-enough SDK -> workflow', () => {
const tmp = createTempProject('claw-detect-on-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true, execution_backend: 'auto' } }),
'utf8',
);
const parsed = runAndParse([
'claude-orchestration', 'detect-backend',
'--runtime', 'claude',
'--agent-sdk-version', '1.2.0',
], tmp);
assert.strictEqual(parsed.backend, 'workflow');
assert.strictEqual(parsed.available, true);
} finally {
cleanup(tmp);
}
});
test('non-Claude runtime -> inline (criterion 6)', () => {
const tmp = createTempProject('claw-detect-nonclaude-');
try {
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.planning', 'config.json'),
JSON.stringify({ claude_orchestration: { enabled: true } }),
'utf8',
);
const parsed = runAndParse([
'claude-orchestration', 'detect-backend',
'--runtime', 'codex',
'--agent-sdk-version', '1.0.0',
], tmp);
assert.strictEqual(parsed.backend, 'inline');
assert.match(parsed.reason, /claude/i);
} finally {
cleanup(tmp);
}
});
});