fix(gsd): address QA round 2

- Initialize autoStartTime, originalModelId, originalModelProvider, and
  autoModeStartModel on file-based paused-session resume path so
  dashboard elapsed time, model restore on stop, and error-recovery
  model fallback all work correctly.
- Validate worktreePath existence before using it as assessment base;
  fall back to project root when the worktree has been torn down.
- Add tests for paused-session-only (no lock) with resumable disk state
  and for worktreePath fallback when the directory no longer exists.
This commit is contained in:
Derek Pearson 2026-03-21 21:11:11 -04:00
parent 9e357f8a85
commit 364cc7dcbc
3 changed files with 43 additions and 1 deletions

View file

@ -1023,6 +1023,12 @@ export async function startAuto(
s.stepMode = s.stepMode || requestedStepMode;
s.cmdCtx = ctx;
s.basePath = base;
s.autoStartTime = Date.now();
s.originalModelId = ctx.model?.id ?? null;
s.originalModelProvider = ctx.model?.provider ?? null;
if (ctx.model) {
s.autoModeStartModel = { provider: ctx.model.provider, id: ctx.model.id };
}
s.unitDispatchCount.clear();
s.unitLifetimeDispatches.clear();
if (!getLedger()) initMetrics(base);

View file

@ -74,7 +74,10 @@ export async function assessInterruptedSession(
basePath: string,
): Promise<InterruptedSessionAssessment> {
const pausedSession = readPausedSessionMetadata(basePath);
const assessmentBasePath = pausedSession?.worktreePath || basePath;
const worktreeExists = pausedSession?.worktreePath
? existsSync(pausedSession.worktreePath)
: false;
const assessmentBasePath = worktreeExists ? pausedSession!.worktreePath! : basePath;
const rawLock = readCrashLock(basePath);
const lock = rawLock && rawLock.pid !== process.pid ? rawLock : null;

View file

@ -251,6 +251,39 @@ test("assessInterruptedSession marks stale paused-session metadata as stale when
}
});
test("assessInterruptedSession classifies paused session without lock as recoverable when disk state is resumable", async () => {
const base = makeTmpBase();
try {
writeRoadmap(base, false);
writePausedSession(base, "M001", true);
const assessment = await assessInterruptedSession(base);
assert.equal(assessment.classification, "recoverable");
assert.equal(assessment.lock, null);
assert.equal(assessment.pausedSession?.milestoneId, "M001");
assert.equal(assessment.hasResumableDiskState, true);
assert.equal(assessment.isBootstrapCrash, false);
} finally {
cleanup(base);
}
});
test("assessInterruptedSession falls back to basePath when worktreePath no longer exists", async () => {
const base = makeTmpBase();
try {
writeRoadmap(base, false);
// Reference a worktree that doesn't exist on disk
writePausedSession(base, "M001", false, "/nonexistent/worktree");
const assessment = await assessInterruptedSession(base);
// Should use basePath (which has an unfinished roadmap) instead of the missing worktree
assert.equal(assessment.classification, "recoverable");
assert.equal(assessment.hasResumableDiskState, true);
} finally {
cleanup(base);
}
});
test("assessInterruptedSession prefers paused worktree state when worktreePath is recorded", async () => {
const base = makeTmpBase();
const worktree = join(base, "worktree-copy");