@@ -30,6 +30,7 @@ import * as ThreadPlanProgress from "../ThreadPlanProgress.ts";
3030import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts" ;
3131import { encodeThreadDetailPageCursor } from "../threadDetailCursor.ts" ;
3232import { projectThreadDetailSnapshot } from "../ActivityPayloadProjection.ts" ;
33+ import { readSweepSnapshot } from "../ThreadPullRequestReactor.ts" ;
3334import { makeSqlStatementCounter } from "../../../integration/SqlStatementCounter.integration.ts" ;
3435
3536const asProjectId = ( value : string ) : ProjectId => ProjectId . make ( value ) ;
@@ -3574,6 +3575,74 @@ it.effect(
35743575 } ,
35753576) ;
35763577
3578+ it . effect ( "reads one sweep thread and its projects like the shell snapshot" , ( ) => {
3579+ const layer = OrchestrationProjectionSnapshotQueryLive . pipe (
3580+ Layer . provide ( ThreadBackgroundLiveness . layer ) ,
3581+ Layer . provide ( ThreadPlanProgress . layer ) ,
3582+ Layer . provide (
3583+ Layer . succeed ( RepositoryIdentityResolver . RepositoryIdentityResolver , {
3584+ resolve : ( ) =>
3585+ Effect . succeed ( {
3586+ canonicalKey : "github.com/acme/web" ,
3587+ provider : "github" ,
3588+ displayName : "acme/web" ,
3589+ locator : {
3590+ source : "git-remote" as const ,
3591+ remoteName : "origin" ,
3592+ remoteUrl : "https://github.com/acme/web.git" ,
3593+ } ,
3594+ } ) ,
3595+ } ) ,
3596+ ) ,
3597+ Layer . provideMerge ( SqlitePersistenceMemory ) ,
3598+ ) ;
3599+ return Effect . gen ( function * ( ) {
3600+ const sql = yield * SqlClient . SqlClient ;
3601+ const query = yield * ProjectionSnapshotQuery ;
3602+ yield * sql `INSERT INTO projection_projects (project_id, title, workspace_root, scripts_json, created_at, updated_at)
3603+ VALUES ('p1', 'One', '/one', '[]', '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z'),
3604+ ('p2', 'Two', '/two', '[]', '2026-09-02T00:00:00Z', '2026-09-02T00:00:00Z'),
3605+ ('p3', 'Three', '/three', '[]', '2026-09-03T00:00:00Z', '2026-09-03T00:00:00Z')` ;
3606+ yield * sql `INSERT INTO projection_threads (thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, branch, worktree_path, branch_pull_request_json, latest_turn_id, latest_user_message_at, pending_approval_count, snoozed_until, snoozed_at, created_at, updated_at, settled_override, settled_at)
3607+ VALUES
3608+ ('t-linked', 'p1', 'Linked', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', 'feature', '/one/wt', NULL, 'turn-1', '2026-09-02T00:00:00Z', 1, NULL, NULL, '2026-09-01T00:00:00Z', '2026-09-02T00:00:00Z', NULL, NULL),
3609+ ('t-branch', 'p1', 'Branch', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', 'other', NULL,
3610+ '{"projectId":"p2","repository":"acme/web","number":8,"url":"https://github.com/acme/web/pull/8"}',
3611+ NULL, NULL, 0, '2026-09-10T00:00:00Z', '2026-09-02T00:00:00Z', '2026-09-01T00:00:00Z', '2026-09-02T00:00:00Z', 'settled', '2026-09-03T00:00:00Z'),
3612+ ('t-other', 'p3', 'Other', '{"provider":"codex","model":"gpt-5"}', 'full-access', 'default', NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, '2026-09-01T00:00:00Z', '2026-09-01T00:00:00Z', NULL, NULL)` ;
3613+ yield * sql `INSERT INTO projection_thread_pull_requests (thread_id, host, repository, number, url, source, linked_at)
3614+ VALUES ('t-linked', 'github.com', 'acme/web', 7, 'https://github.com/acme/web/pull/7', 'agent', '2026-09-02T00:00:00Z')` ;
3615+ yield * sql `INSERT INTO projection_turns (thread_id, turn_id, state, requested_at, started_at, completed_at, checkpoint_files_json)
3616+ VALUES ('t-linked', 'turn-1', 'completed', '2026-09-02T00:00:00Z', '2026-09-02T00:00:01Z', '2026-09-02T00:00:02Z', '[]')` ;
3617+ yield * sql `INSERT INTO projection_thread_sessions (thread_id, status, provider_name, active_turn_id, last_error, updated_at)
3618+ VALUES ('t-linked', 'ready', 'codex', NULL, NULL, '2026-09-02T00:00:03Z')` ;
3619+ for ( const projector of Object . values ( ORCHESTRATION_PROJECTOR_NAMES ) ) {
3620+ yield * sql `INSERT INTO projection_state (projector, last_applied_sequence, updated_at)
3621+ VALUES (${ projector } , 9, '2026-09-02T00:00:03Z')` ;
3622+ }
3623+
3624+ const full = yield * query . getShellSnapshot ( ) ;
3625+ // The seeded fields must reach the snapshot, or the parity check is empty.
3626+ const linked = full . threads . find ( ( thread ) => thread . id === ThreadId . make ( "t-linked" ) ) ;
3627+ assert . strictEqual ( full . snapshotSequence , 9 ) ;
3628+ assert . strictEqual ( linked ?. linkedPullRequest ?. number , 7 ) ;
3629+ assert . strictEqual ( linked ?. latestTurn ?. turnId , asTurnId ( "turn-1" ) ) ;
3630+ assert . strictEqual ( linked ?. session ?. status , "ready" ) ;
3631+
3632+ for ( const [ threadId , projectIds ] of [
3633+ [ ThreadId . make ( "t-linked" ) , [ asProjectId ( "p1" ) ] ] ,
3634+ // Settlement also needs the project that the saved branch PR names.
3635+ [ ThreadId . make ( "t-branch" ) , [ asProjectId ( "p1" ) , asProjectId ( "p2" ) ] ] ,
3636+ ] as const ) {
3637+ assert . deepStrictEqual ( yield * readSweepSnapshot ( query , threadId ) , {
3638+ snapshotSequence : full . snapshotSequence ,
3639+ projects : full . projects . filter ( ( project ) => projectIds . includes ( project . id ) ) ,
3640+ threads : full . threads . filter ( ( thread ) => thread . id === threadId ) ,
3641+ } ) ;
3642+ }
3643+ } ) . pipe ( Effect . provide ( layer ) ) ;
3644+ } ) ;
3645+
35773646projectionSnapshotLayer ( "ProjectionSnapshotQuery activities by kind" , ( it ) => {
35783647 it . effect ( "lists one kind across active threads only, without hydrating the threads" , ( ) =>
35793648 Effect . gen ( function * ( ) {
0 commit comments