import { assert, it } from "@effect/vitest";import { Effect, Layer } from "effect";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("016_CanonicalizeModelSelections", (it) => { it.effect( "migrates legacy projection rows and event payloads to the canonical model-selection shape", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; // Setup base state { yield* runMigrations({ toMigrationInclusive: 15 }); yield* sql` INSERT INTO projection_projects ( project_id, title, workspace_root, default_model, scripts_json, created_at, updated_at, deleted_at ) VALUES ('project-codex', 'Codex project', '/tmp/project-codex', 'gpt-5.4', '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL), ('project-claude', 'Claude project', '/tmp/project-claude', 'claude-sonnet-4-6', '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL), ('project-null', 'Null project', '/tmp/project-null', NULL, '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL) `; yield* sql` UPDATE projection_projects SET default_model = 'claude-opus-4-6' WHERE project_id = 'project-claude' `; yield* sql` INSERT INTO projection_threads ( thread_id, project_id, title, model, branch, worktree_path, latest_turn_id, created_at, updated_at, deleted_at, runtime_mode, interaction_mode ) VALUES ('thread-session', 'project-codex', 'Session thread', 'gpt-5.4', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default'), ('thread-claude', 'project-claude', 'Claude thread', 'claude-opus-4-6', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default'), ('thread-codex', 'project-codex', 'Codex thread', 'gpt-5.4', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default'), ('thread-legacy-options', 'project-claude', 'Legacy options thread', 'claude-opus-4-6', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default') `; yield* sql` INSERT INTO projection_thread_sessions ( thread_id, status, provider_name, provider_session_id, provider_thread_id, active_turn_id, last_error, updated_at, runtime_mode ) VALUES ( 'thread-session', 'running', 'claudeAgent', 'provider-session-1', 'provider-thread-1', NULL, NULL, '2026-01-01T00:00:00.000Z', 'full-access' ) `; yield* sql` INSERT INTO orchestration_events ( event_id, aggregate_kind, stream_id, stream_version, event_type, occurred_at, command_id, causation_event_id, correlation_id, actor_kind, payload_json, metadata_json ) VALUES ( 'event-project-created', 'project', 'project-1', 1, 'project.created', '2026-01-01T00:00:00.000Z', 'command-project-created', NULL, 'correlation-project-created', 'user', '{"projectId":"project-1","title":"Project","workspaceRoot":"/tmp/project","defaultModel":"claude-opus-4-6","defaultModelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max"}},"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}', '{}' ), ( 'event-project-created-fallback', 'project', 'project-2', 1, 'project.created', '2026-01-01T00:00:00.000Z', 'command-project-created-fallback', NULL, 'correlation-project-created-fallback', 'user', '{"projectId":"project-2","title":"Fallback Project","workspaceRoot":"/tmp/project-2","defaultModel":"claude-opus-4-6","defaultModelOptions":{"codex":{"reasoningEffort":"low"}},"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}', '{}' ), ( 'event-project-created-null-model', 'project', 'project-3', 1, 'project.created', '2026-01-01T00:00:00.000Z', 'command-project-created-null-model', NULL, 'correlation-project-created-null-model', 'user', '{"projectId":"project-3","title":"Null Model Project","workspaceRoot":"/tmp/project-3","defaultModel":null,"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}', '{}' ), ( 'event-thread-created', 'thread', 'thread-1', 1, 'thread.created', '2026-01-01T00:00:00.000Z', 'command-thread-created', NULL, 'correlation-thread-created', 'user', '{"threadId":"thread-1","projectId":"project-1","title":"Thread","model":"claude-opus-4-6","modelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max","thinking":false}},"runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}', '{}' ), ( 'event-thread-created-fallback', 'thread', 'thread-2', 1, 'thread.created', '2026-01-01T00:00:00.000Z', 'command-thread-created-fallback', NULL, 'correlation-thread-created-fallback', 'user', '{"threadId":"thread-2","projectId":"project-1","title":"Fallback Thread","model":"gpt-5.4","modelOptions":{"claudeAgent":{"effort":"max"}},"runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}', '{}' ), ( 'event-turn-start-requested', 'thread', 'thread-1', 2, 'thread.turn-start-requested', '2026-01-01T00:00:00.000Z', 'command-turn-start-requested', NULL, 'correlation-turn-start-requested', 'user', '{"threadId":"thread-1","turnId":"turn-1","input":"hi","model":"gpt-5.4","modelOptions":{"codex":{"fastMode":true},"claudeAgent":{"effort":"max"}},"deliveryMode":"buffered"}', '{}' ), ( 'event-thread-created-no-model', 'thread', 'thread-3', 1, 'thread.created', '2026-01-01T00:00:00.000Z', 'command-thread-created-no-model', NULL, 'correlation-thread-created-no-model', 'user', '{"threadId":"thread-3","projectId":"project-1","title":"Ancient Thread","runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}', '{}' ) `; } // Execute migration under test yield* runMigrations({ toMigrationInclusive: 16 }); // Assert expected state { const projectRows = yield* sql<{ readonly projectId: string; readonly defaultModelSelection: string | null; }>` SELECT project_id AS "projectId", default_model_selection_json AS "defaultModelSelection" FROM projection_projects ORDER BY project_id `; assert.deepStrictEqual(projectRows, [ { projectId: "project-claude", defaultModelSelection: '{"provider":"claudeAgent","model":"claude-opus-4-6"}', }, { projectId: "project-codex", defaultModelSelection: '{"provider":"codex","model":"gpt-5.4"}', }, { projectId: "project-null", defaultModelSelection: null }, ]); const threadRows = yield* sql<{ readonly threadId: string; readonly modelSelection: string | null; }>` SELECT thread_id AS "threadId", model_selection_json AS "modelSelection" FROM projection_threads ORDER BY thread_id `; assert.deepStrictEqual(threadRows, [ { threadId: "thread-claude", modelSelection: '{"provider":"claudeAgent","model":"claude-opus-4-6"}', }, { threadId: "thread-codex", modelSelection: '{"provider":"codex","model":"gpt-5.4"}', }, { threadId: "thread-legacy-options", modelSelection: '{"provider":"claudeAgent","model":"claude-opus-4-6"}', }, { threadId: "thread-session", modelSelection: '{"provider":"claudeAgent","model":"gpt-5.4"}', }, ]); const eventRows = yield* sql<{ readonly payloadJson: string; }>` SELECT payload_json AS "payloadJson" FROM orchestration_events ORDER BY rowid ASC `; assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), { projectId: "project-1", title: "Project", workspaceRoot: "/tmp/project", defaultModelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", options: { effort: "max", }, }, scripts: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), { projectId: "project-2", title: "Fallback Project", workspaceRoot: "/tmp/project-2", defaultModelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", options: { reasoningEffort: "low", }, }, scripts: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), { projectId: "project-3", title: "Null Model Project", workspaceRoot: "/tmp/project-3", defaultModelSelection: null, scripts: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); assert.deepStrictEqual(JSON.parse(eventRows[3]!.payloadJson), { threadId: "thread-1", projectId: "project-1", title: "Thread", modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", options: { effort: "max", thinking: false, }, }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); assert.deepStrictEqual(JSON.parse(eventRows[4]!.payloadJson), { threadId: "thread-2", projectId: "project-1", title: "Fallback Thread", modelSelection: { provider: "codex", model: "gpt-5.4", options: { effort: "max", }, }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); assert.deepStrictEqual(JSON.parse(eventRows[5]!.payloadJson), { threadId: "thread-1", turnId: "turn-1", input: "hi", modelSelection: { provider: "codex", model: "gpt-5.4", options: { fastMode: true, }, }, deliveryMode: "buffered", }); assert.deepStrictEqual(JSON.parse(eventRows[6]!.payloadJson), { threadId: "thread-3", projectId: "project-1", title: "Ancient Thread", modelSelection: { provider: "codex", model: "gpt-5.4", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); } }), );});
import { assert, it } from "@effect/vitest";
import { Effect, Layer } from "effect";
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("016_CanonicalizeModelSelections", (it) => {
it.effect(
"migrates legacy projection rows and event payloads to the canonical model-selection shape",
() =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
// Setup base state
{
yield* runMigrations({ toMigrationInclusive: 15 });
yield* sql`
INSERT INTO projection_projects (
project_id,
title,
workspace_root,
default_model,
scripts_json,
created_at,
updated_at,
deleted_at
)
VALUES
('project-codex', 'Codex project', '/tmp/project-codex', 'gpt-5.4', '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL),
('project-claude', 'Claude project', '/tmp/project-claude', 'claude-sonnet-4-6', '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL),
('project-null', 'Null project', '/tmp/project-null', NULL, '[]', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL)
`;
yield* sql`
UPDATE projection_projects
SET default_model = 'claude-opus-4-6'
WHERE project_id = 'project-claude'
`;
yield* sql`
INSERT INTO projection_threads (
thread_id,
project_id,
title,
model,
branch,
worktree_path,
latest_turn_id,
created_at,
updated_at,
deleted_at,
runtime_mode,
interaction_mode
)
VALUES
('thread-session', 'project-codex', 'Session thread', 'gpt-5.4', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default'),
('thread-claude', 'project-claude', 'Claude thread', 'claude-opus-4-6', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default'),
('thread-codex', 'project-codex', 'Codex thread', 'gpt-5.4', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default'),
('thread-legacy-options', 'project-claude', 'Legacy options thread', 'claude-opus-4-6', NULL, NULL, NULL, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z', NULL, 'full-access', 'default')
`;
yield* sql`
INSERT INTO projection_thread_sessions (
thread_id,
status,
provider_name,
provider_session_id,
provider_thread_id,
active_turn_id,
last_error,
updated_at,
runtime_mode
)
VALUES (
'thread-session',
'running',
'claudeAgent',
'provider-session-1',
'provider-thread-1',
NULL,
NULL,
'2026-01-01T00:00:00.000Z',
'full-access'
)
`;
yield* sql`
INSERT INTO orchestration_events (
event_id,
aggregate_kind,
stream_id,
stream_version,
event_type,
occurred_at,
command_id,
causation_event_id,
correlation_id,
actor_kind,
payload_json,
metadata_json
)
VALUES
(
'event-project-created',
'project',
'project-1',
1,
'project.created',
'2026-01-01T00:00:00.000Z',
'command-project-created',
NULL,
'correlation-project-created',
'user',
'{"projectId":"project-1","title":"Project","workspaceRoot":"/tmp/project","defaultModel":"claude-opus-4-6","defaultModelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max"}},"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}',
'{}'
),
(
'event-project-created-fallback',
'project',
'project-2',
1,
'project.created',
'2026-01-01T00:00:00.000Z',
'command-project-created-fallback',
NULL,
'correlation-project-created-fallback',
'user',
'{"projectId":"project-2","title":"Fallback Project","workspaceRoot":"/tmp/project-2","defaultModel":"claude-opus-4-6","defaultModelOptions":{"codex":{"reasoningEffort":"low"}},"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}',
'{}'
),
(
'event-project-created-null-model',
'project',
'project-3',
1,
'project.created',
'2026-01-01T00:00:00.000Z',
'command-project-created-null-model',
NULL,
'correlation-project-created-null-model',
'user',
'{"projectId":"project-3","title":"Null Model Project","workspaceRoot":"/tmp/project-3","defaultModel":null,"scripts":[],"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}',
'{}'
),
(
'event-thread-created',
'thread',
'thread-1',
1,
'thread.created',
'2026-01-01T00:00:00.000Z',
'command-thread-created',
NULL,
'correlation-thread-created',
'user',
'{"threadId":"thread-1","projectId":"project-1","title":"Thread","model":"claude-opus-4-6","modelOptions":{"codex":{"reasoningEffort":"high"},"claudeAgent":{"effort":"max","thinking":false}},"runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}',
'{}'
),
(
'event-thread-created-fallback',
'thread',
'thread-2',
1,
'thread.created',
'2026-01-01T00:00:00.000Z',
'command-thread-created-fallback',
NULL,
'correlation-thread-created-fallback',
'user',
'{"threadId":"thread-2","projectId":"project-1","title":"Fallback Thread","model":"gpt-5.4","modelOptions":{"claudeAgent":{"effort":"max"}},"runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}',
'{}'
),
(
'event-turn-start-requested',
'thread',
'thread-1',
2,
'thread.turn-start-requested',
'2026-01-01T00:00:00.000Z',
'command-turn-start-requested',
NULL,
'correlation-turn-start-requested',
'user',
'{"threadId":"thread-1","turnId":"turn-1","input":"hi","model":"gpt-5.4","modelOptions":{"codex":{"fastMode":true},"claudeAgent":{"effort":"max"}},"deliveryMode":"buffered"}',
'{}'
),
(
'event-thread-created-no-model',
'thread',
'thread-3',
1,
'thread.created',
'2026-01-01T00:00:00.000Z',
'command-thread-created-no-model',
NULL,
'correlation-thread-created-no-model',
'user',
'{"threadId":"thread-3","projectId":"project-1","title":"Ancient Thread","runtimeMode":"full-access","interactionMode":"default","branch":null,"worktreePath":null,"createdAt":"2026-01-01T00:00:00.000Z","updatedAt":"2026-01-01T00:00:00.000Z"}',
'{}'
)
`;
}
// Execute migration under test
yield* runMigrations({ toMigrationInclusive: 16 });
// Assert expected state
{
const projectRows = yield* sql<{
readonly projectId: string;
readonly defaultModelSelection: string | null;
}>`
SELECT
project_id AS "projectId",
default_model_selection_json AS "defaultModelSelection"
FROM projection_projects
ORDER BY project_id
`;
assert.deepStrictEqual(projectRows, [
{
projectId: "project-claude",
defaultModelSelection: '{"provider":"claudeAgent","model":"claude-opus-4-6"}',
},
{
projectId: "project-codex",
defaultModelSelection: '{"provider":"codex","model":"gpt-5.4"}',
},
{ projectId: "project-null", defaultModelSelection: null },
]);
const threadRows = yield* sql<{
readonly threadId: string;
readonly modelSelection: string | null;
}>`
SELECT
thread_id AS "threadId",
model_selection_json AS "modelSelection"
FROM projection_threads
ORDER BY thread_id
`;
assert.deepStrictEqual(threadRows, [
{
threadId: "thread-claude",
modelSelection: '{"provider":"claudeAgent","model":"claude-opus-4-6"}',
},
{
threadId: "thread-codex",
modelSelection: '{"provider":"codex","model":"gpt-5.4"}',
},
{
threadId: "thread-legacy-options",
modelSelection: '{"provider":"claudeAgent","model":"claude-opus-4-6"}',
},
{
threadId: "thread-session",
modelSelection: '{"provider":"claudeAgent","model":"gpt-5.4"}',
},
]);
const eventRows = yield* sql<{
readonly payloadJson: string;
}>`
SELECT payload_json AS "payloadJson"
FROM orchestration_events
ORDER BY rowid ASC
`;
assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), {
projectId: "project-1",
title: "Project",
workspaceRoot: "/tmp/project",
defaultModelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
effort: "max",
},
},
scripts: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), {
projectId: "project-2",
title: "Fallback Project",
workspaceRoot: "/tmp/project-2",
defaultModelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
reasoningEffort: "low",
},
},
scripts: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), {
projectId: "project-3",
title: "Null Model Project",
workspaceRoot: "/tmp/project-3",
defaultModelSelection: null,
scripts: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
assert.deepStrictEqual(JSON.parse(eventRows[3]!.payloadJson), {
threadId: "thread-1",
projectId: "project-1",
title: "Thread",
modelSelection: {
provider: "claudeAgent",
model: "claude-opus-4-6",
options: {
effort: "max",
thinking: false,
},
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
assert.deepStrictEqual(JSON.parse(eventRows[4]!.payloadJson), {
threadId: "thread-2",
projectId: "project-1",
title: "Fallback Thread",
modelSelection: {
provider: "codex",
model: "gpt-5.4",
options: {
effort: "max",
},
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
assert.deepStrictEqual(JSON.parse(eventRows[5]!.payloadJson), {
threadId: "thread-1",
turnId: "turn-1",
input: "hi",
modelSelection: {
provider: "codex",
model: "gpt-5.4",
options: {
fastMode: true,
},
},
deliveryMode: "buffered",
});
assert.deepStrictEqual(JSON.parse(eventRows[6]!.payloadJson), {
threadId: "thread-3",
projectId: "project-1",
title: "Ancient Thread",
modelSelection: {
provider: "codex",
model: "gpt-5.4",
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
}
}),
);
});