Files
gstack/test/helpers/codex-multiblock.ts
T
garrytan c74c3df073 E6: a block that reads a SKILL.md is not a missing install
The first live codex-multiblock-live run passed its real check (the /learn
block found the install and printed the seeded learning) but the oracle
flagged a sed of the learn SKILL.md, whose prelude source quotes the
'gstack: no install found (tried $_r)' text. Only an emitted line that
starts with the message and names a real root counts now; free replay
negative added.
2026-10-04 19:23:45 +00:00

72 lines
3.8 KiB
TypeScript

/**
* Oracles for the live Codex multi-block case (E6,
* test/codex-e2e-multiblock-live.test.ts). Codex runs every shell block in a
* fresh shell, so a gstack skill works there only when each later block
* resolves the install on its own (the env-var host prelude). The case
* installs gstack with `./setup --host codex` into a fresh CODEX_HOME, seeds
* one learning, and runs /learn's "Show recent" block through `codex exec`.
* Pure functions: the free test replays synthetic `exec --json` events.
*/
export const MULTIBLOCK_SENTINEL_KEY = 'e6-multiblock-sentinel';
interface CommandEvent { command: string; output: string; exitCode: number | null }
function completedCommands(rawLines: readonly string[]): CommandEvent[] {
const commands: CommandEvent[] = [];
for (const line of rawLines) {
try {
const event = JSON.parse(line);
if (event?.type !== 'item.completed' || event.item?.type !== 'command_execution') continue;
commands.push({ command: String(event.item.command ?? ''), output: String(event.item.aggregated_output ?? ''),
exitCode: typeof event.item.exit_code === 'number' ? event.item.exit_code : null });
} catch { /* not an event line */ }
}
return commands;
}
/**
* Problems with a live run: any block that could not find the install, a
* gstack helper path that did not resolve, or no later block (the skill's
* learnings-search block, which only the prelude can root) that ran the
* installed helper and printed the seeded learning.
*/
export function multiblockProblems(rawLines: readonly string[], sentinel = MULTIBLOCK_SENTINEL_KEY): string[] {
const commands = completedCommands(rawLines);
const problems: string[] = [];
for (const { command, output } of commands) {
// The emitted line names the root it tried; the prelude's source text
// (a block that reads a SKILL.md) quotes it mid-line with `$_r`.
const missing = /^gstack: no install found \(tried (?!\$)[^\n]*/m.exec(output);
if (missing) problems.push(`a block could not find the install: ${missing[0].slice(0, 200)}`);
const unresolved = /\S*\/bin\/gstack-[\w-]+: (?:No such file or directory|command not found|not found)/.exec(output);
if (unresolved) problems.push(`a gstack helper path did not resolve: ${unresolved[0]} (in: ${command.slice(0, 120)})`);
}
const search = commands.filter(c => c.command.includes('gstack-learnings-search'));
if (search.length === 0) problems.push('no block ran gstack-learnings-search (the skill\'s "Show recent" block)');
else if (!search.some(c => c.exitCode === 0 && c.output.includes(sentinel) && !/GSTACK_ROOT=["']?\//.test(c.command))) {
problems.push(`no learnings-search block rooted by its own prelude printed the seeded learning ${sentinel}: `
+ search.map(c => `exit ${c.exitCode}, ${c.output.includes(sentinel) ? 'sentinel present' : 'sentinel absent'}`
+ `${/GSTACK_ROOT=["']?\//.test(c.command) ? ', GSTACK_ROOT set by hand' : ''}`).join('; '));
}
return problems;
}
/**
* The `gstack` router entry from `codex debug prompt-input` output (a JSON
* array of input items): the skill path it names, resolved through the skill
* roots table, or null when Codex does not list the router.
*/
export function routerPathFromPromptInput(promptInput: string): string | null {
let text = promptInput;
try {
const items = JSON.parse(promptInput.slice(promptInput.indexOf('[')));
text = (Array.isArray(items) ? items : []).flatMap((item: { content?: Array<{ text?: string }> }) => item.content ?? [])
.map(part => part.text ?? '').join('\n');
} catch { /* scan the raw text */ }
const entry = /^- gstack: .*\(file: (r\d+)\/gstack\/SKILL\.md\)$/m.exec(text);
if (!entry) return null;
const root = new RegExp(`^- \`${entry[1]}\` = \`([^\`]+)\`$`, 'm').exec(text);
return root ? `${root[1]}/gstack/SKILL.md` : null;
}