// tests/commands/trekendsession.test.mjs // Regression tests for /trekendsession (commands/trekendsession.md). // // Bug (2026-07-03): two of the three !`...` eager-exec blocks contained // unresolved placeholders ( etc.). The harness executes // eager-exec blocks at command LOAD time, so zsh parsed as // input redirection and the command aborted before the model saw a single // instruction. Eager-exec is only valid for self-contained commands. // // Pattern D (markdown structure) — assertions against command prose. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { childEnv } from '../helpers/child-env.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const COMMANDS_DIR = join(ROOT, 'commands'); const COMMAND_FILE = join(COMMANDS_DIR, 'trekendsession.md'); function readCommand() { return readFileSync(COMMAND_FILE, 'utf8'); } function extractPhase(commandText, phaseHeader) { const startIdx = commandText.indexOf(phaseHeader); if (startIdx === -1) return ''; const rest = commandText.slice(startIdx); const nextPhase = rest.search(/\n## (?:Phase |Hard )/); if (nextPhase === -1) return rest; return rest.slice(0, nextPhase); } // Extract all eager-exec blocks (!`...`) from a command/skill file, // including multi-line blocks. Returns [{ content, line }]. function extractEagerBlocks(text) { const blocks = []; const re = /!`([^`]+)`/g; let m; while ((m = re.exec(text)) !== null) { const line = text.slice(0, m.index).split('\n').length; blocks.push({ content: m[1], line }); } return blocks; } // --------------------------------------------------------------- // Marketplace-wide regression guard: eager-exec blocks must be // self-contained. An unresolved placeholder ( or {curly}) in an // eager block is executed verbatim by the shell at load time — is // parsed as input redirection and aborts the whole command load. // --------------------------------------------------------------- test('eager-exec guard — no !`-block in commands/ contains an unresolved placeholder', () => { const offenders = []; for (const file of readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'))) { const text = readFileSync(join(COMMANDS_DIR, file), 'utf8'); for (const { content, line } of extractEagerBlocks(text)) { // Placeholder conventions: or {curly_word}. Curly must // contain a separator (- or _) so JS destructuring like {join} in a // legitimate self-contained script does not false-positive; angle // placeholders are unambiguous (shell would parse them as redirects). if (/<[a-z][a-z0-9_-]*>/.test(content) || /\{[a-z][a-z0-9]*([_-][a-z0-9]+)+\}/.test(content)) { offenders.push(`${file}:${line}`); } } } assert.deepEqual( offenders, [], `eager-exec !\`-blocks run at command LOAD time and must be self-contained; ` + `placeholder found in: ${offenders.join(', ')}`, ); }); // --------------------------------------------------------------- // trekendsession-specific: exactly one eager block (Phase 1 project // discovery — self-contained, legitimate); Phases 3 and 4 are runtime // Bash-tool commands with model-substituted values, never eager. // --------------------------------------------------------------- test('trekendsession — exactly one eager-exec block remains (Phase 1 discovery)', () => { const cmd = readCommand(); const blocks = extractEagerBlocks(cmd); assert.equal( blocks.length, 1, `expected exactly 1 eager-exec block (Phase 1 discovery), got ${blocks.length} at line(s) ${blocks.map((b) => b.line).join(', ')}`, ); assert.match( blocks[0].content, /readdirSync\(root\)/, 'the surviving eager block must be the self-contained Phase 1 discovery script', ); }); test('trekendsession Phase 3 — atomic-write block is runtime Bash (no eager prefix) with plugin-root import', () => { const phase3 = extractPhase(readCommand(), '## Phase 3 '); assert.doesNotMatch(phase3, /!`/, 'Phase 3 must not use eager-exec — values exist only at runtime'); assert.match( phase3, /\$\{CLAUDE_PLUGIN_ROOT\}\/lib\/util\/atomic-write\.mjs/, 'Phase 3 import must use the absolute ${CLAUDE_PLUGIN_ROOT} path — cwd is the user repo, not the plugin root', ); assert.doesNotMatch( phase3, /['"]\.\/lib\/util\/atomic-write\.mjs['"]/, 'Phase 3 must not import atomic-write.mjs via a cwd-relative path', ); }); test('trekendsession Phase 4 — validator call is runtime Bash (no eager prefix) with plugin-root path', () => { const phase4 = extractPhase(readCommand(), '## Phase 4 '); assert.doesNotMatch(phase4, /!`/, 'Phase 4 must not use eager-exec — the state-file path exists only at runtime'); assert.match( phase4, /\$\{CLAUDE_PLUGIN_ROOT\}\/lib\/validators\/session-state-validator\.mjs/, 'Phase 4 validator path must use the absolute ${CLAUDE_PLUGIN_ROOT} convention', ); assert.doesNotMatch( phase4, /<[a-z][a-z0-9_-]*>/, 'Phase 4 must not use placeholders in commands — zsh parses as input redirection', ); }); // --------------------------------------------------------------- // DJ5 (fix round 4.5b): Phase 3 splices no label into its shell line. // The block used to end in '{project_dir}' '{next_brief_path}' // '{next_label}', filled in by the model: a label holding a ' closed the // quote and the rest ran as shell. Phase 3 now re-reads $ARGUMENTS through // the quoted heredoc on stdin, parses it with parseArgs and re-derives the // project directory itself. The runtime tests run the block as Claude Code // would after it substitutes ${CLAUDE_PLUGIN_ROOT} and $ARGUMENTS. // --------------------------------------------------------------- const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E'; const PROJECT_REL = join('.claude', 'projects', '2026-01-01-demo'); const NEXT_BRIEF = '.claude/projects/2026-01-01-demo/next-brief.md'; /** The one runtime ```bash block of Phase 3 (the atomic-write block). */ function phase3Block() { const phase3 = extractPhase(readCommand(), '## Phase 3 '); const blocks = phase3.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0]) .filter((b) => b.includes('atomic-write.mjs')); assert.equal(blocks.length, 1, `Phase 3 must hold one \`\`\`bash block that runs atomic-write.mjs; found ${blocks.length}`); return blocks[0]; } /** True when `bin` can be started from PATH. */ function onPath(bin) { const r = spawnSync(bin, ['-c', 'exit 0'], { env: childEnv(), encoding: 'utf8' }); return r.error === undefined && r.status === 0; } /** * Run the Phase 3 block with `shell` in a fresh fixture project, with * `argText` standing where Claude Code puts $ARGUMENTS. Returns the result, * what the run wrote, and whether a PWNED file appeared; the fixture is gone * on return. */ function runPhase3(shell, argText) { const block = phase3Block(); assert.ok(block.includes('$ARGUMENTS'), 'Phase 3 does not re-read $ARGUMENTS: its values still come from text the model splices into the shell line'); const script = block.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT).split('$ARGUMENTS').join(argText); const fixture = mkdtempSync(join(tmpdir(), 'trekendsession-dj5-')); try { mkdirSync(join(fixture, PROJECT_REL), { recursive: true }); writeFileSync(join(fixture, PROJECT_REL, 'brief.md'), '# demo brief\n'); const r = spawnSync(shell, ['-c', script], { cwd: fixture, env: childEnv(), encoding: 'utf8' }); const stateFile = join(fixture, PROJECT_REL, '.session-state.local.json'); const promptFile = join(fixture, PROJECT_REL, 'NEXT-SESSION-PROMPT.local.md'); return { r, pwned: ['PWNED', 'PWNED2'].filter((f) => existsSync(join(fixture, f))), state: existsSync(stateFile) ? JSON.parse(readFileSync(stateFile, 'utf8')) : null, prompt: existsSync(promptFile) ? readFileSync(promptFile, 'utf8') : null, }; } finally { rmSync(fixture, { recursive: true, force: true }); } } /** Assert one run wrote `label` whole and ran nothing from it. */ function assertLabelKept(shell, label) { const { r, pwned, state, prompt } = runPhase3(shell, `${NEXT_BRIEF} ${label}`); const how = `${shell}, label ${JSON.stringify(label)}: exit ${r.status}\n${r.stdout}${r.stderr}`; assert.deepEqual(pwned, [], `the label ran as shell under ${how}`); assert.equal(r.status, 0, `Phase 3 did not run cleanly under ${how}`); assert.ok(state, `no .session-state.local.json was written under ${how}`); assert.equal(state.next_session_label, label, `the written label is not the typed one under ${how}`); assert.equal(state.next_session_brief_path, NEXT_BRIEF, `the written brief path is not the typed one under ${how}`); assert.equal(state.project, PROJECT_REL, `the project directory was not re-derived under ${how}`); assert.ok(prompt && prompt.includes(`\n# ${label}\n`), `NEXT-SESSION-PROMPT.local.md does not carry the label under ${how}`); } test('[DJ5] Phase 3 has no quoted placeholder for the label, brief or project', () => { const block = phase3Block(); const placeholders = block.match(/\{[a-z][a-z0-9]*(?:[_ -][a-z0-9]+)*\}/g) || []; assert.deepEqual(placeholders, [], 'Phase 3 still has the model fill {placeholders} into its shell line'); assert.doesNotMatch(block, /'\{/, 'Phase 3 still quotes a placeholder as a shell word'); const lines = block.split('\n'); const open = lines.findIndex((l) => l.trimEnd().endsWith(`<<'${ARGS_DELIM}'`)); assert.ok(open !== -1, `Phase 3 does not re-read the arguments through the quoted heredoc <<'${ARGS_DELIM}'`); assert.equal(lines[open + 1], '$ARGUMENTS', 'the line inside the Phase 3 heredoc is not $ARGUMENTS alone'); const filled = lines.filter((l) => l.trim() !== ''); assert.ok(lines[open + 2] === ARGS_DELIM && filled[filled.length - 1] === ARGS_DELIM, `the closing line ${ARGS_DELIM} does not follow $ARGUMENTS as the block's last non-empty line`); assert.equal(lines.filter((l, i) => i !== open + 1 && l.includes('$ARGUMENTS')).length, 0, 'a line outside the Phase 3 heredoc carries $ARGUMENTS'); assert.match(block, /import \{ parseArgs \} from '\$\{CLAUDE_PLUGIN_ROOT\}\/lib\/parsers\/arg-parser\.mjs'/, 'Phase 3 does not import parseArgs at runtime from the plugin root'); }); test('[DJ5] Phase 3 run with an injection label creates no file and keeps the label whole', (t) => { // The step's label, and the same with a command substitution: under bash, // and under zsh (the shell the Bash tool runs) when zsh is on PATH. const labels = ["a'; touch PWNED; echo 'b", "a'; touch PWNED; echo 'b $(touch PWNED2)"]; for (const label of labels) assertLabelKept('bash', label); if (onPath('zsh')) { for (const label of labels) assertLabelKept('zsh', label); } else { t.diagnostic('zsh is not on PATH: the zsh half of this test was not run'); } }); test("[DJ5] a label with an apostrophe is kept", () => { assertLabelKept('bash', "Fix the user's login"); });