2026-05-31 01:08:38 +00:00
import { describe , expect , test } from "bun:test"
import { $ } from "bun"
import { fileURLToPath } from "url"
2026-06-04 03:02:17 +00:00
import path from "path"
2026-05-31 01:08:38 +00:00
import { SqliteClient } from "@effect/sql-sqlite-bun"
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
2026-06-04 03:02:17 +00:00
import { Effect , Layer } from "effect"
2026-06-02 00:54:41 +00:00
import { eq , inArray , sql } from "drizzle-orm"
2026-05-31 01:08:38 +00:00
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
2026-06-05 03:26:43 +00:00
import { migrations } from "@opencode-ai/core/database/migration.gen"
2026-05-31 01:08:38 +00:00
import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage"
2026-06-02 00:54:41 +00:00
import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths"
2026-06-04 03:02:17 +00:00
import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
2026-06-04 23:24:30 +00:00
import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input"
2026-06-05 15:19:55 +00:00
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
2026-06-12 06:45:25 +00:00
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
2026-06-22 21:51:49 +00:00
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
2026-06-29 15:35:17 +00:00
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
2026-06-22 19:39:41 +00:00
import { EventV2 } from "@opencode-ai/core/event"
2026-06-02 00:54:41 +00:00
import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionSchema } from "@opencode-ai/core/session/schema"
import { SessionTable } from "@opencode-ai/core/session/sql"
2026-06-01 04:50:43 +00:00
import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata"
2026-05-31 01:08:38 +00:00
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
2026-06-04 03:02:17 +00:00
import { Database } from "@opencode-ai/core/database/database"
2026-06-22 19:39:41 +00:00
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionV1 } from "@opencode-ai/core/v1/session"
2026-06-04 03:02:17 +00:00
import { tmpdir } from "./fixture/tmpdir"
2026-05-31 01:08:38 +00:00
const run = < A , E > ( effect : Effect.Effect < A , E , SqlClientService > ) = >
2026-05-31 01:09:55 +00:00
Effect . runPromise (
effect . pipe ( Effect . provide ( SqliteClient . layer ( { filename : ":memory:" , disableWAL : true } ) ) , Effect . scoped ) ,
)
2026-05-31 01:08:38 +00:00
const makeDb = EffectDrizzleSqlite . makeWithDefaults ( )
describe ( "DatabaseMigration" , ( ) = > {
2026-06-04 03:02:17 +00:00
test ( "serializes concurrent embedded initialization for one database path" , async ( ) = > {
await using tmp = await tmpdir ( )
const filename = path . join ( tmp . path , "embedded.sqlite" )
const layers = [ Database . layerFromPath ( filename ) , Database . layerFromPath ( filename ) ]
await Effect . runPromise (
2026-06-04 03:03:39 +00:00
Effect . all (
layers . map ( ( layer ) = > Effect . scoped ( Layer . build ( layer ) ) ) ,
{ concurrency : "unbounded" } ,
) ,
2026-06-04 03:02:17 +00:00
)
} )
2026-05-31 01:08:38 +00:00
if ( process . platform === "linux" ) {
test ( "declared schema has no ungenerated migrations" , async ( ) = > {
2026-05-31 01:09:55 +00:00
const result = await $ ` bun ${ fileURLToPath ( new URL ( "../script/migration.ts" , import . meta . url ) ) } --check `
. quiet ( )
. nothrow ( )
2026-05-31 01:08:38 +00:00
expect ( result . exitCode , result . stderr . toString ( ) ) . toBe ( 0 )
expect ( result . stdout . toString ( ) ) . toContain ( "No schema changes, nothing to migrate" )
} , 30 _000 )
}
test ( "applies tracked migrations to an empty database" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * DatabaseMigration . apply ( db )
expect ( yield * db . get ( sql ` SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session' ` ) ) . toEqual ( {
name : "session" ,
} )
2026-06-04 03:02:17 +00:00
expect (
yield * db . get ( sql ` SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_input' ` ) ,
) . toEqual ( { name : "session_input" } )
2026-06-05 03:26:43 +00:00
expect (
yield * db . get ( sql ` SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_context_epoch' ` ) ,
) . toEqual ( { name : "session_context_epoch" } )
2026-06-05 15:19:55 +00:00
expect (
yield * db . get (
2026-06-22 15:34:03 +00:00
sql ` SELECT name FROM pragma_table_info('session_context_epoch') WHERE name IN ('agent', 'replacement_seq', 'revision') ` ,
2026-06-05 15:19:55 +00:00
) ,
2026-06-22 15:34:03 +00:00
) . toBeUndefined ( )
2026-06-05 03:26:43 +00:00
expect ( yield * db . get ( sql ` SELECT count(*) as count FROM migration ` ) ) . toEqual ( { count : migrations.length } )
2026-06-04 03:02:17 +00:00
expect (
yield * db . all (
2026-06-04 23:24:30 +00:00
sql ` SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('event_aggregate_seq_idx', 'event_aggregate_type_seq_idx', 'session_input_session_pending_seq_idx', 'session_input_session_pending_delivery_seq_idx', 'session_input_session_admitted_seq_idx', 'session_input_session_promoted_seq_idx', 'session_message_session_idx', 'session_message_session_type_idx', 'session_message_session_seq_idx', 'session_message_session_type_seq_idx', 'session_message_session_time_created_id_idx') ORDER BY name ` ,
2026-06-04 03:02:17 +00:00
) ,
) . toEqual ( [
{ name : "event_aggregate_seq_idx" } ,
{ name : "event_aggregate_type_seq_idx" } ,
2026-06-04 23:24:30 +00:00
{ name : "session_input_session_admitted_seq_idx" } ,
2026-06-04 03:02:17 +00:00
{ name : "session_input_session_pending_delivery_seq_idx" } ,
2026-06-04 23:24:30 +00:00
{ name : "session_input_session_promoted_seq_idx" } ,
2026-06-04 03:02:17 +00:00
{ name : "session_message_session_seq_idx" } ,
{ name : "session_message_session_time_created_id_idx" } ,
{ name : "session_message_session_type_seq_idx" } ,
] )
2026-05-31 01:08:38 +00:00
} ) ,
)
} )
2026-06-12 06:15:25 +00:00
test ( "rejects a non-empty database without a session table" , async ( ) = > {
await expect (
run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE unrelated (id text PRIMARY KEY) ` )
yield * DatabaseMigration . apply ( db )
} ) ,
) ,
) . rejects . toThrow ( "Database is not empty and has no session table" )
} )
2026-06-05 15:19:55 +00:00
test ( "backfills existing Context Epoch rows to the build agent" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run (
sql ` CREATE TABLE session_context_epoch (session_id text PRIMARY KEY, baseline text NOT NULL, snapshot text NOT NULL, baseline_seq integer NOT NULL, replacement_seq integer, revision integer DEFAULT 0 NOT NULL) ` ,
)
yield * db . run (
sql ` INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('ses_existing', 'baseline', '{}', 0) ` ,
)
yield * DatabaseMigration . applyOnly ( db , [ contextEpochAgentMigration ] )
expect ( yield * db . get ( sql ` SELECT agent FROM session_context_epoch WHERE session_id = 'ses_existing' ` ) ) . toEqual ( {
agent : "build" ,
} )
} ) ,
)
} )
2026-06-12 06:45:25 +00:00
test ( "keeps legacy credential fields nullable" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run (
sql ` CREATE TABLE credential (id text PRIMARY KEY, connector_id text NOT NULL, method_id text NOT NULL, label text NOT NULL, value text NOT NULL, active integer DEFAULT false NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL) ` ,
)
yield * db . run (
sql ` CREATE UNIQUE INDEX credential_connector_active_idx ON credential (connector_id) WHERE active = 1 ` ,
)
yield * DatabaseMigration . applyOnly ( db , [ simplifyIntegrationCredentialsMigration ] )
yield * db . run (
sql ` INSERT INTO credential (id, connector_id, method_id, label, value, active, time_created, time_updated) VALUES ('legacy', 'openai', 'oauth', 'Legacy', '{}', 1, 1, 1) ` ,
)
yield * db . run (
sql ` INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES ('current', 'anthropic', 'Current', '{}', 2, 2) ` ,
)
2026-06-12 06:47:04 +00:00
expect ( yield * db . get ( sql ` SELECT connector_id, method_id, active FROM credential WHERE id = 'current' ` ) ) . toEqual (
{ connector_id : null , method_id : null , active : null } ,
)
2026-06-12 06:45:25 +00:00
} ) ,
)
} )
2026-06-04 23:24:30 +00:00
test ( "resets beta history and rebuilds event-sourced Session input storage" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE session (id text PRIMARY KEY, workspace_id text) ` )
yield * db . run ( sql ` CREATE TABLE workspace (id text PRIMARY KEY) ` )
yield * db . run ( sql ` CREATE TABLE message (id text PRIMARY KEY) ` )
yield * db . run ( sql ` CREATE TABLE part (id text PRIMARY KEY) ` )
yield * db . run ( sql ` CREATE TABLE event_sequence (aggregate_id text PRIMARY KEY, seq integer NOT NULL) ` )
yield * db . run (
sql ` CREATE TABLE event (id text PRIMARY KEY, aggregate_id text NOT NULL, seq integer NOT NULL, type text NOT NULL, data text NOT NULL) ` ,
)
yield * db . run ( sql ` CREATE INDEX event_aggregate_seq_idx ON event (aggregate_id, seq) ` )
yield * db . run ( sql ` CREATE INDEX event_aggregate_type_seq_idx ON event (aggregate_id, type, seq) ` )
yield * db . run (
sql ` CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL) ` ,
)
yield * db . run ( sql ` CREATE INDEX session_message_session_seq_idx ON session_message (session_id, seq) ` )
yield * db . run (
sql ` CREATE TABLE session_input (seq integer PRIMARY KEY AUTOINCREMENT, id text NOT NULL UNIQUE, session_id text NOT NULL, prompt text NOT NULL, delivery text NOT NULL, promoted_seq integer, time_created integer NOT NULL) ` ,
)
yield * db . run (
sql ` CREATE INDEX session_input_session_pending_delivery_seq_idx ON session_input (session_id, promoted_seq, delivery, seq) ` ,
)
yield * db . run ( sql ` INSERT INTO session (id, workspace_id) VALUES ('session', 'wrk_old') ` )
yield * db . run ( sql ` INSERT INTO workspace (id) VALUES ('wrk_old') ` )
yield * db . run ( sql ` INSERT INTO message (id) VALUES ('message') ` )
yield * db . run ( sql ` INSERT INTO part (id) VALUES ('part') ` )
yield * db . run ( sql ` INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 0) ` )
yield * db . run (
sql ` INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('evt_old', 'session', 0, 'old.1', '{}') ` ,
)
yield * db . run (
sql ` INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('msg_old', 'session', 'user', 0, 1, 1, '{}') ` ,
)
yield * db . run (
sql ` INSERT INTO session_input (id, session_id, prompt, delivery, time_created) VALUES ('msg_pending', 'session', '{}', 'steer', 1) ` ,
)
yield * DatabaseMigration . applyOnly ( db , [ eventSourcedSessionInputMigration ] )
expect ( yield * db . all ( sql ` SELECT id, workspace_id FROM session ` ) ) . toEqual ( [
{ id : "session" , workspace_id : null } ,
] )
expect ( yield * db . all ( sql ` SELECT id FROM workspace ` ) ) . toEqual ( [ ] )
expect ( yield * db . all ( sql ` SELECT id FROM message ` ) ) . toEqual ( [ { id : "message" } ] )
expect ( yield * db . all ( sql ` SELECT id FROM part ` ) ) . toEqual ( [ { id : "part" } ] )
expect ( yield * db . all ( sql ` SELECT id FROM event ` ) ) . toEqual ( [ ] )
expect ( yield * db . all ( sql ` SELECT aggregate_id FROM event_sequence ` ) ) . toEqual ( [ ] )
expect ( yield * db . all ( sql ` SELECT id FROM session_message ` ) ) . toEqual ( [ ] )
expect ( yield * db . all ( sql ` SELECT id FROM session_input ` ) ) . toEqual ( [ ] )
expect (
( yield * db . all < { name : string } > ( sql ` PRAGMA table_info(session_input) ` ) ) . map ( ( column ) = > column . name ) ,
) . toEqual ( [ "id" , "session_id" , "prompt" , "delivery" , "admitted_seq" , "promoted_seq" , "time_created" ] )
expect (
( yield * db . all < { name : string ; unique : number } > ( sql ` PRAGMA index_list(session_message) ` ) ) . find (
( index ) = > index . name === "session_message_session_seq_idx" ,
) ,
) . toMatchObject ( { unique : 1 } )
expect (
( yield * db . all < { name : string ; unique : number } > ( sql ` PRAGMA index_list(event) ` ) ) . find (
( index ) = > index . name === "event_aggregate_seq_idx" ,
) ,
) . toMatchObject ( { unique : 1 } )
expect (
( yield * db . all < { name : string ; unique : number } > ( sql ` PRAGMA index_list(session_input) ` ) ) . filter ( ( index ) = >
[ "session_input_session_admitted_seq_idx" , "session_input_session_promoted_seq_idx" ] . includes ( index . name ) ,
) ,
) . toEqual ( [
expect . objectContaining ( { name : "session_input_session_promoted_seq_idx" , unique : 1 } ) ,
expect . objectContaining ( { name : "session_input_session_admitted_seq_idx" , unique : 1 } ) ,
] )
} ) ,
)
} )
2026-06-22 19:39:41 +00:00
test ( "preserves canonical V1 state and restarts its event stream" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` PRAGMA foreign_keys = ON ` )
yield * DatabaseMigration . apply ( db )
yield * db . run (
sql ` INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('global', '/project', 1, 1, '[]') ` ,
)
yield * db . run (
sql ` INSERT INTO workspace (id, type, project_id, time_used) VALUES ('workspace', 'local', 'global', 1) ` ,
)
yield * db . run (
sql ` INSERT INTO session (id, project_id, workspace_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'global', 'workspace', 'session', '/project', 'Before', 'test', 1, 1) ` ,
)
yield * db . run (
sql ` INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('message', 'session', 1, 1, '{}') ` ,
)
yield * db . run (
sql ` INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('part', 'message', 'session', 1, 1, '{}') ` ,
)
yield * db . run ( sql ` INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 9) ` )
yield * db . run (
sql ` INSERT INTO event (id, aggregate_id, seq, type, data) VALUES ('event', 'session', 9, 'session.updated.1', '{}') ` ,
)
yield * db . run (
sql ` INSERT INTO session_input (id, session_id, prompt, delivery, admitted_seq, time_created) VALUES ('input', 'session', '{}', 'steer', 9, 1) ` ,
)
yield * db . run (
sql ` INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('projected', 'session', 'user', 9, 1, 1, '{}') ` ,
)
yield * db . run (
sql ` INSERT INTO session_context_epoch (session_id, baseline, snapshot, baseline_seq) VALUES ('session', 'baseline', '{}', 9) ` ,
)
2026-06-22 21:51:49 +00:00
yield * db . run ( sql ` DELETE FROM migration WHERE id = ${ simplifySessionInputMigration . id } ` )
yield * DatabaseMigration . applyOnly ( db , [ simplifySessionInputMigration ] )
2026-06-22 19:39:41 +00:00
const database = Layer . succeed ( Database . Service , { db } )
yield * EventV2 . Service . use ( ( service ) = >
service . publish ( SessionV1 . Event . Updated , {
sessionID : SessionSchema.ID.make ( "session" ) ,
info : {
id : SessionSchema.ID.make ( "session" ) ,
slug : "session" ,
projectID : ProjectV2.ID.global ,
directory : "/project" ,
title : "After" ,
version : "test" ,
time : { created : 1 , updated : 2 } ,
} ,
} ) ,
) . pipe (
Effect . provide (
2026-06-29 15:35:17 +00:00
AppNodeBuilder . build ( LayerNode . group ( [ EventV2 . node , SessionProjector . node ] ) , [ [ Database . node , database ] ] ) ,
2026-06-22 19:39:41 +00:00
) ,
)
expect (
yield * db . get ( sql `
SELECT
( SELECT title FROM session WHERE id = 'session' ) AS title ,
( SELECT workspace_id FROM session WHERE id = 'session' ) AS workspaceID ,
( SELECT COUNT ( * ) FROM message WHERE id = 'message' ) AS messages ,
( SELECT COUNT ( * ) FROM part WHERE id = 'part' ) AS parts ,
( SELECT COUNT ( * ) FROM workspace ) AS workspaces ,
( SELECT COUNT ( * ) FROM session_input ) AS sessionInputs ,
( SELECT COUNT ( * ) FROM session_message ) AS sessionMessages ,
( SELECT COUNT ( * ) FROM session_context_epoch ) AS contextEpochs ,
( SELECT seq FROM event_sequence WHERE aggregate_id = 'session' ) AS seq ,
( SELECT type FROM event WHERE aggregate_id = 'session' ) AS eventType
` ),
) . toEqual ( {
title : "After" ,
workspaceID : null ,
messages : 1 ,
parts : 1 ,
workspaces : 0 ,
sessionInputs : 0 ,
sessionMessages : 0 ,
contextEpochs : 0 ,
seq : 0 ,
eventType : "session.updated.1" ,
} )
} ) ,
)
} )
2026-06-04 13:43:10 +00:00
test ( "resets incompatible projected Session messages before adding sequence order" , async ( ) = > {
2026-06-04 03:02:17 +00:00
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
2026-06-04 13:43:10 +00:00
yield * db . run ( sql ` CREATE TABLE session (id text PRIMARY KEY) ` )
yield * db . run (
sql ` CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL) ` ,
)
yield * db . run (
sql ` CREATE TABLE part (id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL) ` ,
)
2026-06-04 03:02:17 +00:00
yield * db . run ( sql ` CREATE TABLE event (id text PRIMARY KEY, seq integer NOT NULL) ` )
yield * db . run (
2026-06-04 13:43:10 +00:00
sql ` CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL) ` ,
2026-06-04 03:02:17 +00:00
)
yield * db . run (
sql ` CREATE INDEX session_message_session_time_created_id_idx ON session_message (session_id, time_created, id) ` ,
)
yield * db . run (
sql ` CREATE INDEX session_message_session_type_time_created_id_idx ON session_message (session_id, type, time_created, id) ` ,
)
2026-06-04 13:43:10 +00:00
yield * db . run ( sql ` INSERT INTO session (id) VALUES ('session') ` )
yield * db . run (
sql ` INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES ('legacy_message', 'session', 1, 1, '{"role":"user"}') ` ,
)
yield * db . run (
sql ` INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES ('legacy_part', 'legacy_message', 'session', 1, 1, '{"type":"text","text":"hello"}') ` ,
)
2026-06-04 03:02:17 +00:00
yield * db . run (
2026-06-04 13:43:10 +00:00
sql ` INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('stale_projection', 'session', 'user', 1, 1, '{}') ` ,
2026-06-04 03:02:17 +00:00
)
yield * DatabaseMigration . applyOnly ( db , [ sessionMessageProjectionOrderMigration ] )
2026-06-04 13:43:10 +00:00
expect ( yield * db . all ( sql ` SELECT id, session_id, data FROM message ` ) ) . toEqual ( [
{ id : "legacy_message" , session_id : "session" , data : '{"role":"user"}' } ,
2026-06-04 03:02:17 +00:00
] )
2026-06-04 13:43:10 +00:00
expect ( yield * db . all ( sql ` SELECT id, message_id, session_id, data FROM part ` ) ) . toEqual ( [
{
id : "legacy_part" ,
message_id : "legacy_message" ,
session_id : "session" ,
data : '{"type":"text","text":"hello"}' ,
} ,
] )
expect ( yield * db . all ( sql ` SELECT id FROM session_message ` ) ) . toEqual ( [ ] )
yield * db . run (
sql ` INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('fresh_projection', 'session', 'user', 7, 2, 2, '{}') ` ,
)
expect ( yield * db . get ( sql ` SELECT id, seq FROM session_message ` ) ) . toEqual ( { id : "fresh_projection" , seq : 7 } )
2026-06-04 03:02:17 +00:00
} ) ,
)
} )
2026-05-31 01:08:38 +00:00
test ( "runs session usage backfill in order with schema changes" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE session (id text PRIMARY KEY, time_updated integer NOT NULL) ` )
yield * db . run ( sql ` CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, data text NOT NULL) ` )
yield * db . run ( sql ` INSERT INTO session (id, time_updated) VALUES ('session_1', 1) ` )
yield * db . run (
sql ` INSERT INTO message (id, session_id, data) VALUES ('message_1', 'session_1', '{"role":"assistant","cost":1.25,"tokens":{"input":2,"output":3,"reasoning":4,"cache":{"read":5,"write":6}}}') ` ,
)
yield * DatabaseMigration . applyOnly ( db , [ sessionUsageMigration ] )
expect (
yield * db . get (
sql ` SELECT cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write FROM session WHERE id = 'session_1' ` ,
) ,
) . toEqual ( {
cost : 1.25 ,
tokens_input : 2 ,
tokens_output : 3 ,
tokens_reasoning : 4 ,
tokens_cache_read : 5 ,
tokens_cache_write : 6 ,
} )
} ) ,
)
} )
2026-06-02 00:54:41 +00:00
test ( "normalizes Windows storage paths and leaves POSIX paths untouched" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE project (id text PRIMARY KEY, worktree text NOT NULL, sandboxes text NOT NULL) ` )
yield * db . run ( sql ` CREATE TABLE session (id text PRIMARY KEY, directory text NOT NULL, path text) ` )
// Windows-shaped rows (drive + backslash) must be normalized.
yield * db . run (
sql ` INSERT INTO project (id, worktree, sandboxes) VALUES ( ${ "win" } , ${ "C:\\Repo\\Thing" } , ${ JSON . stringify ( [
"C:\\Repo\\Thing\\sandbox" ,
] ) } ) ` ,
)
yield * db . run (
sql ` INSERT INTO session (id, directory, path) VALUES ( ${ "win" } , ${ "C:\\Repo\\Thing\\packages\\api" } , ${ "packages\\api" } ) ` ,
)
// UNC worktrees and their sandboxes must normalize too (not just drive paths).
yield * db . run (
sql ` INSERT INTO project (id, worktree, sandboxes) VALUES ( ${ "unc" } , ${ "\\\\server\\share" } , ${ JSON . stringify ( [
"\\\\server\\share\\sandbox" ,
] ) } ) ` ,
)
// The "/" worktree sentinel and POSIX paths (including a pathological
// backslash in a POSIX filename) must survive byte-for-byte.
yield * db . run ( sql ` INSERT INTO project (id, worktree, sandboxes) VALUES ( ${ "global" } , ${ "/" } , ${ "[]" } ) ` )
yield * db . run (
sql ` INSERT INTO session (id, directory, path) VALUES ( ${ "posix" } , ${ "/home/me/we\\ird" } , ${ "src\\weird" } ) ` ,
)
yield * DatabaseMigration . applyOnly ( db , [ normalizeStoragePathsMigration ] )
expect ( yield * db . get ( sql ` SELECT worktree, sandboxes FROM project WHERE id = 'win' ` ) ) . toEqual ( {
worktree : "C:/Repo/Thing" ,
sandboxes : JSON.stringify ( [ "C:/Repo/Thing/sandbox" ] ) ,
} )
expect ( yield * db . get ( sql ` SELECT directory, path FROM session WHERE id = 'win' ` ) ) . toEqual ( {
directory : "C:/Repo/Thing/packages/api" ,
path : "packages/api" ,
} )
expect ( yield * db . get ( sql ` SELECT worktree, sandboxes FROM project WHERE id = 'unc' ` ) ) . toEqual ( {
worktree : "//server/share" ,
sandboxes : JSON.stringify ( [ "//server/share/sandbox" ] ) ,
} )
expect ( yield * db . get ( sql ` SELECT worktree FROM project WHERE id = 'global' ` ) ) . toEqual ( { worktree : "/" } )
expect ( yield * db . get ( sql ` SELECT directory, path FROM session WHERE id = 'posix' ` ) ) . toEqual ( {
directory : "/home/me/we\\ird" ,
path : "src\\weird" ,
} )
} ) ,
)
} )
test ( "maps native Windows paths through database columns" , async ( ) = > {
if ( process . platform !== "win32" ) return
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * DatabaseMigration . apply ( db )
const projectID = ProjectV2 . ID . make ( "codec_project" )
const worktree = AbsolutePath . make ( "C:\\Repo\\Thing" )
const sandbox = AbsolutePath . make ( "C:\\Repo\\Thing\\sandbox" )
const directory = "C:\\Repo\\Thing\\packages\\api"
const sessionID = SessionSchema . ID . make ( "ses_codec" )
expect ( ( ) = >
Effect . runSync (
db
. insert ( ProjectTable )
. values ( {
id : ProjectV2.ID.make ( "invalid_path" ) ,
worktree : AbsolutePath.make ( "not-absolute" ) ,
sandboxes : [ ] ,
time_created : 1 ,
time_updated : 1 ,
} )
. run ( ) ,
) ,
) . toThrow ( )
yield * db
. insert ( ProjectTable )
. values ( {
id : projectID ,
worktree ,
sandboxes : [ sandbox ] ,
time_created : 1 ,
time_updated : 1 ,
} )
. run ( )
yield * db
. insert ( SessionTable )
. values ( {
id : sessionID ,
project_id : projectID ,
slug : "codec" ,
directory ,
path : "packages\\api" ,
title : "Codec" ,
version : "test" ,
time_created : 1 ,
time_updated : 1 ,
} )
. run ( )
2026-06-02 00:56:03 +00:00
expect (
yield * db . get < { worktree : string ; sandboxes : string } > (
sql ` SELECT worktree, sandboxes FROM project WHERE id = ${ projectID } ` ,
) ,
) . toEqual ( {
2026-06-02 00:54:41 +00:00
worktree : "C:/Repo/Thing" ,
sandboxes : JSON.stringify ( [ "C:/Repo/Thing/sandbox" ] ) ,
} )
2026-06-02 00:56:03 +00:00
expect (
yield * db . get < { directory : string ; path : string } > (
sql ` SELECT directory, path FROM session WHERE id = ${ sessionID } ` ,
) ,
) . toEqual ( {
2026-06-02 00:54:41 +00:00
directory : "C:/Repo/Thing/packages/api" ,
path : "packages/api" ,
} )
const project = yield * db . select ( ) . from ( ProjectTable ) . where ( eq ( ProjectTable . worktree , worktree ) ) . get ( )
const session = yield * db . select ( ) . from ( SessionTable ) . where ( eq ( SessionTable . directory , directory ) ) . get ( )
expect ( project ? . worktree ) . toBe ( worktree )
expect ( project ? . sandboxes ) . toEqual ( [ sandbox ] )
expect ( session ? . directory ) . toBe ( directory )
expect ( session ? . path ) . toBe ( "packages/api" )
expect ( ( yield * db . select ( ) . from ( SessionTable ) . where ( eq ( SessionTable . path , "packages\\api" ) ) . get ( ) ) ? . id ) . toBe (
sessionID ,
)
const moved = AbsolutePath . make ( "D:\\Moved\\Thing" )
const updated = yield * db
. update ( ProjectTable )
. set ( { worktree : moved , sandboxes : [ moved ] } )
. where ( eq ( ProjectTable . id , projectID ) )
. returning ( )
. get ( )
expect ( updated ? . worktree ) . toBe ( moved )
expect ( updated ? . sandboxes ) . toEqual ( [ moved ] )
expect (
2026-06-02 00:56:03 +00:00
yield * db . get < { worktree : string ; sandboxes : string } > (
sql ` SELECT worktree, sandboxes FROM project WHERE id = ${ projectID } ` ,
) ,
2026-06-02 00:54:41 +00:00
) . toEqual ( { worktree : "D:/Moved/Thing" , sandboxes : JSON.stringify ( [ "D:/Moved/Thing" ] ) } )
2026-06-02 00:56:03 +00:00
expect (
( yield * db
. select ( )
. from ( ProjectTable )
. where ( inArray ( ProjectTable . worktree , [ moved ] ) )
. get ( ) ) ? . id ,
) . toBe ( projectID )
2026-06-02 00:54:41 +00:00
yield * db . run ( sql ` UPDATE project SET worktree = ${ "not-absolute" } WHERE id = ${ projectID } ` )
2026-06-02 00:56:03 +00:00
expect ( ( ) = >
Effect . runSync ( db . select ( ) . from ( ProjectTable ) . where ( eq ( ProjectTable . id , projectID ) ) . get ( ) ) ,
) . toThrow ( )
2026-06-02 00:54:41 +00:00
} ) ,
)
} )
2026-05-31 01:08:38 +00:00
test ( "imports existing drizzle migration state" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
2026-05-31 01:09:55 +00:00
yield * db . run (
sql ` CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT) ` ,
)
2026-05-31 01:08:38 +00:00
yield * db . run ( sql `
INSERT INTO __drizzle_migrations ( hash , created_at , name , applied_at )
VALUES ( 'hash' , 1 , '20260127222353_familiar_lady_ursula' , $ { new Date ( ) . toISOString ( ) } )
` )
yield * DatabaseMigration . applyOnly ( db , [ ] )
expect ( yield * db . get ( sql ` SELECT id FROM migration ` ) ) . toEqual ( { id : "20260127222353_familiar_lady_ursula" } )
} ) ,
)
} )
2026-06-01 04:50:43 +00:00
test ( "does not replay a migrated session metadata column" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE session (id text PRIMARY KEY, metadata text) ` )
yield * db . run (
sql ` CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT) ` ,
)
yield * db . run ( sql `
INSERT INTO __drizzle_migrations ( hash , created_at , name , applied_at )
VALUES ( 'hash' , 1 , '20260511173437_session-metadata' , $ { new Date ( ) . toISOString ( ) } )
` )
yield * DatabaseMigration . applyOnly ( db , [ sessionMetadataMigration ] )
expect ( yield * db . all ( sql ` SELECT id FROM migration ` ) ) . toEqual ( [ { id : "20260511173437_session-metadata" } ] )
} ) ,
)
} )
test ( "accepts the temporary replacement session metadata migration id" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE session (id text PRIMARY KEY, metadata text) ` )
yield * db . run ( sql ` CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL) ` )
yield * db . run ( sql ` INSERT INTO migration (id, time_completed) VALUES ('20260530232709_lovely_romulus', 1) ` )
yield * DatabaseMigration . applyOnly ( db , [ sessionMetadataMigration ] )
expect ( yield * db . all ( sql ` SELECT id FROM migration ORDER BY id ` ) ) . toEqual ( [
{ id : "20260511173437_session-metadata" } ,
{ id : "20260530232709_lovely_romulus" } ,
] )
} ) ,
)
} )
2026-05-31 01:08:38 +00:00
test ( "skips drizzle import when migration table already has state" , async ( ) = > {
await run (
Effect . gen ( function * ( ) {
const db = yield * makeDb
yield * db . run ( sql ` CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL) ` )
yield * db . run ( sql ` INSERT INTO migration (id, time_completed) VALUES ('existing', 1) ` )
2026-05-31 01:09:55 +00:00
yield * db . run (
sql ` CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT) ` ,
)
2026-05-31 01:08:38 +00:00
yield * db . run ( sql `
INSERT INTO __drizzle_migrations ( hash , created_at , name , applied_at )
VALUES ( 'hash' , 1 , '20260127222353_familiar_lady_ursula' , $ { new Date ( ) . toISOString ( ) } )
` )
yield * DatabaseMigration . applyOnly ( db , [ ] )
expect ( yield * db . all ( sql ` SELECT id FROM migration ORDER BY id ` ) ) . toEqual ( [ { id : "existing" } ] )
} ) ,
)
} )
} )