1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as SqlClient from "effect/sql/SqlClient";
5
6import { runMigrations } from "../Migrations.ts";
7import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
8
9const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layer({ filename: ":memory:" })));
10
11layer("024_BackfillProjectionThreadShellSummary", (it) => {
12 it.effect("backfills thread shell summary fields and clears stale projected approvals", () =>
13 Effect.gen(function* () {
14 const sql = yield* SqlClient.SqlClient;
15
16 yield* runMigrations({ toMigrationInclusive: 23 });
17
18 yield* sql`
19 INSERT INTO projection_threads (
20 thread_id,
21 project_id,
22 title,
23 model_selection_json,
24 runtime_mode,
25 interaction_mode,
26 branch,
27 worktree_path,
28 latest_turn_id,
29 created_at,
30 updated_at,
31 archived_at,
32 latest_user_message_at,
33 pending_approval_count,
34 pending_user_input_count,
35 has_actionable_proposed_plan,
36 deleted_at
37 )
38 VALUES (
39 'thread-1',
40 'project-1',
41 'Thread 1',
42 '{"provider":"codex","model":"gpt-5-codex"}',
43 'approval-required',
44 'plan',
45 NULL,
46 NULL,
47 'turn-1',
48 '2026-02-24T00:00:00.000Z',
49 '2026-02-24T00:00:00.000Z',
50 NULL,
51 NULL,
52 0,
53 0,
54 0,
55 NULL
56 )
57 `;
58
59 yield* sql`
60 INSERT INTO projection_thread_messages (
61 message_id,
62 thread_id,
63 turn_id,
64 role,
65 text,
66 attachments_json,
67 is_streaming,
68 created_at,
69 updated_at
70 )
71 VALUES (
72 'message-user-1',
73 'thread-1',
74 'turn-1',
75 'user',
76 'Need help',
77 NULL,
78 0,
79 '2026-02-24T00:01:00.000Z',
80 '2026-02-24T00:01:00.000Z'
81 )
82 `;
83
84 yield* sql`
85 INSERT INTO projection_thread_activities (
86 activity_id,
87 thread_id,
88 turn_id,
89 tone,
90 kind,
91 summary,
92 payload_json,
93 sequence,
94 created_at
95 )
96 VALUES
97 (
98 'activity-approval-requested',
99 'thread-1',
100 'turn-1',
101 'approval',
102 'approval.requested',
103 'Command approval requested',
104 '{"requestId":"approval-1","requestKind":"command"}',
105 NULL,
106 '2026-02-24T00:02:00.000Z'
107 ),
108 (
109 'activity-approval-stale',
110 'thread-1',
111 'turn-1',
112 'error',
113 'provider.approval.respond.failed',
114 'Provider approval response failed',
115 '{"requestId":"approval-1","detail":"Unknown pending permission request: approval-1"}',
116 NULL,
117 '2026-02-24T00:03:00.000Z'
118 ),
119 (
120 'activity-user-input-requested',
121 'thread-1',
122 'turn-1',
123 'info',
124 'user-input.requested',
125 'User input requested',
126 '{"requestId":"input-1","questions":[{"id":"area","header":"Area","question":"Which repo area should I inspect next?","options":[{"label":"Server","description":"Server orchestration."}]}]}',
127 NULL,
128 '2026-02-24T00:04:00.000Z'
129 )
130 `;
131
132 yield* sql`
133 INSERT INTO projection_thread_proposed_plans (
134 plan_id,
135 thread_id,
136 turn_id,
137 plan_markdown,
138 implemented_at,
139 implementation_thread_id,
140 created_at,
141 updated_at
142 )
143 VALUES (
144 'plan-1',
145 'thread-1',
146 'turn-1',
147 '# Do the thing',
148 NULL,
149 NULL,
150 '2026-02-24T00:05:00.000Z',
151 '2026-02-24T00:05:00.000Z'
152 )
153 `;
154
155 yield* sql`
156 INSERT INTO projection_pending_approvals (
157 request_id,
158 thread_id,
159 turn_id,
160 status,
161 decision,
162 created_at,
163 resolved_at
164 )
165 VALUES (
166 'approval-1',
167 'thread-1',
168 'turn-1',
169 'pending',
170 NULL,
171 '2026-02-24T00:02:00.000Z',
172 NULL
173 )
174 `;
175
176 yield* runMigrations({ toMigrationInclusive: 24 });
177
178 const threadRows = yield* sql<{
179 readonly latestUserMessageAt: string | null;
180 readonly pendingApprovalCount: number;
181 readonly pendingUserInputCount: number;
182 readonly hasActionableProposedPlan: number;
183 }>`
184 SELECT
185 latest_user_message_at AS "latestUserMessageAt",
186 pending_approval_count AS "pendingApprovalCount",
187 pending_user_input_count AS "pendingUserInputCount",
188 has_actionable_proposed_plan AS "hasActionableProposedPlan"
189 FROM projection_threads
190 WHERE thread_id = 'thread-1'
191 `;
192 assert.deepStrictEqual(threadRows, [
193 {
194 latestUserMessageAt: "2026-02-24T00:01:00.000Z",
195 pendingApprovalCount: 0,
196 pendingUserInputCount: 1,
197 hasActionableProposedPlan: 1,
198 },
199 ]);
200
201 const approvalRows = yield* sql<{
202 readonly status: string;
203 readonly resolvedAt: string | null;
204 }>`
205 SELECT
206 status,
207 resolved_at AS "resolvedAt"
208 FROM projection_pending_approvals
209 WHERE request_id = 'approval-1'
210 `;
211 assert.deepStrictEqual(approvalRows, [
212 {
213 status: "resolved",
214 resolvedAt: "2026-02-24T00:03:00.000Z",
215 },
216 ]);
217 }),
218 );
219});