From 0eb85cbe711d741132f1b09ed7183d3363ac906a Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 31 Jul 2026 10:19:24 -0700 Subject: [PATCH 1/2] fix: reconcile cross-surface SQLite migrations --- .../scripts/workbench_schema.py | 130 +++++++++- sdk/typescript/tests-ts/runtime.test.ts | 245 ++++++++++++++++++ 2 files changed, 374 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py b/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py index 748772c4..2ca8ba97 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py @@ -604,6 +604,14 @@ ), ( 25, + "persist scan model settings", + """ + ALTER TABLE scans ADD COLUMN model TEXT; + ALTER TABLE scans ADD COLUMN reasoning_effort TEXT; + """, + ), + ( + 26, "persist scan completion warnings", """ ALTER TABLE scans @@ -613,7 +621,11 @@ ) -def normalize_pre_release_migrations(connection: sqlite3.Connection, timestamp: str) -> None: +def normalize_pre_release_migrations( + connection: sqlite3.Connection, timestamp: str +) -> None: + normalize_pre_release_execution_profile_migrations(connection, timestamp) + phase_progress_migration = connection.execute( "SELECT name FROM schema_migrations WHERE version = 12" ).fetchone() @@ -693,6 +705,122 @@ def normalize_pre_release_migrations(connection: sqlite3.Connection, timestamp: ) +def normalize_pre_release_execution_profile_migrations( + connection: sqlite3.Connection, timestamp: str +) -> None: + execution_migrations = { + row["version"]: row["name"] + for row in connection.execute( + "SELECT version, name FROM schema_migrations WHERE version IN (11, 12, 25, 26)" + ) + } + legacy_names = { + 11: "scan execution profiles", + 12: "dynamic scan execution profiles", + } + model_migration_name = "persist scan model settings" + warnings_migration_name = "persist scan completion warnings" + has_legacy_profile_history = execution_migrations.get(11) == legacy_names[11] + has_public_warnings_history = ( + execution_migrations.get(25) == warnings_migration_name + ) + + scan_columns = { + row["name"] for row in connection.execute("PRAGMA table_info(scans)") + } + workspace_columns = { + row["name"] for row in connection.execute("PRAGMA table_info(workspaces)") + } + has_legacy_profile_columns = ( + "execution_model" in scan_columns or "execution_model" in workspace_columns + ) + if not ( + has_legacy_profile_history + or has_public_warnings_history + or has_legacy_profile_columns + ): + return + + if ( + has_legacy_profile_history + and execution_migrations.get(12) not in (None, legacy_names[12]) + ) or ( + not has_legacy_profile_history + and execution_migrations.get(12) == legacy_names[12] + ): + raise SystemExit( + "The Codex Security database has an unsupported execution-profile migration history." + ) + + expected_legacy_columns = {"execution_model", "reasoning_effort"} + renamed_legacy_columns = { + "legacy_execution_model", + "legacy_reasoning_effort", + } + if has_legacy_profile_columns and ( + not expected_legacy_columns.issubset(scan_columns) + or not expected_legacy_columns.issubset(workspace_columns) + or renamed_legacy_columns.intersection(scan_columns) + or renamed_legacy_columns.intersection(workspace_columns) + ): + raise SystemExit( + "The Codex Security database has an unsupported execution-profile migration history." + ) + if has_legacy_profile_history and not has_legacy_profile_columns: + raise SystemExit( + "The Codex Security database has an unsupported execution-profile migration history." + ) + + if has_public_warnings_history: + if execution_migrations.get(26) is not None: + raise SystemExit( + "The Codex Security database has an unsupported pre-release migration history." + ) + connection.execute( + "UPDATE schema_migrations SET version = 26 WHERE version = 25 AND name = ?", + (warnings_migration_name,), + ) + execution_migrations.pop(25) + + if execution_migrations.get(25) not in (None, model_migration_name): + raise SystemExit( + "The Codex Security database has an unsupported execution-profile migration history." + ) + + # Keep the historical values and constraints for recovery while moving + # them out of the namespace used by the current independent scan settings. + for table in ("workspaces", "scans") if has_legacy_profile_columns else (): + connection.execute( + f"ALTER TABLE {table} RENAME COLUMN execution_model TO legacy_execution_model" + ) + connection.execute( + f"ALTER TABLE {table} RENAME COLUMN reasoning_effort TO legacy_reasoning_effort" + ) + add_column_if_missing(connection, "scans", "model", "TEXT") + add_column_if_missing(connection, "scans", "reasoning_effort", "TEXT") + if has_legacy_profile_columns: + connection.execute( + """ + UPDATE scans + SET model = COALESCE(model, legacy_execution_model), + reasoning_effort = legacy_reasoning_effort + WHERE legacy_execution_model IS NOT NULL + OR legacy_reasoning_effort IS NOT NULL + """ + ) + if has_legacy_profile_history: + for version, name in legacy_names.items(): + connection.execute( + "DELETE FROM schema_migrations WHERE version = ? AND name = ?", + (version, name), + ) + if execution_migrations.get(25) is None: + connection.execute( + "INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)", + (25, model_migration_name, timestamp), + ) + + def add_column_if_missing( connection: sqlite3.Connection, table: str, column: str, definition: str ) -> None: diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 18069d95..4d0b53b4 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -1859,6 +1859,251 @@ describe("runtime directories and plugin Python boundary", () => { expect(result).toEqual({ ok: true }); }); + test("upgrades colliding legacy execution-profile and public CLI migrations", async () => { + const root = await temporaryDirectory("codex-security-legacy-migrations-"); + const repository = join(root, "repository"); + const stateDirectory = join(root, "state"); + const scanDirectory = join(root, "scan"); + await mkdir(repository); + await mkdir(stateDirectory); + await mkdir(scanDirectory, { mode: 0o700 }); + + const python = Bun.which("python3") ?? Bun.which("python"); + expect(python).not.toBeNull(); + const fixture = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + [ + "import sqlite3, sys", + "from pathlib import Path", + "sys.path.insert(0, sys.argv[1])", + "from workbench_schema import MIGRATIONS, sql_statements", + "repository = Path(sys.argv[2])", + "connection = sqlite3.connect(Path(sys.argv[3]) / 'workbench.sqlite3')", + "connection.execute('CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)')", + "timestamp = '2026-07-09T00:00:00Z'", + "for version, name, migration in MIGRATIONS:", + " if version > 10: break", + " for statement in sql_statements(migration): connection.execute(statement)", + " connection.execute('INSERT INTO schema_migrations VALUES (?, ?, ?)', (version, name, timestamp))", + "for table in ('workspaces', 'scans'):", + " connection.execute(f'ALTER TABLE {table} ADD COLUMN execution_model TEXT CHECK (execution_model IS NULL OR length(execution_model) BETWEEN 1 AND 128)')", + " connection.execute(f'ALTER TABLE {table} ADD COLUMN reasoning_effort TEXT CHECK ((reasoning_effort IS NULL OR length(reasoning_effort) BETWEEN 1 AND 64) AND ((execution_model IS NULL) = (reasoning_effort IS NULL)))')", + "connection.executemany('INSERT INTO schema_migrations VALUES (?, ?, ?)', [(11, 'scan execution profiles', timestamp), (12, 'dynamic scan execution profiles', timestamp)])", + "connection.execute(\"ALTER TABLE scans ADD COLUMN completion_warnings_json TEXT NOT NULL DEFAULT '[]'\")", + "connection.execute('INSERT INTO schema_migrations VALUES (?, ?, ?)', (25, 'persist scan completion warnings', timestamp))", + "connection.execute('INSERT INTO workspaces (id, target_path, thread_id, execution_model, reasoning_effort, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', ('legacy-workspace', str(repository), 'legacy-thread', 'gpt-workspace', 'medium', timestamp, timestamp))", + "connection.execute('INSERT INTO scans (id, workspace_id, target_path, target_revision, scope, mode, scan_dir, status, phase, started_at, created_at, updated_at, execution_model, reasoning_effort) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ('legacy-scan', 'legacy-workspace', str(repository), 'legacy-revision', '.', 'standard', str(repository / 'legacy-scan'), 'complete', 'reporting', timestamp, timestamp, timestamp, 'gpt-legacy', 'high'))", + "connection.execute('UPDATE scans SET completion_warnings_json = ? WHERE id = ?', ('[\"legacy warning\"]', 'legacy-scan'))", + "connection.commit()", + "connection.close()", + ].join("\n"), + join(PLUGIN_ROOT, "scripts"), + repository, + stateDirectory, + ], + { encoding: "utf8" }, + ); + expect(fixture.status).toBe(0); + expect(fixture.stderr).toBe(""); + + const registration = await runWorkbench( + { + python: python!, + pluginRoot: PLUGIN_ROOT, + environment: { + PATH: process.env["PATH"], + CODEX_SECURITY_STATE_DIR: stateDirectory, + }, + }, + [ + "register-cli-scan", + "--repository", + repository, + "--scan-dir", + scanDirectory, + "--recipe-json", + JSON.stringify({ + config: {}, + mode: "standard", + repository, + target: { kind: "repository", paths: [] }, + }), + ], + ); + expect(registration["scanId"]).toBeString(); + + const upgraded = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + [ + "import json, sqlite3, sys", + "connection = sqlite3.connect(sys.argv[1])", + "connection.row_factory = sqlite3.Row", + "columns = {row['name'] for row in connection.execute('PRAGMA table_info(scans)')}", + "migrations = {row['version']: row['name'] for row in connection.execute('SELECT version, name FROM schema_migrations WHERE version IN (11, 12, 25, 26)')}", + "profile = connection.execute('SELECT legacy_execution_model, legacy_reasoning_effort, model, reasoning_effort FROM scans WHERE id = ?', ('legacy-scan',)).fetchone()", + "workspace_profile = connection.execute('SELECT legacy_execution_model, legacy_reasoning_effort FROM workspaces WHERE id = ?', ('legacy-workspace',)).fetchone()", + "warnings = connection.execute('SELECT completion_warnings_json FROM scans WHERE id = ?', ('legacy-scan',)).fetchone()[0]", + "connection.execute('UPDATE scans SET model = ?, reasoning_effort = NULL WHERE id = ?', ('gpt-current', sys.argv[2]))", + "connection.execute('UPDATE scans SET reasoning_effort = ? WHERE id = ?', ('high', sys.argv[2]))", + "current_profile = connection.execute('SELECT legacy_execution_model, legacy_reasoning_effort, model, reasoning_effort FROM scans WHERE id = ?', (sys.argv[2],)).fetchone()", + "deep_scan_tables = connection.execute(\"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'deep_scan_runs'\").fetchone()", + "print(json.dumps({'columns': sorted(columns & {'deep_scan_owner_thread_id', 'continuation_thread_id', 'model', 'reasoning_effort', 'completion_warnings_json', 'legacy_execution_model', 'legacy_reasoning_effort'}), 'migrations': migrations, 'profile': dict(profile), 'workspaceProfile': dict(workspace_profile), 'warnings': json.loads(warnings), 'currentProfile': dict(current_profile), 'deepScanTables': deep_scan_tables is not None}))", + ].join("\n"), + join(stateDirectory, "workbench.sqlite3"), + String(registration["scanId"]), + ], + { encoding: "utf8" }, + ); + expect(upgraded.status).toBe(0); + expect(upgraded.stderr).toBe(""); + expect(JSON.parse(upgraded.stdout)).toEqual({ + columns: [ + "completion_warnings_json", + "continuation_thread_id", + "deep_scan_owner_thread_id", + "legacy_execution_model", + "legacy_reasoning_effort", + "model", + "reasoning_effort", + ], + migrations: { + "11": "deep scan orchestration state", + "12": "scan continuation threads", + "25": "persist scan model settings", + "26": "persist scan completion warnings", + }, + profile: { + legacy_execution_model: "gpt-legacy", + legacy_reasoning_effort: "high", + model: "gpt-legacy", + reasoning_effort: "high", + }, + workspaceProfile: { + legacy_execution_model: "gpt-workspace", + legacy_reasoning_effort: "medium", + }, + warnings: ["legacy warning"], + currentProfile: { + legacy_execution_model: null, + legacy_reasoning_effort: null, + model: "gpt-current", + reasoning_effort: "high", + }, + deepScanTables: true, + }); + }); + + test("aligns an existing public CLI database with the maintained plugin schema", async () => { + const root = await temporaryDirectory("codex-security-public-migrations-"); + const repository = join(root, "repository"); + const stateDirectory = join(root, "state"); + const scanDirectory = join(root, "scan"); + await mkdir(repository); + await mkdir(stateDirectory); + await mkdir(scanDirectory, { mode: 0o700 }); + + const python = Bun.which("python3") ?? Bun.which("python"); + expect(python).not.toBeNull(); + const fixture = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + [ + "import sqlite3, sys", + "from pathlib import Path", + "sys.path.insert(0, sys.argv[1])", + "from workbench_schema import MIGRATIONS, sql_statements", + "repository = Path(sys.argv[2])", + "connection = sqlite3.connect(Path(sys.argv[3]) / 'workbench.sqlite3')", + "connection.execute('CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)')", + "timestamp = '2026-07-30T00:00:00Z'", + "for version, name, migration in MIGRATIONS:", + " if version > 24: break", + " for statement in sql_statements(migration): connection.execute(statement)", + " connection.execute('INSERT INTO schema_migrations VALUES (?, ?, ?)', (version, name, timestamp))", + "connection.execute(\"ALTER TABLE scans ADD COLUMN completion_warnings_json TEXT NOT NULL DEFAULT '[]'\")", + "connection.execute('INSERT INTO schema_migrations VALUES (?, ?, ?)', (25, 'persist scan completion warnings', timestamp))", + "connection.execute('INSERT INTO workspaces (id, target_path, created_at, updated_at) VALUES (?, ?, ?, ?)', ('legacy-workspace', str(repository), timestamp, timestamp))", + "connection.execute('INSERT INTO scans (id, workspace_id, target_path, target_revision, scope, mode, scan_dir, status, phase, started_at, created_at, updated_at, completion_warnings_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ('legacy-scan', 'legacy-workspace', str(repository), 'legacy-revision', '.', 'standard', str(repository / 'legacy-scan'), 'complete', 'reporting', timestamp, timestamp, timestamp, '[\"existing warning\"]'))", + "connection.commit()", + "connection.close()", + ].join("\n"), + join(PLUGIN_ROOT, "scripts"), + repository, + stateDirectory, + ], + { encoding: "utf8" }, + ); + expect(fixture.status).toBe(0); + expect(fixture.stderr).toBe(""); + + const registration = await runWorkbench( + { + python: python!, + pluginRoot: PLUGIN_ROOT, + environment: { + PATH: process.env["PATH"], + CODEX_SECURITY_STATE_DIR: stateDirectory, + }, + }, + [ + "register-cli-scan", + "--repository", + repository, + "--scan-dir", + scanDirectory, + "--recipe-json", + JSON.stringify({ + config: {}, + mode: "standard", + repository, + target: { kind: "repository", paths: [] }, + }), + ], + ); + expect(registration["scanId"]).toBeString(); + + const upgraded = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + [ + "import json, sqlite3, sys", + "connection = sqlite3.connect(sys.argv[1])", + "connection.row_factory = sqlite3.Row", + "columns = {row['name'] for row in connection.execute('PRAGMA table_info(scans)')}", + "migrations = {row['version']: row['name'] for row in connection.execute('SELECT version, name FROM schema_migrations WHERE version IN (25, 26)')}", + "warnings = connection.execute('SELECT completion_warnings_json FROM scans WHERE id = ?', ('legacy-scan',)).fetchone()[0]", + "print(json.dumps({'columns': sorted(columns & {'model', 'reasoning_effort', 'completion_warnings_json'}), 'migrations': migrations, 'warnings': json.loads(warnings)}))", + ].join("\n"), + join(stateDirectory, "workbench.sqlite3"), + ], + { encoding: "utf8" }, + ); + expect(upgraded.status).toBe(0); + expect(upgraded.stderr).toBe(""); + expect(JSON.parse(upgraded.stdout)).toEqual({ + columns: ["completion_warnings_json", "model", "reasoning_effort"], + migrations: { + "25": "persist scan model settings", + "26": "persist scan completion warnings", + }, + warnings: ["existing warning"], + }); + }); + test.each([ ["all required draft artifacts", []], ["the manifest draft", ["findings.json", "coverage.json"]], From 86ea5ae5da04faaaae6b38224e3103743b782891 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 31 Jul 2026 11:21:16 -0700 Subject: [PATCH 2/2] fix: reconcile mixed legacy and released scan migrations --- .../scripts/workbench_schema.py | 47 +++++- sdk/typescript/tests-ts/runtime.test.ts | 140 ++++++++++++++++++ 2 files changed, 181 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py b/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py index 2ca8ba97..2c329cff 100644 --- a/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py +++ b/sdk/typescript/_bundled_plugin/scripts/workbench_schema.py @@ -624,7 +624,27 @@ def normalize_pre_release_migrations( connection: sqlite3.Connection, timestamp: str ) -> None: - normalize_pre_release_execution_profile_migrations(connection, timestamp) + execution_migrations = { + row["version"]: row["name"] + for row in connection.execute( + "SELECT version, name FROM schema_migrations WHERE version IN (11, 12)" + ) + } + supported_execution_migrations = { + 11: {"deep scan orchestration state", "scan execution profiles"}, + 12: { + "scan continuation threads", + "dynamic scan execution profiles", + "phase-specific scan progress", + }, + } + if any( + name not in supported_execution_migrations[version] + for version, name in execution_migrations.items() + ): + raise SystemExit( + "The Codex Security database has an unsupported execution-profile migration history." + ) phase_progress_migration = connection.execute( "SELECT name FROM schema_migrations WHERE version = 12" @@ -645,6 +665,8 @@ def normalize_pre_release_migrations( ("phase-specific scan progress",), ) + normalize_pre_release_execution_profile_migrations(connection, timestamp) + preflight_progress_migration = connection.execute( "SELECT name FROM schema_migrations WHERE version = 13" ).fetchone() @@ -718,6 +740,10 @@ def normalize_pre_release_execution_profile_migrations( 11: "scan execution profiles", 12: "dynamic scan execution profiles", } + released_names = { + 11: "deep scan orchestration state", + 12: "scan continuation threads", + } model_migration_name = "persist scan model settings" warnings_migration_name = "persist scan completion warnings" has_legacy_profile_history = execution_migrations.get(11) == legacy_names[11] @@ -742,11 +768,20 @@ def normalize_pre_release_execution_profile_migrations( return if ( - has_legacy_profile_history - and execution_migrations.get(12) not in (None, legacy_names[12]) - ) or ( - not has_legacy_profile_history - and execution_migrations.get(12) == legacy_names[12] + any( + execution_migrations.get(version) + not in (None, released_names[version], legacy_names[version]) + for version in (11, 12) + ) + or ( + has_legacy_profile_history + and execution_migrations.get(12) + not in (None, legacy_names[12], released_names[12]) + ) + or ( + not has_legacy_profile_history + and execution_migrations.get(12) == legacy_names[12] + ) ): raise SystemExit( "The Codex Security database has an unsupported execution-profile migration history." diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 4d0b53b4..7cb55da3 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -2001,6 +2001,146 @@ describe("runtime directories and plugin Python boundary", () => { }); }); + test.each([ + [ + "released continuation v12", + "scan execution profiles", + "scan continuation threads", + true, + ], + [ + "historical phase-progress v12", + "scan execution profiles", + "phase-specific scan progress", + true, + ], + [ + "unknown v11 plus released continuation v12", + "unknown execution profile migration", + "scan continuation threads", + false, + ], + ] as const)( + "reconciles %s without corrupting migration history", + async (_history, profileMigration, followUpMigration, supportedHistory) => { + const root = await temporaryDirectory( + "codex-security-migration-history-", + ); + const stateDirectory = join(root, "state"); + await mkdir(stateDirectory); + const database = join(stateDirectory, "workbench.sqlite3"); + const python = Bun.which("python3") ?? Bun.which("python"); + expect(python).not.toBeNull(); + + const fixture = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + [ + "import sqlite3, sys", + "sys.path.insert(0, sys.argv[1])", + "from workbench_schema import MIGRATIONS, sql_statements", + "connection = sqlite3.connect(sys.argv[2])", + "connection.execute('CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)')", + "timestamp = '2026-07-30T00:00:00Z'", + "for version, name, migration in MIGRATIONS:", + " if version > 10: break", + " for statement in sql_statements(migration): connection.execute(statement)", + " connection.execute('INSERT INTO schema_migrations VALUES (?, ?, ?)', (version, name, timestamp))", + "for table in ('workspaces', 'scans'):", + " connection.execute(f'ALTER TABLE {table} ADD COLUMN execution_model TEXT')", + " connection.execute(f'ALTER TABLE {table} ADD COLUMN reasoning_effort TEXT')", + "follow_up = next(item for item in MIGRATIONS if item[1] == sys.argv[4])", + "for statement in sql_statements(follow_up[2]): connection.execute(statement)", + "connection.executemany('INSERT INTO schema_migrations VALUES (?, ?, ?)', [(11, sys.argv[3], timestamp), (12, sys.argv[4], timestamp)])", + "connection.execute(\"ALTER TABLE scans ADD COLUMN completion_warnings_json TEXT NOT NULL DEFAULT '[]'\")", + "connection.execute('INSERT INTO schema_migrations VALUES (?, ?, ?)', (25, 'persist scan completion warnings', timestamp))", + "connection.commit()", + "connection.close()", + ].join("\n"), + join(PLUGIN_ROOT, "scripts"), + database, + profileMigration, + followUpMigration, + ], + { encoding: "utf8" }, + ); + expect(fixture.status).toBe(0); + expect(fixture.stderr).toBe(""); + + const upgrade = spawnSync( + python!, + [ + "-I", + "-B", + join(PLUGIN_ROOT, "scripts", "workbench_db.py"), + "database-info", + ], + { + encoding: "utf8", + env: { + ...process.env, + CODEX_SECURITY_STATE_DIR: stateDirectory, + }, + }, + ); + expect(upgrade.status).toBe(supportedHistory ? 0 : 1); + if (!supportedHistory) { + expect(upgrade.stderr).toContain( + "unsupported execution-profile migration history", + ); + } + + const inspected = spawnSync( + python!, + [ + "-I", + "-B", + "-c", + [ + "import json, sqlite3, sys", + "connection = sqlite3.connect(sys.argv[1])", + "connection.row_factory = sqlite3.Row", + "migrations = {row['version']: row['name'] for row in connection.execute('SELECT version, name FROM schema_migrations WHERE version IN (11, 12, 20, 25, 26)')}", + "columns = {row['name'] for row in connection.execute('PRAGMA table_info(scans)')}", + "deep_scan_tables = connection.execute(\"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'deep_scan_runs'\").fetchone()", + "print(json.dumps({'migrations': migrations, 'legacyColumnsRenamed': 'legacy_execution_model' in columns, 'deepScanTables': deep_scan_tables is not None}))", + ].join("\n"), + database, + ], + { encoding: "utf8" }, + ); + expect(inspected.status).toBe(0); + expect(inspected.stderr).toBe(""); + if (!supportedHistory) { + expect(JSON.parse(inspected.stdout)).toEqual({ + migrations: { + "11": "unknown execution profile migration", + "12": "scan continuation threads", + "25": "persist scan completion warnings", + }, + legacyColumnsRenamed: false, + deepScanTables: false, + }); + return; + } + + expect(JSON.parse(inspected.stdout)).toEqual({ + migrations: { + "11": "deep scan orchestration state", + "12": "scan continuation threads", + "20": "phase-specific scan progress", + "25": "persist scan model settings", + "26": "persist scan completion warnings", + }, + legacyColumnsRenamed: true, + deepScanTables: true, + }); + }, + ); + test("aligns an existing public CLI database with the maintained plugin schema", async () => { const root = await temporaryDirectory("codex-security-public-migrations-"); const repository = join(root, "repository");