fix(state): preserve additive column order (#121062)

Keep fresh shared-state table definitions aligned with SQLite ADD COLUMN ordering so current-version upgraded databases pass canonical schema validation.\n\nRefs #121061
This commit is contained in:
Peter Steinberger
2026-08-09 06:26:54 -07:00
committed by GitHub
parent cb50289e28
commit 0b4c8a9fc9
2 changed files with 46 additions and 35 deletions
+42 -31
View File
@@ -1956,39 +1956,50 @@ INSERT INTO macos_port_guardian_records VALUES (4242, 18789, '/usr/bin/ssh', 're
).toEqual([{ task_id: "task-index-repair" }]);
});
it("repairs the same-version worktree cleanup column with physical index drift", () => {
const stateDir = createTempStateDir();
const env = { OPENCLAW_STATE_DIR: stateDir };
const databasePath = materializeCurrentStateDatabase(stateDir);
it.each([
{ columnName: "run_end_cleanup_json", tableName: "worktrees" },
{ columnName: "shared_host", tableName: "worker_environments" },
])(
"appends same-version $columnName to $tableName before schema validation",
({ columnName, tableName }) => {
const stateDir = createTempStateDir();
const env = { OPENCLAW_STATE_DIR: stateDir };
const databasePath = materializeCurrentStateDatabase(stateDir);
const { DatabaseSync } = requireNodeSqlite();
const shippedSchema = new DatabaseSync(databasePath);
try {
shippedSchema.exec("ALTER TABLE worktrees DROP COLUMN run_end_cleanup_json;");
expect(readSqliteNumberPragma(shippedSchema, "user_version")).toBe(
OPENCLAW_STATE_SCHEMA_VERSION,
);
} finally {
shippedSchema.close();
}
createTaskRunStatusIndexPhysicalDrift(databasePath);
const { DatabaseSync } = requireNodeSqlite();
const shippedSchema = new DatabaseSync(databasePath);
let canonicalColumnOrder: string[];
try {
canonicalColumnOrder = (
shippedSchema.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name: string }>
).map((column) => column.name);
shippedSchema.exec(`ALTER TABLE ${tableName} DROP COLUMN ${columnName};`);
expect(readSqliteNumberPragma(shippedSchema, "user_version")).toBe(
OPENCLAW_STATE_SCHEMA_VERSION,
);
} finally {
shippedSchema.close();
}
createTaskRunStatusIndexPhysicalDrift(databasePath);
const reopened = openOpenClawStateDatabase({ env });
const columns = reopened.db.prepare("PRAGMA table_info(worktrees)").all() as Array<{
name: string;
}>;
expect(columns.map((column) => column.name)).toContain("run_end_cleanup_json");
expect(reopened.db.prepare("PRAGMA integrity_check").get()).toEqual({
integrity_check: "ok",
});
expect(
reopened.db
.prepare(
"SELECT task_id FROM task_runs INDEXED BY idx_task_runs_status WHERE status = 'running'",
)
.all(),
).toEqual([{ task_id: "task-index-repair" }]);
});
const reopened = openOpenClawStateDatabase({ env });
const columns = reopened.db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
name: string;
}>;
expect(columns.map((column) => column.name)).toEqual(canonicalColumnOrder);
expect(canonicalColumnOrder.at(-1)).toBe(columnName);
expect(reopened.db.prepare("PRAGMA integrity_check").get()).toEqual({
integrity_check: "ok",
});
expect(
reopened.db
.prepare(
"SELECT task_id FROM task_runs INDEXED BY idx_task_runs_status WHERE status = 'running'",
)
.all(),
).toEqual([{ task_id: "task-index-repair" }]);
},
);
it("does not add Claw bootstrap columns before rejecting unrelated index corruption", () => {
const stateDir = createTempStateDir();
+4 -4
View File
@@ -1801,10 +1801,10 @@ CREATE TABLE IF NOT EXISTS worktrees (
owner_id TEXT,
snapshot_ref TEXT,
provisioned_paths_json TEXT,
run_end_cleanup_json TEXT,
created_at INTEGER NOT NULL,
last_active_at INTEGER NOT NULL,
removed_at INTEGER
removed_at INTEGER,
run_end_cleanup_json TEXT
) STRICT;
CREATE INDEX IF NOT EXISTS idx_worktrees_repo_fingerprint
@@ -1846,7 +1846,6 @@ CREATE TABLE IF NOT EXISTS worker_environments (
profile_snapshot_json TEXT NOT NULL,
provision_operation_id TEXT NOT NULL UNIQUE,
lease_id TEXT,
shared_host INTEGER CHECK (shared_host IN (0, 1)),
ssh_host TEXT,
ssh_port INTEGER CHECK (ssh_port IS NULL OR (ssh_port >= 1 AND ssh_port <= 65535)),
ssh_user TEXT,
@@ -1878,7 +1877,8 @@ CREATE TABLE IF NOT EXISTS worker_environments (
state_changed_at_ms INTEGER NOT NULL,
idle_since_at_ms INTEGER,
destroy_requested_at_ms INTEGER,
last_error TEXT
last_error TEXT,
shared_host INTEGER CHECK (shared_host IN (0, 1))
) STRICT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_worker_environments_provider_lease