mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-18 02:42:25 +02:00
fix(test): tolerate skill-discovery race in PTY plan-mode smoke
The e2e-pty-plan-smoke suite (office-hours / plan-mode-no-op) failed in CI with `Unknown command: /office-hours` (claude exited ~10s) while passing locally. Root cause: a cold CI container's overlay-FS scan of the symlinked ~/.claude/skills registry finishes AFTER the runner's 8s boot grace, so the first `/skill` send reaches claude before the skill is indexed and is rejected as unknown. The runner gave up on the first "Unknown command:" line. runPlanSkillObservation now re-sends the skill command up to 3x (6s apart), re-marking the buffer each time so stale scrollback can't re-trip the check, before concluding the skill is genuinely unregistered. A real dangling-symlink / missing-skill still surfaces as 'exited' (after retries), preserving the original diagnostic. Pure-helper contract unchanged: 95/95 unit tests pass. This is a pre-existing harness bug (fails identically on #2077's own branch, which introduced the suite) surfaced while shipping the activation feature.
This commit is contained in:
@@ -1605,7 +1605,7 @@ export async function runPlanSkillObservation(opts: {
|
|||||||
// command.
|
// command.
|
||||||
await Bun.sleep(3000);
|
await Bun.sleep(3000);
|
||||||
}
|
}
|
||||||
const since = session.mark();
|
let since = session.mark();
|
||||||
session.send(`/${opts.skillName}\r`);
|
session.send(`/${opts.skillName}\r`);
|
||||||
|
|
||||||
const budgetMs = opts.timeoutMs ?? 180_000;
|
const budgetMs = opts.timeoutMs ?? 180_000;
|
||||||
@@ -1619,6 +1619,7 @@ export async function runPlanSkillObservation(opts: {
|
|||||||
// even if the current state is 'working'.
|
// even if the current state is 'working'.
|
||||||
let proseAUQEverObserved = false;
|
let proseAUQEverObserved = false;
|
||||||
let waitingEverObserved = false;
|
let waitingEverObserved = false;
|
||||||
|
let unknownCmdRetries = 0;
|
||||||
const JUDGE_AFTER_MS = 60_000;
|
const JUDGE_AFTER_MS = 60_000;
|
||||||
const JUDGE_INTERVAL_MS = 30_000;
|
const JUDGE_INTERVAL_MS = 30_000;
|
||||||
while (Date.now() - start < budgetMs) {
|
while (Date.now() - start < budgetMs) {
|
||||||
@@ -1634,9 +1635,24 @@ export async function runPlanSkillObservation(opts: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (visible.includes('Unknown command:')) {
|
if (visible.includes('Unknown command:')) {
|
||||||
|
// Skill-discovery race: in a cold CI container the overlay-FS scan of
|
||||||
|
// the symlinked ~/.claude/skills registry can finish AFTER the boot
|
||||||
|
// grace, so the first `/skill` send reaches claude before the skill is
|
||||||
|
// indexed and is rejected as unknown. (Reproduces only in CI — passes
|
||||||
|
// locally where claude is warm and discovery is instant.) Re-send the
|
||||||
|
// command a few times, re-marking first so the stale "Unknown command:"
|
||||||
|
// line in scrollback can't immediately re-trip this check, before
|
||||||
|
// concluding the skill is genuinely unregistered.
|
||||||
|
if (unknownCmdRetries < 3) {
|
||||||
|
unknownCmdRetries++;
|
||||||
|
await Bun.sleep(6000);
|
||||||
|
since = session.mark();
|
||||||
|
session.send(`/${opts.skillName}\r`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
outcome: 'exited',
|
outcome: 'exited',
|
||||||
summary: `claude rejected /${opts.skillName} as unknown command (skill not registered in this cwd)`,
|
summary: `claude rejected /${opts.skillName} as unknown command after ${unknownCmdRetries} retries (skill not registered in this cwd)`,
|
||||||
evidence: visible.slice(-2000),
|
evidence: visible.slice(-2000),
|
||||||
elapsedMs: Date.now() - startedAt,
|
elapsedMs: Date.now() - startedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user