Phase 3 (atomic-write) and Phase 4 (validator) used !`...` eager-exec
with unresolved runtime placeholders; the harness executes those at
command LOAD time, so zsh parsed <project-dir> as input redirection and
/trekendsession aborted before the model saw a single instruction.
Both blocks are now plain runtime Bash fences with {curly} placeholders
(shell-inert, trekplan.md convention) and absolute ${CLAUDE_PLUGIN_ROOT}
paths (cwd-relative plugin paths were a latent ERR_MODULE_NOT_FOUND in
any user repo). Phase 1 discovery block keeps its legitimate eager-exec.
Regression guard: new tests/commands/trekendsession.test.mjs flags any
!`-block in commands/*.md containing <angle>/{curly} placeholders, and
pins Phase 3/4 as runtime Bash. Suite 828 -> 832 (830/0/2). E2E: fixed
blocks run with real values write both state files, validator valid:true;
trekcontinue.md:147 runtime-verified self-contained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NXzF3F2zAM8S7FCBXCqAb
127 lines
5.2 KiB
JavaScript
127 lines
5.2 KiB
JavaScript
// 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 (<project-dir> etc.). The harness executes
|
|
// eager-exec blocks at command LOAD time, so zsh parsed <project-dir> 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 { readFileSync, readdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
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 (<angle> or {curly}) in an
|
|
// eager block is executed verbatim by the shell at load time — <x> 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: <angle-word> 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 <angle> placeholders in commands — zsh parses <x> as input redirection',
|
|
);
|
|
});
|