import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as SqlClient from "effect/unstable/sql/SqlClient"; import { runMigrations } from "../Migrations.ts";import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); layer("024_BackfillProjectionThreadShellSummary", (it) => { it.effect("backfills thread shell summary fields and clears stale projected approvals", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* runMigrations({ toMigrationInclusive: 23 }); yield* sql` INSERT INTO projection_threads ( thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, branch, worktree_path, latest_turn_id, created_at, updated_at, archived_at, latest_user_message_at, pending_approval_count, pending_user_input_count, has_actionable_proposed_plan, deleted_at ) VALUES ( 'thread-1', 'project-1', 'Thread 1', '{"provider":"codex","model":"gpt-5-codex"}', 'approval-required', 'plan', NULL, NULL, 'turn-1', '2026-02-24T00:00:00.000Z', '2026-02-24T00:00:00.000Z', NULL, NULL, 0, 0, 0, NULL ) `; yield* sql` INSERT INTO projection_thread_messages ( message_id, thread_id, turn_id, role, text, attachments_json, is_streaming, created_at, updated_at ) VALUES ( 'message-user-1', 'thread-1', 'turn-1', 'user', 'Need help', NULL, 0, '2026-02-24T00:01:00.000Z', '2026-02-24T00:01:00.000Z' ) `; yield* sql` INSERT INTO projection_thread_activities ( activity_id, thread_id, turn_id, tone, kind, summary, payload_json, sequence, created_at ) VALUES ( 'activity-approval-requested', 'thread-1', 'turn-1', 'approval', 'approval.requested', 'Command approval requested', '{"requestId":"approval-1","requestKind":"command"}', NULL, '2026-02-24T00:02:00.000Z' ), ( 'activity-approval-stale', 'thread-1', 'turn-1', 'error', 'provider.approval.respond.failed', 'Provider approval response failed', '{"requestId":"approval-1","detail":"Unknown pending permission request: approval-1"}', NULL, '2026-02-24T00:03:00.000Z' ), ( 'activity-user-input-requested', 'thread-1', 'turn-1', 'info', 'user-input.requested', 'User input requested', '{"requestId":"input-1","questions":[{"id":"area","header":"Area","question":"Which repo area should I inspect next?","options":[{"label":"Server","description":"Server orchestration."}]}]}', NULL, '2026-02-24T00:04:00.000Z' ) `; yield* sql` INSERT INTO projection_thread_proposed_plans ( plan_id, thread_id, turn_id, plan_markdown, implemented_at, implementation_thread_id, created_at, updated_at ) VALUES ( 'plan-1', 'thread-1', 'turn-1', '# Do the thing', NULL, NULL, '2026-02-24T00:05:00.000Z', '2026-02-24T00:05:00.000Z' ) `; yield* sql` INSERT INTO projection_pending_approvals ( request_id, thread_id, turn_id, status, decision, created_at, resolved_at ) VALUES ( 'approval-1', 'thread-1', 'turn-1', 'pending', NULL, '2026-02-24T00:02:00.000Z', NULL ) `; yield* runMigrations({ toMigrationInclusive: 24 }); const threadRows = yield* sql<{ readonly latestUserMessageAt: string | null; readonly pendingApprovalCount: number; readonly pendingUserInputCount: number; readonly hasActionableProposedPlan: number; }>` SELECT latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", has_actionable_proposed_plan AS "hasActionableProposedPlan" FROM projection_threads WHERE thread_id = 'thread-1' `; assert.deepStrictEqual(threadRows, [ { latestUserMessageAt: "2026-02-24T00:01:00.000Z", pendingApprovalCount: 0, pendingUserInputCount: 1, hasActionableProposedPlan: 1, }, ]); const approvalRows = yield* sql<{ readonly status: string; readonly resolvedAt: string | null; }>` SELECT status, resolved_at AS "resolvedAt" FROM projection_pending_approvals WHERE request_id = 'approval-1' `; assert.deepStrictEqual(approvalRows, [ { status: "resolved", resolvedAt: "2026-02-24T00:03:00.000Z", }, ]); }), );});
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as SqlClient from "effect/unstable/sql/SqlClient";
import { runMigrations } from "../Migrations.ts";
import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
layer("024_BackfillProjectionThreadShellSummary", (it) => {
it.effect("backfills thread shell summary fields and clears stale projected approvals", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* runMigrations({ toMigrationInclusive: 23 });
yield* sql`
INSERT INTO projection_threads (
thread_id,
project_id,
title,
model_selection_json,
runtime_mode,
interaction_mode,
branch,
worktree_path,
latest_turn_id,
created_at,
updated_at,
archived_at,
latest_user_message_at,
pending_approval_count,
pending_user_input_count,
has_actionable_proposed_plan,
deleted_at
)
VALUES (
'thread-1',
'project-1',
'Thread 1',
'{"provider":"codex","model":"gpt-5-codex"}',
'approval-required',
'plan',
NULL,
NULL,
'turn-1',
'2026-02-24T00:00:00.000Z',
'2026-02-24T00:00:00.000Z',
NULL,
NULL,
0,
0,
0,
NULL
)
`;
yield* sql`
INSERT INTO projection_thread_messages (
message_id,
thread_id,
turn_id,
role,
text,
attachments_json,
is_streaming,
created_at,
updated_at
)
VALUES (
'message-user-1',
'thread-1',
'turn-1',
'user',
'Need help',
NULL,
0,
'2026-02-24T00:01:00.000Z',
'2026-02-24T00:01:00.000Z'
)
`;
yield* sql`
INSERT INTO projection_thread_activities (
activity_id,
thread_id,
turn_id,
tone,
kind,
summary,
payload_json,
sequence,
created_at
)
VALUES
(
'activity-approval-requested',
'thread-1',
'turn-1',
'approval',
'approval.requested',
'Command approval requested',
'{"requestId":"approval-1","requestKind":"command"}',
NULL,
'2026-02-24T00:02:00.000Z'
),
(
'activity-approval-stale',
'thread-1',
'turn-1',
'error',
'provider.approval.respond.failed',
'Provider approval response failed',
'{"requestId":"approval-1","detail":"Unknown pending permission request: approval-1"}',
NULL,
'2026-02-24T00:03:00.000Z'
),
(
'activity-user-input-requested',
'thread-1',
'turn-1',
'info',
'user-input.requested',
'User input requested',
'{"requestId":"input-1","questions":[{"id":"area","header":"Area","question":"Which repo area should I inspect next?","options":[{"label":"Server","description":"Server orchestration."}]}]}',
NULL,
'2026-02-24T00:04:00.000Z'
)
`;
yield* sql`
INSERT INTO projection_thread_proposed_plans (
plan_id,
thread_id,
turn_id,
plan_markdown,
implemented_at,
implementation_thread_id,
created_at,
updated_at
)
VALUES (
'plan-1',
'thread-1',
'turn-1',
'# Do the thing',
NULL,
NULL,
'2026-02-24T00:05:00.000Z',
'2026-02-24T00:05:00.000Z'
)
`;
yield* sql`
INSERT INTO projection_pending_approvals (
request_id,
thread_id,
turn_id,
status,
decision,
created_at,
resolved_at
)
VALUES (
'approval-1',
'thread-1',
'turn-1',
'pending',
NULL,
'2026-02-24T00:02:00.000Z',
NULL
)
`;
yield* runMigrations({ toMigrationInclusive: 24 });
const threadRows = yield* sql<{
readonly latestUserMessageAt: string | null;
readonly pendingApprovalCount: number;
readonly pendingUserInputCount: number;
readonly hasActionableProposedPlan: number;
}>`
SELECT
latest_user_message_at AS "latestUserMessageAt",
pending_approval_count AS "pendingApprovalCount",
pending_user_input_count AS "pendingUserInputCount",
has_actionable_proposed_plan AS "hasActionableProposedPlan"
FROM projection_threads
WHERE thread_id = 'thread-1'
`;
assert.deepStrictEqual(threadRows, [
{
latestUserMessageAt: "2026-02-24T00:01:00.000Z",
pendingApprovalCount: 0,
pendingUserInputCount: 1,
hasActionableProposedPlan: 1,
},
]);
const approvalRows = yield* sql<{
readonly status: string;
readonly resolvedAt: string | null;
}>`
SELECT
status,
resolved_at AS "resolvedAt"
FROM projection_pending_approvals
WHERE request_id = 'approval-1'
`;
assert.deepStrictEqual(approvalRows, [
{
status: "resolved",
resolvedAt: "2026-02-24T00:03:00.000Z",
},
]);
}),
);
});