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 "../NodeSqliteClient.ts"; const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); layer("025_CleanupInvalidProjectionPendingApprovals", (it) => { it.effect("removes pending-approval rows that do not come from approval requests", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* runMigrations({ toMigrationInclusive: 24 }); 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-valid', 'project-1', 'Valid thread', '{"provider":"codex","model":"gpt-5-codex"}', 'approval-required', 'default', NULL, NULL, 'turn-valid', '2026-04-13T00:00:00.000Z', '2026-04-13T00:00:00.000Z', NULL, NULL, 2, 0, 0, NULL ), ( 'thread-invalid', 'project-1', 'Invalid thread', '{"provider":"codex","model":"gpt-5-codex"}', 'approval-required', 'default', NULL, NULL, 'turn-invalid', '2026-04-13T00:00:00.000Z', '2026-04-13T00:00:00.000Z', NULL, NULL, 1, 0, 0, NULL ) `; 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-valid', 'turn-valid', 'approval', 'approval.requested', 'Command approval requested', '{"requestId":"approval-valid","requestKind":"command"}', NULL, '2026-04-13T00:01:00.000Z' ), ( 'activity-user-input-requested', 'thread-invalid', 'turn-invalid', 'info', 'user-input.requested', 'User input requested', '{"requestId":"input-invalid","questions":[{"id":"scope","header":"Scope","question":"What should I inspect?","options":[{"label":"Server","description":"Inspect server code."}]}]}', NULL, '2026-04-13T00:02:00.000Z' ) `; yield* sql` INSERT INTO projection_pending_approvals ( request_id, thread_id, turn_id, status, decision, created_at, resolved_at ) VALUES ( 'approval-valid', 'thread-valid', 'turn-valid', 'pending', NULL, '2026-04-13T00:01:00.000Z', NULL ), ( 'input-invalid', 'thread-invalid', 'turn-invalid', 'pending', NULL, '2026-04-13T00:02:00.000Z', NULL ), ( 'input-invalid-resolved', 'thread-valid', 'turn-valid', 'resolved', NULL, '2026-04-13T00:03:00.000Z', '2026-04-13T00:04:00.000Z' ) `; yield* runMigrations({ toMigrationInclusive: 25 }); const approvalRows = yield* sql<{ readonly requestId: string; readonly status: string; }>` SELECT request_id AS "requestId", status FROM projection_pending_approvals ORDER BY request_id ASC `; assert.deepStrictEqual(approvalRows, [ { requestId: "approval-valid", status: "pending", }, ]); const threadCounts = yield* sql<{ readonly threadId: string; readonly pendingApprovalCount: number; }>` SELECT thread_id AS "threadId", pending_approval_count AS "pendingApprovalCount" FROM projection_threads ORDER BY thread_id ASC `; assert.deepStrictEqual(threadCounts, [ { threadId: "thread-invalid", pendingApprovalCount: 0, }, { threadId: "thread-valid", pendingApprovalCount: 1, }, ]); }), );});
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 "../NodeSqliteClient.ts";
const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
layer("025_CleanupInvalidProjectionPendingApprovals", (it) => {
it.effect("removes pending-approval rows that do not come from approval requests", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* runMigrations({ toMigrationInclusive: 24 });
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-valid',
'project-1',
'Valid thread',
'{"provider":"codex","model":"gpt-5-codex"}',
'approval-required',
'default',
NULL,
NULL,
'turn-valid',
'2026-04-13T00:00:00.000Z',
'2026-04-13T00:00:00.000Z',
NULL,
NULL,
2,
0,
0,
NULL
),
(
'thread-invalid',
'project-1',
'Invalid thread',
'{"provider":"codex","model":"gpt-5-codex"}',
'approval-required',
'default',
NULL,
NULL,
'turn-invalid',
'2026-04-13T00:00:00.000Z',
'2026-04-13T00:00:00.000Z',
NULL,
NULL,
1,
0,
0,
NULL
)
`;
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-valid',
'turn-valid',
'approval',
'approval.requested',
'Command approval requested',
'{"requestId":"approval-valid","requestKind":"command"}',
NULL,
'2026-04-13T00:01:00.000Z'
),
(
'activity-user-input-requested',
'thread-invalid',
'turn-invalid',
'info',
'user-input.requested',
'User input requested',
'{"requestId":"input-invalid","questions":[{"id":"scope","header":"Scope","question":"What should I inspect?","options":[{"label":"Server","description":"Inspect server code."}]}]}',
NULL,
'2026-04-13T00:02:00.000Z'
)
`;
yield* sql`
INSERT INTO projection_pending_approvals (
request_id,
thread_id,
turn_id,
status,
decision,
created_at,
resolved_at
)
VALUES
(
'approval-valid',
'thread-valid',
'turn-valid',
'pending',
NULL,
'2026-04-13T00:01:00.000Z',
NULL
),
(
'input-invalid',
'thread-invalid',
'turn-invalid',
'pending',
NULL,
'2026-04-13T00:02:00.000Z',
NULL
),
(
'input-invalid-resolved',
'thread-valid',
'turn-valid',
'resolved',
NULL,
'2026-04-13T00:03:00.000Z',
'2026-04-13T00:04:00.000Z'
)
`;
yield* runMigrations({ toMigrationInclusive: 25 });
const approvalRows = yield* sql<{
readonly requestId: string;
readonly status: string;
}>`
SELECT
request_id AS "requestId",
status
FROM projection_pending_approvals
ORDER BY request_id ASC
`;
assert.deepStrictEqual(approvalRows, [
{
requestId: "approval-valid",
status: "pending",
},
]);
const threadCounts = yield* sql<{
readonly threadId: string;
readonly pendingApprovalCount: number;
}>`
SELECT
thread_id AS "threadId",
pending_approval_count AS "pendingApprovalCount"
FROM projection_threads
ORDER BY thread_id ASC
`;
assert.deepStrictEqual(threadCounts, [
{
threadId: "thread-invalid",
pendingApprovalCount: 0,
},
{
threadId: "thread-valid",
pendingApprovalCount: 1,
},
]);
}),
);
});