fix(test): stage live auth profiles from SQLite (#113651)

* fix(test): stage live auth from SQLite

Punchcard-Session: cobalt-cedar-timber-04

* fix(test): snapshot staged auth atomically

Punchcard-Session: cobalt-cedar-timber-04

* fix(test): fail closed on partial auth schema

Punchcard-Session: calm-cedar-river-aa

* fix(test): resolve live auth stage path lazily

Punchcard-Session: calm-cedar-river-aa

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Peter Steinberger
2026-08-05 02:12:09 -07:00
committed by GitHub
parent 81238d0c0e
commit dedfc01628
6 changed files with 343 additions and 30 deletions
@@ -20,6 +20,7 @@ import { withEnvAsync } from "../test-utils/env.js";
import { resolveAgentDir } from "./agent-scope.js";
import { loadPersistedAuthProfileStore } from "./auth-profiles/persisted.js";
import {
inspectPersistedAuthProfileStateRaw,
inspectPersistedAuthProfileStoreRaw,
resolveAuthProfileDatabasePath,
} from "./auth-profiles/sqlite.js";
@@ -179,6 +180,31 @@ describe("auth profile sqlite store", () => {
});
});
it("classifies each missing auth table through an existing database handle", async () => {
await withAgentDirEnv("openclaw-auth-sqlite-partial-schema-", (agentDir) => {
const database = new DatabaseSync(resolveAuthProfileDatabasePath(agentDir));
database.exec(`
CREATE TABLE auth_profile_store (
store_key TEXT NOT NULL PRIMARY KEY,
store_json TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
try {
expect(inspectPersistedAuthProfileStoreRaw(agentDir, { db: database })).toEqual({
status: "missing",
reason: "row",
});
expect(inspectPersistedAuthProfileStateRaw(agentDir, { db: database })).toEqual({
status: "missing",
reason: "table",
});
} finally {
database.close();
}
});
});
it("rejects a newer agent database that has no current auth table", async () => {
await withAgentDirEnv("openclaw-auth-sqlite-newer-schema-", (agentDir) => {
const database = new DatabaseSync(resolveAuthProfileDatabasePath(agentDir));
+22 -14
View File
@@ -101,10 +101,30 @@ function getAuthProfileKysely(db: DatabaseSync) {
return getNodeSqliteKysely<AuthProfileDatabase>(db);
}
function inspectAuthProfileTable(
db: DatabaseSync,
target: "store" | "state",
): PersistedAuthProfileStoreInspection | null {
const tableName = target === "store" ? "auth_profile_store" : "auth_profile_state";
const schemaObject = db
.prepare("SELECT type FROM sqlite_master WHERE name = ?")
.get(tableName) as { type?: unknown } | undefined;
if (!schemaObject) {
// Agent databases shipped before SQLite auth storage do not have these
// additive tables until their next writable bootstrap.
return { status: "missing", reason: "table" };
}
return schemaObject.type === "table" ? null : { status: "unreadable" };
}
function inspectAuthProfileJsonCell(
db: DatabaseSync,
target: "store" | "state",
): PersistedAuthProfileStoreInspection {
const tableInspection = inspectAuthProfileTable(db, target);
if (tableInspection) {
return tableInspection;
}
const kysely = getAuthProfileKysely(db);
let raw: string;
if (target === "store") {
@@ -153,18 +173,6 @@ function inspectAuthProfileJsonCellReadOnly(
if (readSqliteUserVersion(db) > OPENCLAW_AGENT_SCHEMA_VERSION) {
return { status: "unreadable" };
}
const tableName = target === "store" ? "auth_profile_store" : "auth_profile_state";
const schemaObject = db
.prepare("SELECT type FROM sqlite_master WHERE name = ?")
.get(tableName) as { type?: unknown } | undefined;
if (!schemaObject) {
// Agent databases shipped before SQLite auth storage do not have these
// additive tables until their next writable bootstrap.
return { status: "missing", reason: "table" };
}
if (schemaObject.type !== "table") {
return { status: "unreadable" };
}
return inspectAuthProfileJsonCell(db, target);
} catch {
return { status: "unreadable" };
@@ -184,7 +192,7 @@ function readAuthProfileJsonCellReadOnly(pathname: string, target: "store" | "st
/** Distinguishes an absent auth row from a present store that could not be read. */
export function inspectPersistedAuthProfileStoreRaw(
agentDir?: string,
database?: OpenClawAgentDatabase,
database?: Pick<OpenClawAgentDatabase, "db">,
): PersistedAuthProfileStoreInspection {
if (database) {
return inspectAuthProfileJsonCell(database.db, "store");
@@ -199,7 +207,7 @@ export function inspectPersistedAuthProfileStoreRaw(
/** Distinguishes an absent auth-state row from state that could not be read. */
export function inspectPersistedAuthProfileStateRaw(
agentDir?: string,
database?: OpenClawAgentDatabase,
database?: Pick<OpenClawAgentDatabase, "db">,
): PersistedAuthProfileStoreInspection {
if (database) {
return inspectAuthProfileJsonCell(database.db, "state");
@@ -0,0 +1,123 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it } from "vitest";
import {
inspectPersistedAuthProfileStateRaw,
inspectPersistedAuthProfileStoreRaw,
resolveAuthProfileDatabasePath,
runAuthProfileWriteTransaction,
writePersistedAuthProfileStateRaw,
writePersistedAuthProfileStoreRaw,
} from "../../src/agents/auth-profiles/sqlite.js";
import { closeOpenClawAgentDatabasesForTest } from "../../src/state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../src/state/openclaw-state-db.js";
import { stageLiveAuthProfiles } from "./stage-live-auth-profiles.js";
const tempDirs = new Set<string>();
function createStateDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.add(dir);
return dir;
}
function createAuthSource(stateDir: string): string {
const agentDir = path.join(stateDir, "agents", "main", "agent");
runAuthProfileWriteTransaction(
agentDir,
(database) => {
writePersistedAuthProfileStoreRaw(
{
version: 1,
profiles: {
"openai:test": {
type: "api_key",
provider: "openai",
keyRef: { source: "env", provider: "default", id: "OPENCLAW_LIVE_OPENAI_KEY" },
},
},
},
agentDir,
database,
);
writePersistedAuthProfileStateRaw(
{ version: 1, order: { openai: ["openai:test"] } },
agentDir,
database,
);
},
{ stateDir },
);
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
return agentDir;
}
afterEach(() => {
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.clear();
});
describe("stage-live-auth-profiles", () => {
it.each(["auth_profile_store", "auth_profile_state"] as const)(
"fails closed when %s is the only missing auth table",
(missingTable) => {
const sourceStateDir = createStateDir("openclaw-live-auth-partial-source-");
const targetStateDir = createStateDir("openclaw-live-auth-partial-target-");
const sourceAgentDir = createAuthSource(sourceStateDir);
const database = new DatabaseSync(resolveAuthProfileDatabasePath(sourceAgentDir));
database.exec(`DROP TABLE ${missingTable};`);
database.close();
expect(() => stageLiveAuthProfiles(sourceStateDir, targetStateDir)).toThrow(
"canonical auth schema is incomplete",
);
expect(
fs.existsSync(
resolveAuthProfileDatabasePath(path.join(targetStateDir, "agents", "main", "agent")),
),
).toBe(false);
},
);
it("fails closed when both auth tables are absent", () => {
const sourceStateDir = createStateDir("openclaw-live-auth-legacy-source-");
const targetStateDir = createStateDir("openclaw-live-auth-legacy-target-");
const sourceAgentDir = createAuthSource(sourceStateDir);
const database = new DatabaseSync(resolveAuthProfileDatabasePath(sourceAgentDir));
database.exec("DROP TABLE auth_profile_store; DROP TABLE auth_profile_state;");
database.close();
expect(() => stageLiveAuthProfiles(sourceStateDir, targetStateDir)).toThrow(
"canonical auth schema is incomplete",
);
expect(
fs.existsSync(
resolveAuthProfileDatabasePath(path.join(targetStateDir, "agents", "main", "agent")),
),
).toBe(false);
});
it("stages a readable store when the state row is absent", () => {
const sourceStateDir = createStateDir("openclaw-live-auth-row-source-");
const targetStateDir = createStateDir("openclaw-live-auth-row-target-");
const sourceAgentDir = createAuthSource(sourceStateDir);
const database = new DatabaseSync(resolveAuthProfileDatabasePath(sourceAgentDir));
database.exec("DELETE FROM auth_profile_state;");
database.close();
expect(() => stageLiveAuthProfiles(sourceStateDir, targetStateDir)).not.toThrow();
const targetAgentDir = path.join(targetStateDir, "agents", "main", "agent");
expect(inspectPersistedAuthProfileStoreRaw(targetAgentDir).status).toBe("readable");
expect(inspectPersistedAuthProfileStateRaw(targetAgentDir)).toEqual({
status: "missing",
reason: "row",
});
});
});
+100
View File
@@ -0,0 +1,100 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
inspectPersistedAuthProfileStateRaw,
inspectPersistedAuthProfileStoreRaw,
resolveAuthProfileDatabaseOwnerId,
resolveAuthProfileDatabasePath,
runAuthProfileWriteTransaction,
writePersistedAuthProfileStateRaw,
writePersistedAuthProfileStoreRaw,
} from "../../src/agents/auth-profiles/sqlite.js";
import { withOpenClawAgentDatabaseReadOnly } from "../../src/state/openclaw-agent-db-readonly.js";
export function stageLiveAuthProfiles(realStateDir: string, tempStateDir: string): void {
const agentsDir = path.join(realStateDir, "agents");
if (!fs.existsSync(agentsDir)) {
return;
}
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const sourceAgentDir = path.join(agentsDir, entry.name, "agent");
const sourceDatabasePath = resolveAuthProfileDatabasePath(sourceAgentDir);
const sourceSnapshot = withOpenClawAgentDatabaseReadOnly(
(database) => {
database.db.exec("BEGIN");
try {
const snapshot = {
store: inspectPersistedAuthProfileStoreRaw(sourceAgentDir, database),
state: inspectPersistedAuthProfileStateRaw(sourceAgentDir, database),
};
database.db.exec("COMMIT");
return snapshot;
} catch (error) {
if (database.db.isTransaction) {
database.db.exec("ROLLBACK");
}
throw error;
}
},
{
agentId: resolveAuthProfileDatabaseOwnerId(sourceAgentDir),
path: sourceDatabasePath,
},
);
if (!sourceSnapshot.found) {
if (sourceSnapshot.reason === "schema-missing") {
throw new Error(
`Could not safely stage SQLite auth profiles for live agent "${entry.name}".`,
);
}
continue;
}
const sourceStore = sourceSnapshot.value.store;
const sourceState = sourceSnapshot.value.state;
if (sourceStore.status === "unreadable" || sourceState.status === "unreadable") {
throw new Error(
`Could not safely stage SQLite auth profiles for live agent "${entry.name}".`,
);
}
const storeTableMissing = sourceStore.status === "missing" && sourceStore.reason === "table";
const stateTableMissing = sourceState.status === "missing" && sourceState.reason === "table";
if (storeTableMissing || stateTableMissing) {
throw new Error(
`Could not safely stage SQLite auth profiles for live agent "${entry.name}": canonical auth schema is incomplete.`,
);
}
if (sourceStore.status !== "readable" && sourceState.status !== "readable") {
continue;
}
const targetAgentDir = path.join(tempStateDir, "agents", entry.name, "agent");
fs.mkdirSync(targetAgentDir, { recursive: true });
// Copy only canonical auth rows; cloning the agent database would expose
// unrelated sessions to the isolated live-test home.
runAuthProfileWriteTransaction(
targetAgentDir,
(database) => {
if (sourceStore.status === "readable") {
writePersistedAuthProfileStoreRaw(sourceStore.raw, targetAgentDir, database);
}
if (sourceState.status === "readable") {
writePersistedAuthProfileStateRaw(sourceState.raw, targetAgentDir, database);
}
},
{ stateDir: tempStateDir },
);
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
const [realStateDir, tempStateDir] = process.argv.slice(2);
if (!realStateDir || !tempStateDir) {
throw new Error("Expected source and target state directories.");
}
stageLiveAuthProfiles(realStateDir, tempStateDir);
}
+57 -8
View File
@@ -3,6 +3,17 @@ import fs from "node:fs";
import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
inspectPersistedAuthProfileStateRaw,
inspectPersistedAuthProfileStoreRaw,
resolveAuthProfileDatabasePath,
runAuthProfileWriteTransaction,
writePersistedAuthProfileStateRaw,
writePersistedAuthProfileStoreRaw,
} from "../src/agents/auth-profiles/sqlite.js";
import { closeOpenClawAgentDatabaseByPath } from "../src/state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseByPath } from "../src/state/openclaw-state-db.js";
import { resolveOpenClawStateSqlitePath } from "../src/state/openclaw-state-db.paths.js";
import { deleteTestEnvValue, setTestEnvValue } from "../src/test-utils/env.js";
import { cleanupTempDirs, makeTempDir } from "./helpers/temp-dir.js";
import { installTestEnv } from "./test-env.js";
@@ -121,10 +132,43 @@ describe("installTestEnv", () => {
path.join(openClawHome, ".openclaw", "external-plugins", "glueclaw", "openclaw.plugin.json"),
'{"id":"glueclaw"}\n',
);
writeFile(
path.join(openClawHome, ".openclaw", "agents", "main", "agent", "auth-profiles.json"),
JSON.stringify({ version: 1, profiles: { default: { provider: "openai" } } }, null, 2),
const realStateDir = path.join(openClawHome, ".openclaw");
const realAgentDir = path.join(realStateDir, "agents", "main", "agent");
const liveAuthStore = {
version: 1,
profiles: {
"openai:api-key": {
type: "api_key",
provider: "openai",
keyRef: {
source: "env",
provider: "default",
id: "OPENCLAW_LIVE_OPENAI_KEY",
},
},
},
};
const liveAuthState = {
version: 1,
order: { openai: ["openai:api-key"] },
};
runAuthProfileWriteTransaction(
realAgentDir,
(database) => {
writePersistedAuthProfileStoreRaw(liveAuthStore, realAgentDir, database);
writePersistedAuthProfileStateRaw(liveAuthState, realAgentDir, database);
},
{ stateDir: realStateDir },
);
cleanupFns.push(() => {
closeOpenClawAgentDatabaseByPath(resolveAuthProfileDatabasePath(realAgentDir));
closeOpenClawStateDatabaseByPath(
resolveOpenClawStateSqlitePath({
...process.env,
OPENCLAW_STATE_DIR: realStateDir,
}),
);
});
writeFile(path.join(realHome, ".claude", ".credentials.json"), '{"accessToken":"token"}\n');
writeFile(path.join(realHome, ".claude", "projects", "old-session.jsonl"), "session\n");
fs.mkdirSync(path.join(realHome, ".claude", "settings.local.json"), { recursive: true });
@@ -232,11 +276,16 @@ describe("installTestEnv", () => {
),
),
).toBe(true);
expect(
fs.existsSync(
path.join(testEnv.tempHome, ".openclaw", "agents", "main", "agent", "auth-profiles.json"),
),
).toBe(true);
const stagedAgentDir = path.join(testEnv.tempHome, ".openclaw", "agents", "main", "agent");
expect(inspectPersistedAuthProfileStoreRaw(stagedAgentDir)).toEqual({
status: "readable",
raw: liveAuthStore,
});
expect(inspectPersistedAuthProfileStateRaw(stagedAgentDir)).toEqual({
status: "readable",
raw: liveAuthState,
});
expect(fs.existsSync(path.join(stagedAgentDir, "auth-profiles.json"))).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", ".credentials.json"))).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", "projects"))).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", "settings.local.json"))).toBe(
+15 -8
View File
@@ -399,14 +399,21 @@ function copyLiveAuthProfiles(realStateDir: string, tempStateDir: string): void
if (!fs.existsSync(agentsDir)) {
return;
}
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const sourcePath = path.join(agentsDir, entry.name, "agent", "auth-profiles.json");
const targetPath = path.join(tempStateDir, "agents", entry.name, "agent", "auth-profiles.json");
copyFileIfExists(sourcePath, targetPath);
}
const liveAuthStageScript = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"helpers",
"stage-live-auth-profiles.ts",
);
// Live workers need canonical SQLite auth without loading the database stack
// into every hermetic Vitest worker.
execFileSync(
process.execPath,
["--import", "tsx", liveAuthStageScript, realStateDir, tempStateDir],
{
env: { ...process.env, NODE_OPTIONS: undefined },
stdio: "pipe",
},
);
}
function stageLiveTestState(params: {