// tests/lib/session-orders.test.mjs // /trekexecute's multi-session path turns each wave into orders plus starters // and stops: the user opens every session themselves. Voyage starts nothing. // These tests run the module and its CLI on a small fixture plan and prove // both halves: the orders land, and no process is started. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync, chmodSync, cpSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { parseExecutionStrategy, buildWaveOrders, progressFileFor, waveStatus, } from '../../lib/util/session-orders.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const MODULE = join(ROOT, 'lib', 'util', 'session-orders.mjs'); const FIXTURE = join(ROOT, 'tests', 'fixtures', 'session-orders', 'plan.md'); const PLAN_REL = '.claude/plans/demo.md'; function withRepo(fn) { const root = mkdtempSync(join(tmpdir(), 'session-orders-')); try { mkdirSync(join(root, '.claude', 'plans'), { recursive: true }); cpSync(FIXTURE, join(root, PLAN_REL)); return fn(root); } finally { rmSync(root, { recursive: true, force: true }); } } test('parseExecutionStrategy reads sessions, waves, step ranges, dependencies and cwd', () => { const s = parseExecutionStrategy(readFileSync(FIXTURE, 'utf-8')); assert.deepEqual(s.waves, [1, 2]); assert.deepEqual(s.sessions.map(x => [x.n, x.title, x.wave, x.steps, x.dependsOn, x.cwd]), [ [1, 'Parser', 1, [1, 2], [], '.'], [2, 'Plugin subdir', 1, [3, 4], [], 'plugins/demo'], [3, 'Integration', 2, [5], [1, 2], '.'], ]); }); test('parseExecutionStrategy refuses a plan without a strategy, and a session without a wave', () => { assert.throws(() => parseExecutionStrategy('# Plan\n\n## Verification\n'), /no ## Execution Strategy/); assert.throws( () => parseExecutionStrategy('## Execution Strategy\n\n### Session 1: X\n- **Steps:** 1\n'), /Session 1 has no Wave/, ); }); test('buildWaveOrders gives one order per session in the wave, each with its own worktree and a starter', () => { const s = parseExecutionStrategy(readFileSync(FIXTURE, 'utf-8')); const orders = buildWaveOrders({ strategy: s, wave: 1, planPath: PLAN_REL, sessionDir: '/r/.claude/trekplan-sessions/demo' }); assert.equal(orders.length, 2); assert.deepEqual(orders.map(o => o.starter), [ `/trekexecute --session 1 ${PLAN_REL}`, `/trekexecute --session 2 ${PLAN_REL}`, ]); assert.deepEqual(orders.map(o => o.openIn), [ '/r/.claude/trekplan-sessions/demo/worktrees/session-1', '/r/.claude/trekplan-sessions/demo/worktrees/session-2/plugins/demo', ]); assert.equal(new Set(orders.map(o => o.worktreePath)).size, 2, 'two sessions never share a worktree'); for (const o of orders) { assert.match(o.body, /Voyage does not start this session/); assert.ok(o.body.includes(o.starter)); assert.ok(o.body.includes(o.progressFile)); } assert.throws(() => buildWaveOrders({ strategy: s, wave: 3, planPath: PLAN_REL, sessionDir: '/r' }), /no sessions in wave 3/); assert.throws(() => buildWaveOrders({ strategy: s, wave: 1, planPath: '/abs/plan.md', sessionDir: '/r' }), /repo-relative/); }); test('the module cannot start a process: it imports nothing that could', () => { const src = readFileSync(MODULE, 'utf-8'); assert.doesNotMatch(src, /child_process|worker_threads|\bspawn\(|\bexec(File)?(Sync)?\(/); }); test('CLI on the fixture: wave 1 writes two orders and wave.json, prints the starters, and starts nothing', () => { withRepo((root) => { // Trap: a `claude` first on PATH that leaves a marker if anything runs it. const bin = join(root, 'trap-bin'); const marker = join(root, 'claude-was-started'); mkdirSync(bin); writeFileSync(join(bin, 'claude'), `#!/bin/sh\ntouch "${marker}"\n`); chmodSync(join(bin, 'claude'), 0o755); const sessionDir = join(root, '.claude', 'trekplan-sessions', 'demo'); const r = spawnSync(process.execPath, [MODULE, 'write', '--plan', PLAN_REL, '--wave', '1', '--session-dir', sessionDir], { cwd: root, encoding: 'utf-8', env: { ...process.env, PATH: `${bin}:${process.env.PATH}` }, }); assert.equal(r.status, 0, r.stderr); assert.equal(existsSync(marker), false, 'nothing may start Claude Code'); const ordersDir = join(sessionDir, 'orders'); assert.deepEqual(readdirSync(ordersDir).sort(), ['wave-1-session-1.md', 'wave-1-session-2.md', 'wave.json']); const wave = JSON.parse(readFileSync(join(ordersDir, 'wave.json'), 'utf-8')); assert.deepEqual([wave.wave, wave.sessions, wave.status, wave.plan], [1, [1, 2], 'awaiting-sessions', PLAN_REL]); assert.match(r.stdout, /\/trekexecute --session 1 \.claude\/plans\/demo\.md/); assert.match(r.stdout, /\/trekexecute --session 2 \.claude\/plans\/demo\.md/); assert.match(r.stdout, /Voyage starts none of them/); }); }); test('CLI status: pending until each session writes its progress file, then completed', () => { withRepo((root) => { const sessionDir = join(root, '.claude', 'trekplan-sessions', 'demo'); const run = (...a) => spawnSync(process.execPath, [MODULE, ...a, '--plan', PLAN_REL, '--session-dir', sessionDir], { cwd: root, encoding: 'utf-8' }); assert.equal(run('write', '--wave', '1').status, 0); let st = JSON.parse(run('status', '--wave', '1').stdout); assert.deepEqual(st.sessions.map(x => x.status), ['pending', 'pending']); assert.equal(st.all_completed, false); const s = parseExecutionStrategy(readFileSync(FIXTURE, 'utf-8')); const put = (n, status) => { const f = progressFileFor({ strategy: s, n, planPath: PLAN_REL, sessionDir }); mkdirSync(dirname(f), { recursive: true }); writeFileSync(f, JSON.stringify({ status })); }; put(1, 'completed'); put(2, 'failed'); st = JSON.parse(run('status', '--wave', '1').stdout); assert.deepEqual(st.sessions.map(x => x.status), ['completed', 'failed']); assert.equal(st.all_completed, false); put(2, 'completed'); st = waveStatus({ strategy: s, wave: 1, planPath: PLAN_REL, sessionDir }); assert.equal(st.all_completed, true); }); }); test('CLI refuses a plan without an Execution Strategy, loudly', () => { withRepo((root) => { writeFileSync(join(root, PLAN_REL), '# Plan\n\n## Verification\n'); const r = spawnSync(process.execPath, [MODULE, 'write', '--plan', PLAN_REL, '--wave', '1', '--session-dir', join(root, 's')], { cwd: root, encoding: 'utf-8' }); assert.equal(r.status, 1); assert.match(r.stderr, /no ## Execution Strategy/); }); });