mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
81238d0c0e
commit
dedfc01628
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user