| @@ -292,6 +292,7 @@ describe("CheckpointReactor", () => { · 291 unmodified lines |
| 292 | 292 | async function createHarness(options?: { |
| 293 | 293 | readonly hasSession?: boolean; |
| 294 | 294 | readonly seedFilesystemCheckpoints?: boolean; |
| 295 | + readonly initializeGit?: boolean; |
| 295 | 296 | readonly projectWorkspaceRoot?: string; |
| 296 | 297 | readonly threadWorktreePath?: string | null; |
| 297 | 298 | readonly threadBranch?: string | null; |
| @@ -303,6 +304,9 @@ describe("CheckpointReactor", () => { · 5 unmodified lines |
| 303 | 304 | readonly pullRequestRefreshCalls?: Array<string>; |
| 304 | 305 | }) { |
| 305 | 306 | const cwd = createGitRepository(); |
| 307 | + if (options?.initializeGit === false) { |
| 308 | + NodeFS.rmSync(NodePath.join(cwd, ".git"), { recursive: true }); |
| 309 | + } |
| 306 | 310 | tempDirs.push(cwd); |
| 307 | 311 | const provider = createProviderServiceHarness( |
| 308 | 312 | cwd, |
| @@ -1158,52 +1162,148 @@ describe("CheckpointReactor", () => { · 849 unmodified lines |
| 1158 | 1162 | ).toBe(true); |
| 1159 | 1163 | }); |
| 1160 | 1164 | |
| 1161 | | − it("appends capture failure activity when turn diff summary cannot be derived", async () => { |
| 1162 | | − const harness = await createHarness({ seedFilesystemCheckpoints: false }); |
| 1163 | | − const createdAt = "2026-01-01T00:00:00.000Z"; |
| 1164 | | − |
| 1165 | | − await Effect.runPromise( |
| 1166 | | − harness.engine.dispatch({ |
| 1167 | | − type: "thread.session.set", |
| 1168 | | − commandId: CommandId.make("cmd-session-set-missing-baseline-diff"), |
| 1165 | + effectIt.effect("captures a checkpoint without a summary when the baseline is missing", () => |
| 1166 | + Effect.gen(function* () { |
| 1167 | + const harness = yield* Effect.promise(() => |
| 1168 | + createHarness({ seedFilesystemCheckpoints: false }), |
| 1169 | + ); |
| 1170 | + harness.provider.emit({ |
| 1171 | + type: "turn.completed", |
| 1172 | + eventId: EventId.make("evt-turn-completed-missing-baseline"), |
| 1173 | + provider: ProviderDriverKind.make("codex"), |
| 1174 | + createdAt: "2026-01-01T00:00:00.000Z", |
| 1169 | 1175 | threadId: ThreadId.make("thread-1"), |
| 1170 | | − session: { |
| 1171 | | − threadId: ThreadId.make("thread-1"), |
| 1172 | | − status: "ready", |
| 1173 | | − providerName: "codex", |
| 1174 | | − runtimeMode: "approval-required", |
| 1175 | | − activeTurnId: null, |
| 1176 | | − lastError: null, |
| 1177 | | − updatedAt: createdAt, |
| 1178 | | − }, |
| 1179 | | − createdAt, |
| 1180 | | − }), |
| 1181 | | − ); |
| 1182 | | − |
| 1183 | | − harness.provider.emit({ |
| 1184 | | − type: "turn.completed", |
| 1185 | | − eventId: EventId.make("evt-turn-completed-missing-baseline"), |
| 1186 | | − provider: ProviderDriverKind.make("codex"), |
| 1176 | + turnId: asTurnId("turn-missing-baseline"), |
| 1177 | + payload: { state: "completed" }, |
| 1178 | + }); |
| 1179 | + expect(yield* harness.nextReceipt).toMatchObject({ |
| 1180 | + type: "checkpoint.diff.finalized", |
| 1181 | + checkpointTurnCount: 1, |
| 1182 | + }); |
| 1183 | + yield* Effect.promise(harness.drain); |
| 1184 | + const thread = (yield* Effect.promise(harness.readModel)).threads[0]; |
| 1185 | + expect(thread?.checkpoints[0]).toMatchObject({ |
| 1186 | + status: "ready", |
| 1187 | + checkpointTurnCount: 1, |
| 1188 | + files: [], |
| 1189 | + }); |
| 1190 | + expect( |
| 1191 | + gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1)), |
| 1192 | + ).toBe(true); |
| 1193 | + expect( |
| 1194 | + thread?.activities.some((activity) => activity.kind === "checkpoint.capture.failed"), |
| 1195 | + ).toBe(false); |
| 1196 | + }), |
| 1197 | + ); |
| 1187 | 1198 | |
| 1188 | | − createdAt: "2026-01-01T00:00:00.000Z", |
| 1189 | | − threadId: ThreadId.make("thread-1"), |
| 1190 | | − turnId: asTurnId("turn-missing-baseline"), |
| 1191 | | − payload: { state: "completed" }, |
| 1192 | | − }); |
| 1199 | + effectIt.effect.each([ |
| 1200 | + { timing: "between turns", commit: false }, |
| 1201 | + { timing: "between turns", commit: true }, |
| 1202 | + { timing: "during a turn", commit: false }, |
| 1203 | + { timing: "during a turn", commit: true }, |
| 1204 | + ])("resumes checkpointing after git init $timing (commit: $commit)", ({ timing, commit }) => |
| 1205 | + Effect.gen(function* () { |
| 1206 | + const harness = yield* Effect.promise(() => |
| 1207 | + createHarness({ initializeGit: false, seedFilesystemCheckpoints: false }), |
| 1208 | + ); |
| 1209 | + const threadId = ThreadId.make("thread-1"); |
| 1210 | + const createdAt = "2026-01-01T00:00:00.000Z"; |
| 1211 | + const emit = (type: "turn.started" | "turn.completed", turn: number) => |
| 1212 | + harness.provider.emit({ |
| 1213 | + type, |
| 1214 | + eventId: EventId.make(`${type}-${turn}`), |
| 1215 | + provider: ProviderDriverKind.make("codex"), |
| 1216 | + createdAt, |
| 1217 | + threadId, |
| 1218 | + turnId: asTurnId(`turn-${turn}`), |
| 1219 | + ...(type === "turn.completed" ? { payload: { state: "completed" } } : {}), |
| 1220 | + }); |
| 1221 | + emit("turn.started", 1); |
| 1222 | + yield* Effect.promise(harness.drain); |
| 1223 | + NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "before git\n"); |
| 1224 | + emit("turn.completed", 1); |
| 1225 | + yield* Effect.promise(harness.drain); |
| 1226 | + expect((yield* Effect.promise(harness.readModel)).threads[0]?.checkpoints).toEqual([]); |
| 1193 | 1227 | |
| 1194 | | − await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); |
| 1195 | | − const thread = await waitForThread( |
| 1196 | | − harness.readModel, |
| 1197 | | − (entry) => |
| 1198 | | − entry.checkpoints.length === 1 && |
| 1199 | | − entry.activities.some((activity) => activity.kind === "checkpoint.capture.failed"), |
| 1200 | | − ); |
| 1228 | + if (timing === "during a turn") { |
| 1229 | + emit("turn.started", 2); |
| 1230 | + yield* Effect.promise(harness.drain); |
| 1231 | + } |
| 1232 | + runGit(harness.cwd, ["init", "--initial-branch=main"]); |
| 1233 | + if (commit) { |
| 1234 | + runGit(harness.cwd, ["add", "."]); |
| 1235 | + runGit(harness.cwd, [ |
| 1236 | + "-c", |
| 1237 | + "user.name=Test", |
| 1238 | + "-c", |
| 1239 | + "user.email=test@example.com", |
| 1240 | + "commit", |
| 1241 | + "-m", |
| 1242 | + "Initial", |
| 1243 | + ]); |
| 1244 | + } |
| 1245 | + if (timing === "between turns") { |
| 1246 | + // Exercise the domain entry point as well as the provider turn-start event. |
| 1247 | + yield* harness.engine.dispatch({ |
| 1248 | + type: "thread.turn.start", |
| 1249 | + commandId: CommandId.make("cmd-after-git-init"), |
| 1250 | + threadId, |
| 1251 | + message: { |
| 1252 | + messageId: MessageId.make("message-after-git-init"), |
| 1253 | + role: "user", |
| 1254 | + text: "continue", |
| 1255 | + attachments: [], |
| 1256 | + }, |
| 1257 | + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 1258 | + runtimeMode: "approval-required", |
| 1259 | + createdAt, |
| 1260 | + }); |
| 1261 | + expect(yield* harness.nextReceipt).toMatchObject({ |
| 1262 | + type: "checkpoint.baseline.captured", |
| 1263 | + checkpointTurnCount: 0, |
| 1264 | + }); |
| 1265 | + emit("turn.started", 2); |
| 1266 | + yield* Effect.promise(harness.drain); |
| 1267 | + } |
| 1268 | + NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "after git\n"); |
| 1269 | + emit("turn.completed", 2); |
| 1270 | + expect(yield* harness.nextReceipt).toMatchObject({ |
| 1271 | + type: "checkpoint.diff.finalized", |
| 1272 | + checkpointTurnCount: 1, |
| 1273 | + }); |
| 1274 | + expect(yield* harness.nextReceipt).toMatchObject({ type: "turn.processing.quiesced" }); |
| 1275 | + yield* Effect.promise(harness.drain); |
| 1276 | + const firstCheckpoint = (yield* Effect.promise(harness.readModel)).threads[0]?.checkpoints[0]; |
| 1277 | + expect(firstCheckpoint?.files).toEqual( |
| 1278 | + timing === "between turns" |
| 1279 | + ? [{ path: "README.md", kind: "modified", additions: 1, deletions: 1 }] |
| 1280 | + : [], |
| 1281 | + ); |
| 1282 | + expect( |
| 1283 | + gitShowFileAtRef(harness.cwd, checkpointRefForThreadTurn(threadId, 1), "README.md"), |
| 1284 | + ).toBe("after git\n"); |
| 1285 | + expect(gitRefExists(harness.cwd, checkpointRefForThreadTurn(threadId, 0))).toBe( |
| 1286 | + timing === "between turns", |
| 1287 | + ); |
| 1201 | 1288 | |
| 1202 | | − expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 1203 | | − expect( |
| 1204 | | − thread.activities.some((activity) => activity.kind === "checkpoint.capture.failed"), |
| 1205 | | − ).toBe(true); |
| 1206 | | − }); |
| 1289 | + emit("turn.started", 3); |
| 1290 | + yield* Effect.promise(harness.drain); |
| 1291 | + NodeFS.writeFileSync(NodePath.join(harness.cwd, "README.md"), "next turn\n"); |
| 1292 | + emit("turn.completed", 3); |
| 1293 | + expect(yield* harness.nextReceipt).toMatchObject({ |
| 1294 | + type: "checkpoint.diff.finalized", |
| 1295 | + checkpointTurnCount: 2, |
| 1296 | + }); |
| 1297 | + yield* Effect.promise(harness.drain); |
| 1298 | + const thread = (yield* Effect.promise(harness.readModel)).threads[0]; |
| 1299 | + expect(thread?.checkpoints[1]?.files).toEqual([ |
| 1300 | + { path: "README.md", kind: "modified", additions: 1, deletions: 1 }, |
| 1301 | + ]); |
| 1302 | + expect( |
| 1303 | + thread?.activities.some((activity) => activity.kind === "checkpoint.capture.failed"), |
| 1304 | + ).toBe(false); |
| 1305 | + }), |
| 1306 | + ); |
| 1207 | 1307 | |
| 1208 | 1308 | it("captures pre-turn baseline from project workspace root when thread worktree is unset", async () => { |
| 1209 | 1309 | const harness = await createHarness({ |