301 lines · 8.7 KB · TypeScript
RawDownload
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("042_ProjectionThreadShellSummaryState", (it) => {  it.effect("upgrades recorded migration 41 and reconciles shell summaries", () =>    Effect.gen(function* () {      const sql = yield* SqlClient.SqlClient;       yield* runMigrations({ toMigrationInclusive: 41 });      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-cleared',            'project-1',            'Cleared',            '{"provider":"codex","model":"gpt-5-codex"}',            'approval-required',            'default',            NULL,            NULL,            NULL,            '2026-08-19T00:00:00.000Z',            '2026-08-19T00:00:00.000Z',            NULL,            '2026-08-19T00:00:05.000Z',            0,            1,            0,            NULL          ),          (            'thread-pending',            'project-1',            'Pending',            '{"provider":"codex","model":"gpt-5-codex"}',            'approval-required',            'default',            NULL,            NULL,            NULL,            '2026-08-19T00:00:00.000Z',            '2026-08-19T00:00:00.000Z',            NULL,            NULL,            0,            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          (            'A',            'thread-cleared',            NULL,            'info',            'user-input.requested',            'Tie requested',            '{"requestId":"request-tie"}',            NULL,            '2026-08-19T00:00:02.000Z'          ),          (            'a',            'thread-cleared',            NULL,            'info',            'user-input.resolved',            'Tie resolved',            '{"requestId":"request-tie"}',            NULL,            '2026-08-19T00:00:02.000Z'          ),          (            'activity-pending',            'thread-pending',            NULL,            'info',            'user-input.requested',            'Pending request',            '{"requestId":"request-pending"}',            NULL,            '2026-08-19T00:00:01.000Z'          ),          (            'activity-object-detail',            'thread-pending',            NULL,            'error',            'provider.user-input.respond.failed',            'Object detail',            '{"requestId":"request-pending","detail":{"message":"stale pending user-input request"}}',            NULL,            '2026-08-19T00:00:02.000Z'          ),          (            'activity-malformed',            'thread-pending',            NULL,            'info',            'user-input.requested',            'Malformed',            '{',            NULL,            '2026-08-19T00:00:03.000Z'          )      `;       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-assistant',            'thread-cleared',            NULL,            'assistant',            'No user message remains',            NULL,            0,            '2026-08-19T00:00:05.000Z',            '2026-08-19T00:00:05.000Z'          ),          (            'message-user',            'thread-pending',            NULL,            'user',            'Latest user message',            NULL,            0,            '2026-08-19T00:00:06.000Z',            '2026-08-19T00:00:06.000Z'          )      `;       yield* runMigrations({ toMigrationInclusive: 42 });       const normalizedRows = yield* sql<{        readonly activityId: string;        readonly requestId: string | null;        readonly state: string | null;      }>`        SELECT          activity_id AS "activityId",          user_input_request_id AS "requestId",          user_input_state AS "state"        FROM projection_thread_activities        WHERE activity_id IN ('activity-malformed', 'activity-object-detail')        ORDER BY activity_id      `;      assert.deepStrictEqual(normalizedRows, [        { activityId: "activity-malformed", requestId: null, state: null },        { activityId: "activity-object-detail", requestId: null, state: null },      ]);       const summaryRows = yield* sql<{        readonly threadId: string;        readonly pendingCount: number;      }>`        SELECT          thread_id AS "threadId",          pending_count AS "pendingCount"        FROM projection_thread_user_input_summaries        ORDER BY thread_id      `;      assert.deepStrictEqual(summaryRows, [        { threadId: "thread-cleared", pendingCount: 0 },        { threadId: "thread-pending", pendingCount: 1 },      ]);       const threadRows = yield* sql<{        readonly threadId: string;        readonly latestUserMessageAt: string | null;        readonly pendingUserInputCount: number;      }>`        SELECT          thread_id AS "threadId",          latest_user_message_at AS "latestUserMessageAt",          pending_user_input_count AS "pendingUserInputCount"        FROM projection_threads        ORDER BY thread_id      `;      assert.deepStrictEqual(threadRows, [        {          threadId: "thread-cleared",          latestUserMessageAt: null,          pendingUserInputCount: 0,        },        {          threadId: "thread-pending",          latestUserMessageAt: "2026-08-19T00:00:06.000Z",          pendingUserInputCount: 1,        },      ]);       const legacyIndexes = yield* sql<{ readonly name: string }>`        SELECT name        FROM sqlite_master        WHERE type = 'index'          AND name = 'idx_projection_thread_activities_thread_user_input_lifecycle'      `;      assert.deepStrictEqual(legacyIndexes, []);       const lifecycleIndexColumns = yield* sql<{ readonly name: string }>`        PRAGMA index_info('idx_projection_thread_activities_user_input_state')      `;      assert.deepStrictEqual(        lifecycleIndexColumns.map((column) => column.name),        ["thread_id", "user_input_request_id", "created_at", "activity_id"],      );       const lifecyclePlan = yield* sql<{ readonly detail: string }>`        EXPLAIN QUERY PLAN        SELECT user_input_state        FROM projection_thread_activities        WHERE thread_id = 'thread-pending'          AND user_input_request_id = 'request-pending'          AND user_input_state IS NOT NULL        ORDER BY created_at DESC, activity_id DESC        LIMIT 1      `;      const lifecyclePlanText = lifecyclePlan.map((row) => row.detail).join("\n");      assert.match(        lifecyclePlanText,        /USING INDEX idx_projection_thread_activities_user_input_state/,      );      assert.notMatch(lifecyclePlanText, /USE TEMP B-TREE/);       const summaryPlan = yield* sql<{ readonly detail: string }>`        EXPLAIN QUERY PLAN        SELECT pending_count        FROM projection_thread_user_input_summaries        WHERE thread_id = 'thread-pending'        LIMIT 1      `;      assert.match(summaryPlan.map((row) => row.detail).join("\n"), /USING INDEX/);       const messagePlan = yield* sql<{ readonly detail: string }>`        EXPLAIN QUERY PLAN        SELECT created_at        FROM projection_thread_messages        WHERE thread_id = 'thread-pending'          AND role = 'user'        ORDER BY created_at DESC, message_id DESC        LIMIT 1      `;      const messagePlanText = messagePlan.map((row) => row.detail).join("\n");      assert.match(        messagePlanText,        /USING COVERING INDEX idx_projection_thread_messages_latest_user/,      );      assert.notMatch(messagePlanText, /USE TEMP B-TREE/);    }),  );});