@@ -15,6 +15,7 @@ import {
1515 initSchema ,
1616 isReadonlyError ,
1717 repairSchema ,
18+ runMigrations ,
1819 tableExists ,
1920} from "../../../src/lib/db/schema.js" ;
2021import { useTestConfigDir } from "../../helpers.js" ;
@@ -291,3 +292,155 @@ describe("isReadonlyError", () => {
291292 expect ( isReadonlyError ( undefined ) ) . toBe ( false ) ;
292293 } ) ;
293294} ) ;
295+
296+ describe ( "runMigrations" , ( ) => {
297+ test ( "no-op when already at current version" , ( ) => {
298+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
299+ initSchema ( db ) ;
300+
301+ // Should not throw and version stays at current
302+ runMigrations ( db ) ;
303+
304+ const version = (
305+ db . query ( "SELECT version FROM schema_version" ) . get ( ) as {
306+ version : number ;
307+ }
308+ ) . version ;
309+ expect ( version ) . toBe ( CURRENT_SCHEMA_VERSION ) ;
310+ db . close ( ) ;
311+ } ) ;
312+
313+ test ( "repairs pagination_cursors with wrong single-column PK (CLI-72)" , ( ) => {
314+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
315+ // Build a full schema but with the bugged pagination_cursors table
316+ initSchema ( db ) ;
317+ db . exec ( "DROP TABLE pagination_cursors" ) ;
318+ db . exec (
319+ "CREATE TABLE pagination_cursors (command_key TEXT PRIMARY KEY, context TEXT NOT NULL, cursor TEXT NOT NULL, expires_at INTEGER NOT NULL)"
320+ ) ;
321+ // Set version to 5 so migration 5→6 fires
322+ db . query ( "UPDATE schema_version SET version = 5" ) . run ( ) ;
323+
324+ runMigrations ( db ) ;
325+
326+ // Table should now have the correct composite PK
327+ const row = db
328+ . query (
329+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='pagination_cursors'"
330+ )
331+ . get ( ) as { sql : string } ;
332+ expect ( row . sql ) . toContain ( "PRIMARY KEY (command_key, context)" ) ;
333+
334+ // Version should be bumped to 6
335+ const version = (
336+ db . query ( "SELECT version FROM schema_version" ) . get ( ) as {
337+ version : number ;
338+ }
339+ ) . version ;
340+ expect ( version ) . toBe ( CURRENT_SCHEMA_VERSION ) ;
341+ db . close ( ) ;
342+ } ) ;
343+
344+ test ( "skips pagination_cursors repair when PK is already correct" , ( ) => {
345+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
346+ initSchema ( db ) ;
347+ db . query ( "UPDATE schema_version SET version = 5" ) . run ( ) ;
348+
349+ // pagination_cursors was created by initSchema with the correct PK
350+ runMigrations ( db ) ;
351+
352+ const row = db
353+ . query (
354+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='pagination_cursors'"
355+ )
356+ . get ( ) as { sql : string } ;
357+ expect ( row . sql ) . toContain ( "PRIMARY KEY (command_key, context)" ) ;
358+ db . close ( ) ;
359+ } ) ;
360+
361+ test ( "creates pagination_cursors when missing during migration 4→5" , ( ) => {
362+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
363+ // Set up schema at version 4 without pagination_cursors
364+ const statementsWithoutPagination = Object . entries ( EXPECTED_TABLES )
365+ . filter ( ( [ name ] ) => name !== "pagination_cursors" )
366+ . map ( ( [ , ddl ] ) => ddl ) ;
367+ db . exec ( statementsWithoutPagination . join ( ";\n" ) ) ;
368+ db . query ( "INSERT INTO schema_version (version) VALUES (4)" ) . run ( ) ;
369+
370+ runMigrations ( db ) ;
371+
372+ expect ( tableExists ( db , "pagination_cursors" ) ) . toBe ( true ) ;
373+ db . close ( ) ;
374+ } ) ;
375+ } ) ;
376+
377+ describe ( "repairSchema: wrong primary key" , ( ) => {
378+ test ( "detects and repairs pagination_cursors with wrong single-column PK" , ( ) => {
379+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
380+ initSchema ( db ) ;
381+ // Simulate the bug: drop and recreate with wrong PK
382+ db . exec ( "DROP TABLE pagination_cursors" ) ;
383+ db . exec (
384+ "CREATE TABLE pagination_cursors (command_key TEXT PRIMARY KEY, context TEXT NOT NULL, cursor TEXT NOT NULL, expires_at INTEGER NOT NULL)"
385+ ) ;
386+
387+ const result = repairSchema ( db ) ;
388+
389+ expect (
390+ result . fixed . some ( ( f ) => f . includes ( "pagination_cursors" ) )
391+ ) . toBe ( true ) ;
392+ expect ( result . failed ) . toEqual ( [ ] ) ;
393+
394+ const row = db
395+ . query (
396+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='pagination_cursors'"
397+ )
398+ . get ( ) as { sql : string } ;
399+ expect ( row . sql ) . toContain ( "PRIMARY KEY (command_key, context)" ) ;
400+ db . close ( ) ;
401+ } ) ;
402+
403+ test ( "no-op when pagination_cursors already has correct composite PK" , ( ) => {
404+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
405+ initSchema ( db ) ;
406+
407+ const result = repairSchema ( db ) ;
408+
409+ // Should not report pagination_cursors as fixed
410+ expect (
411+ result . fixed . some ( ( f ) => f . includes ( "pagination_cursors" ) )
412+ ) . toBe ( false ) ;
413+ db . close ( ) ;
414+ } ) ;
415+ } ) ;
416+
417+ describe ( "getSchemaIssues: wrong primary key" , ( ) => {
418+ test ( "detects wrong_primary_key when pagination_cursors has single-column PK" , ( ) => {
419+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
420+ initSchema ( db ) ;
421+ db . exec ( "DROP TABLE pagination_cursors" ) ;
422+ db . exec (
423+ "CREATE TABLE pagination_cursors (command_key TEXT PRIMARY KEY, context TEXT NOT NULL, cursor TEXT NOT NULL, expires_at INTEGER NOT NULL)"
424+ ) ;
425+
426+ const issues = getSchemaIssues ( db ) ;
427+ const pkIssues = issues . filter ( ( i ) => i . type === "wrong_primary_key" ) ;
428+
429+ expect ( pkIssues ) . toContainEqual ( {
430+ type : "wrong_primary_key" ,
431+ table : "pagination_cursors" ,
432+ } ) ;
433+ db . close ( ) ;
434+ } ) ;
435+
436+ test ( "no wrong_primary_key issues for healthy database" , ( ) => {
437+ const db = new Database ( join ( getTestDir ( ) , "test.db" ) ) ;
438+ initSchema ( db ) ;
439+
440+ const issues = getSchemaIssues ( db ) ;
441+ const pkIssues = issues . filter ( ( i ) => i . type === "wrong_primary_key" ) ;
442+
443+ expect ( pkIssues ) . toEqual ( [ ] ) ;
444+ db . close ( ) ;
445+ } ) ;
446+ } ) ;
0 commit comments