@@ -522,4 +522,59 @@ describe("runToolOwnerBackfill", () => {
522522
523523 closeQuietly ( mc ) ;
524524 } ) ;
525+
526+ test ( "ATTACH succeeds when the OpenCode DB path contains a single quote" , ( ) => {
527+ // Regression guard: the OpenCode DB path is interpolated into an
528+ // `ATTACH '<path>'` statement (SQLite/bun:sqlite reject a bound
529+ // parameter there), so an unescaped single quote in the path would
530+ // break out of the SQL string literal and throw a syntax error. The
531+ // path is resolved from XDG_DATA_HOME via getDataDir(), so a data home
532+ // containing a quote exercises the escaping end-to-end.
533+ const fs = require ( "node:fs" ) ;
534+ const base = createTempDir ( "mc-backfill-quote-" ) ;
535+ const dataHome = join ( base , "o'brien" ) ;
536+ fs . mkdirSync ( dataHome , { recursive : true } ) ;
537+ process . env . XDG_DATA_HOME = dataHome ;
538+
539+ // OpenCode DB at $XDG_DATA_HOME/opencode/opencode.db with one tool part.
540+ const ocDir = join ( dataHome , "opencode" ) ;
541+ fs . mkdirSync ( ocDir , { recursive : true } ) ;
542+ const oc = new Database ( join ( ocDir , "opencode.db" ) ) ;
543+ oc . exec ( `
544+ CREATE TABLE message (
545+ id TEXT PRIMARY KEY,
546+ session_id TEXT NOT NULL,
547+ time_created INTEGER NOT NULL,
548+ data TEXT NOT NULL
549+ );
550+ CREATE TABLE part (
551+ id TEXT PRIMARY KEY,
552+ message_id TEXT NOT NULL,
553+ time_created INTEGER NOT NULL,
554+ data TEXT NOT NULL
555+ );
556+ ` ) ;
557+ oc . prepare (
558+ "INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)" ,
559+ ) . run ( "msg-A" , "ses-1" , 1000 , JSON . stringify ( { role : "assistant" } ) ) ;
560+ oc . prepare ( "INSERT INTO part (id, message_id, time_created, data) VALUES (?, ?, ?, ?)" ) . run (
561+ "p-A" ,
562+ "msg-A" ,
563+ 1100 ,
564+ JSON . stringify ( { type : "tool" , callID : "read:1" } ) ,
565+ ) ;
566+ oc . close ( ) ;
567+
568+ const mc = createMcDb ( ) ;
569+ insertTag ( mc , "ses-1" , "read:1" , "tool" , 100 , 1 ) ;
570+
571+ // No throw + the session is backfilled proves ATTACH parsed the quoted path.
572+ const result = runToolOwnerBackfill ( mc ) ;
573+ expect ( result . sessionsCompleted ) . toBe ( 1 ) ;
574+ expect ( result . rowsUpdated ) . toBe ( 1 ) ;
575+ const tags = getTagsBySession ( mc , "ses-1" ) ;
576+ expect ( tags [ 0 ] . toolOwnerMessageId ) . toBe ( "msg-A" ) ;
577+
578+ closeQuietly ( mc ) ;
579+ } ) ;
525580} ) ;
0 commit comments