|
1 | 1 | import { |
| 2 | + CheckpointRef, |
2 | 3 | EventId, |
3 | 4 | GitManagerError, |
4 | 5 | ProjectId, |
5 | 6 | ProviderInstanceId, |
6 | 7 | ThreadId, |
| 8 | + TurnId, |
7 | 9 | type OrchestrationCommand, |
8 | 10 | type OrchestrationEvent, |
9 | 11 | type OrchestrationProjectShell, |
@@ -311,54 +313,79 @@ describe("ThreadPullRequestReactor", () => { |
311 | 313 | ), |
312 | 314 | ); |
313 | 315 |
|
314 | | - it.effect("refreshes discovery after a turn ends without client demand", () => |
315 | | - Effect.scoped( |
316 | | - Effect.gen(function* () { |
317 | | - const current = thread("turn-thread"); |
318 | | - const detected = yield* Ref.make<GitBranchPullRequest | null>(null); |
319 | | - const fixture = yield* makeHarness({ |
320 | | - threads: [current], |
321 | | - branchPullRequest: (_input, options) => |
322 | | - options?.refresh |
323 | | - ? Ref.set(detected, branchPullRequest()).pipe(Effect.andThen(Ref.get(detected))) |
324 | | - : Ref.get(detected), |
325 | | - }); |
326 | | - yield* Effect.gen(function* () { |
327 | | - const reactor = yield* fixture.start(); |
328 | | - expect(yield* Ref.get(fixture.commands)).toHaveLength(0); |
329 | | - yield* fixture.publish({ |
330 | | - type: "thread.session-set", |
331 | | - sequence: 2, |
332 | | - eventId: EventId.make("turn-finished"), |
333 | | - aggregateKind: "thread", |
334 | | - aggregateId: current.id, |
335 | | - occurredAt: NOW, |
336 | | - commandId: null, |
337 | | - causationEventId: null, |
338 | | - correlationId: null, |
339 | | - metadata: {}, |
340 | | - payload: { |
341 | | - threadId: current.id, |
342 | | - session: { |
| 316 | + it.effect.each(["session-first", "checkpoint-first"] as const)( |
| 317 | + "refreshes discovery once after a turn ends with %s event ordering", |
| 318 | + (order) => |
| 319 | + Effect.scoped( |
| 320 | + Effect.gen(function* () { |
| 321 | + const current = thread("turn-thread"); |
| 322 | + const detected = yield* Ref.make<GitBranchPullRequest | null>(null); |
| 323 | + const fixture = yield* makeHarness({ |
| 324 | + threads: [current], |
| 325 | + branchPullRequest: (_input, options) => |
| 326 | + options?.refresh |
| 327 | + ? Ref.set(detected, branchPullRequest()).pipe(Effect.andThen(Ref.get(detected))) |
| 328 | + : Ref.get(detected), |
| 329 | + }); |
| 330 | + yield* Effect.gen(function* () { |
| 331 | + const reactor = yield* fixture.start(); |
| 332 | + expect(yield* Ref.get(fixture.commands)).toHaveLength(0); |
| 333 | + const sessionEvent: OrchestrationEvent = { |
| 334 | + type: "thread.session-set", |
| 335 | + sequence: 2, |
| 336 | + eventId: EventId.make("turn-finished"), |
| 337 | + aggregateKind: "thread", |
| 338 | + aggregateId: current.id, |
| 339 | + occurredAt: NOW, |
| 340 | + commandId: null, |
| 341 | + causationEventId: null, |
| 342 | + correlationId: null, |
| 343 | + metadata: {}, |
| 344 | + payload: { |
343 | 345 | threadId: current.id, |
| 346 | + session: { |
| 347 | + threadId: current.id, |
| 348 | + status: "ready", |
| 349 | + providerName: "Codex", |
| 350 | + runtimeMode: "full-access", |
| 351 | + activeTurnId: null, |
| 352 | + lastError: null, |
| 353 | + updatedAt: NOW, |
| 354 | + }, |
| 355 | + }, |
| 356 | + }; |
| 357 | + const checkpointEvent: OrchestrationEvent = { |
| 358 | + ...sessionEvent, |
| 359 | + type: "thread.turn-diff-completed", |
| 360 | + sequence: 3, |
| 361 | + eventId: EventId.make("checkpoint-finished"), |
| 362 | + payload: { |
| 363 | + threadId: current.id, |
| 364 | + turnId: TurnId.make("turn"), |
| 365 | + checkpointTurnCount: 1, |
| 366 | + checkpointRef: CheckpointRef.make("checkpoint"), |
344 | 367 | status: "ready", |
345 | | - providerName: "Codex", |
346 | | - runtimeMode: "full-access", |
347 | | - activeTurnId: null, |
348 | | - lastError: null, |
349 | | - updatedAt: NOW, |
| 368 | + files: [], |
| 369 | + assistantMessageId: null, |
| 370 | + completedAt: NOW, |
350 | 371 | }, |
351 | | - }, |
352 | | - }); |
353 | | - yield* Queue.take(fixture.reads); |
354 | | - yield* reactor.drain; |
355 | | - expect((yield* Ref.get(fixture.commands))[0]?.branchPullRequest).toEqual(reference(42)); |
356 | | - expect((yield* Ref.get(fixture.branchCalls)).filter((call) => call.refresh)).toHaveLength( |
357 | | - 1, |
358 | | - ); |
359 | | - }).pipe(Effect.provide(fixture.layer)); |
360 | | - }), |
361 | | - ), |
| 372 | + }; |
| 373 | + const events = |
| 374 | + order === "session-first" |
| 375 | + ? [sessionEvent, checkpointEvent] |
| 376 | + : [checkpointEvent, sessionEvent]; |
| 377 | + for (const event of events) { |
| 378 | + yield* fixture.publish(event); |
| 379 | + yield* Queue.take(fixture.reads); |
| 380 | + yield* reactor.drain; |
| 381 | + } |
| 382 | + expect((yield* Ref.get(fixture.commands))[0]?.branchPullRequest).toEqual(reference(42)); |
| 383 | + expect( |
| 384 | + (yield* Ref.get(fixture.branchCalls)).filter((call) => call.refresh), |
| 385 | + ).toHaveLength(1); |
| 386 | + }).pipe(Effect.provide(fixture.layer)); |
| 387 | + }), |
| 388 | + ), |
362 | 389 | ); |
363 | 390 |
|
364 | 391 | it.effect("uses live worktrees and falls back to the project for removed worktrees", () => |
@@ -455,54 +482,56 @@ describe("ThreadPullRequestReactor", () => { |
455 | 482 | ), |
456 | 483 | ); |
457 | 484 |
|
458 | | - it.effect("retries failed settled backfills and stops querying them after success", () => |
459 | | - Effect.scoped( |
460 | | - Effect.gen(function* () { |
461 | | - const online = yield* Ref.make(false); |
462 | | - const fixture = yield* makeHarness({ |
463 | | - threads: [ |
464 | | - thread("backfill", { settledOverride: "settled", settledAt: NOW }), |
465 | | - thread("known", { |
466 | | - branch: "known", |
467 | | - settledOverride: "settled", |
468 | | - settledAt: NOW, |
469 | | - branchPullRequest: reference(1), |
470 | | - }), |
471 | | - ], |
472 | | - branchPullRequest: ({ cwd }) => |
473 | | - Ref.get(online).pipe( |
474 | | - Effect.flatMap((connected) => |
475 | | - connected |
476 | | - ? Effect.succeed(branchPullRequest(42, "merged")) |
477 | | - : Effect.fail( |
478 | | - new GitManagerError({ |
479 | | - operation: "branchPullRequest", |
480 | | - cwd, |
481 | | - detail: "Offline", |
482 | | - }), |
483 | | - ), |
| 485 | + it.effect.each(["settled", null] as const)( |
| 486 | + "retries settled backfills and stops after success with settledOverride %s", |
| 487 | + (settledOverride) => |
| 488 | + Effect.scoped( |
| 489 | + Effect.gen(function* () { |
| 490 | + const online = yield* Ref.make(false); |
| 491 | + const fixture = yield* makeHarness({ |
| 492 | + threads: [ |
| 493 | + thread("backfill", { settledOverride, settledAt: NOW }), |
| 494 | + thread("known", { |
| 495 | + branch: "known", |
| 496 | + settledOverride, |
| 497 | + settledAt: NOW, |
| 498 | + branchPullRequest: reference(1), |
| 499 | + }), |
| 500 | + ], |
| 501 | + branchPullRequest: ({ cwd }) => |
| 502 | + Ref.get(online).pipe( |
| 503 | + Effect.flatMap((connected) => |
| 504 | + connected |
| 505 | + ? Effect.succeed(branchPullRequest(42, "merged")) |
| 506 | + : Effect.fail( |
| 507 | + new GitManagerError({ |
| 508 | + operation: "branchPullRequest", |
| 509 | + cwd, |
| 510 | + detail: "Offline", |
| 511 | + }), |
| 512 | + ), |
| 513 | + ), |
484 | 514 | ), |
485 | | - ), |
486 | | - }); |
487 | | - yield* Effect.gen(function* () { |
488 | | - const reactor = yield* fixture.start(); |
489 | | - expect(yield* Ref.get(fixture.commands)).toHaveLength(0); |
490 | | - yield* Ref.set(online, true); |
491 | | - yield* TestClock.adjust("1 minute"); |
492 | | - yield* Queue.take(fixture.reads); |
493 | | - yield* reactor.drain; |
494 | | - expect((yield* Ref.get(fixture.commands))[0]?.threadId).toBe("backfill"); |
495 | | - yield* TestClock.adjust("1 minute"); |
496 | | - yield* Queue.take(fixture.reads); |
497 | | - yield* reactor.drain; |
498 | | - expect((yield* Ref.get(fixture.branchCalls)).map((call) => call.branch)).toEqual([ |
499 | | - "feature", |
500 | | - "feature", |
501 | | - "feature", |
502 | | - ]); |
503 | | - }).pipe(Effect.provide(fixture.layer)); |
504 | | - }), |
505 | | - ), |
| 515 | + }); |
| 516 | + yield* Effect.gen(function* () { |
| 517 | + const reactor = yield* fixture.start(); |
| 518 | + expect(yield* Ref.get(fixture.commands)).toHaveLength(0); |
| 519 | + yield* Ref.set(online, true); |
| 520 | + yield* TestClock.adjust("1 minute"); |
| 521 | + yield* Queue.take(fixture.reads); |
| 522 | + yield* reactor.drain; |
| 523 | + expect((yield* Ref.get(fixture.commands))[0]?.threadId).toBe("backfill"); |
| 524 | + yield* TestClock.adjust("1 minute"); |
| 525 | + yield* Queue.take(fixture.reads); |
| 526 | + yield* reactor.drain; |
| 527 | + expect((yield* Ref.get(fixture.branchCalls)).map((call) => call.branch)).toEqual([ |
| 528 | + "feature", |
| 529 | + "feature", |
| 530 | + "feature", |
| 531 | + ]); |
| 532 | + }).pipe(Effect.provide(fixture.layer)); |
| 533 | + }), |
| 534 | + ), |
506 | 535 | ); |
507 | 536 |
|
508 | 537 | it.effect("stops retrying a settled backfill after repeated lookup failures", () => |
|
0 commit comments