mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
1c80cf86cf
* fix(release): trust ancestor validation evidence * fix(release): read candidate auth schema
243 lines
7.6 KiB
TypeScript
243 lines
7.6 KiB
TypeScript
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
assertNoLegacyPrimaryAuthRows,
|
|
readSharedAuthProfileStoreText,
|
|
} from "../../scripts/e2e/lib/auth-profile-store-assertions.mjs";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
function makeStateDir(): string {
|
|
const root = tempDirs.make("openclaw-auth-profile-assertions-");
|
|
return path.join(root, ".openclaw");
|
|
}
|
|
|
|
function writeSharedDatabase(
|
|
stateDir: string,
|
|
options: {
|
|
asView?: boolean;
|
|
legacyStoreJson?: string;
|
|
schemaVersion?: 12 | 13;
|
|
storeJson?: string;
|
|
} = {},
|
|
): string {
|
|
const dbPath = path.join(stateDir, "state", "openclaw.sqlite");
|
|
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
const db = new DatabaseSync(dbPath);
|
|
try {
|
|
const schemaVersion = options.schemaVersion ?? 13;
|
|
db.exec(`PRAGMA user_version = ${schemaVersion};`);
|
|
const table = schemaVersion >= 13 ? "config_machine_state" : "auth_profile_stores";
|
|
if (options.asView) {
|
|
if (table === "config_machine_state") {
|
|
db.exec(`
|
|
CREATE VIEW config_machine_state AS
|
|
SELECT 'authProfiles.store' AS state_key, '{}' AS value_json, 1 AS updated_at_ms;
|
|
`);
|
|
} else {
|
|
db.exec(`
|
|
CREATE VIEW auth_profile_stores AS
|
|
SELECT 'shared' AS store_key, '{}' AS store_json, 1 AS updated_at;
|
|
`);
|
|
}
|
|
} else {
|
|
if (table === "config_machine_state") {
|
|
db.exec(`
|
|
CREATE TABLE config_machine_state (
|
|
state_key TEXT NOT NULL PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at_ms INTEGER NOT NULL
|
|
) STRICT;
|
|
`);
|
|
db.prepare("INSERT INTO config_machine_state VALUES (?, ?, ?)").run(
|
|
"authProfiles.store",
|
|
options.storeJson ?? "{}",
|
|
Date.now(),
|
|
);
|
|
} else {
|
|
db.exec(`
|
|
CREATE TABLE auth_profile_stores (
|
|
store_key TEXT NOT NULL PRIMARY KEY,
|
|
store_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
) STRICT;
|
|
`);
|
|
db.prepare("INSERT INTO auth_profile_stores VALUES (?, ?, ?)").run(
|
|
"shared",
|
|
options.storeJson ?? "{}",
|
|
Date.now(),
|
|
);
|
|
}
|
|
}
|
|
if (options.legacyStoreJson !== undefined) {
|
|
db.exec(`
|
|
CREATE TABLE auth_profile_stores (
|
|
store_key TEXT NOT NULL PRIMARY KEY,
|
|
store_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
) STRICT;
|
|
`);
|
|
db.prepare("INSERT INTO auth_profile_stores VALUES (?, ?, ?)").run(
|
|
"shared",
|
|
options.legacyStoreJson,
|
|
Date.now(),
|
|
);
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
return dbPath;
|
|
}
|
|
|
|
function writeAgentDatabase(
|
|
stateDir: string,
|
|
options: {
|
|
stateKeys?: string[];
|
|
storeKeys?: string[];
|
|
storeAsView?: boolean;
|
|
} = {},
|
|
): string {
|
|
const dbPath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite");
|
|
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
const db = new DatabaseSync(dbPath);
|
|
try {
|
|
if (options.storeAsView) {
|
|
db.exec(`
|
|
CREATE VIEW auth_profile_store AS
|
|
SELECT 'primary' AS store_key, '{}' AS store_json, 1 AS updated_at;
|
|
`);
|
|
} else {
|
|
db.exec(`
|
|
CREATE TABLE auth_profile_store (
|
|
store_key TEXT NOT NULL PRIMARY KEY,
|
|
store_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
) STRICT;
|
|
`);
|
|
for (const key of options.storeKeys ?? []) {
|
|
db.prepare("INSERT INTO auth_profile_store VALUES (?, '{}', ?)").run(key, Date.now());
|
|
}
|
|
}
|
|
db.exec(`
|
|
CREATE TABLE auth_profile_state (
|
|
state_key TEXT NOT NULL PRIMARY KEY,
|
|
state_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
) STRICT;
|
|
`);
|
|
for (const key of options.stateKeys ?? []) {
|
|
db.prepare("INSERT INTO auth_profile_state VALUES (?, '{}', ?)").run(key, Date.now());
|
|
}
|
|
} finally {
|
|
db.close();
|
|
}
|
|
return dbPath;
|
|
}
|
|
|
|
describe("auth profile store E2E assertions", () => {
|
|
it("reads the canonical shared row", () => {
|
|
const stateDir = makeStateDir();
|
|
writeSharedDatabase(stateDir, { storeJson: '{"version":1}' });
|
|
|
|
expect(readSharedAuthProfileStoreText(stateDir)).toBe('{"version":1}');
|
|
});
|
|
|
|
it("reads the release-owned shared row before schema v13", () => {
|
|
const stateDir = makeStateDir();
|
|
writeSharedDatabase(stateDir, {
|
|
schemaVersion: 12,
|
|
storeJson: '{"version":1,"schema":12}',
|
|
});
|
|
|
|
expect(readSharedAuthProfileStoreText(stateDir)).toBe('{"version":1,"schema":12}');
|
|
});
|
|
|
|
it("does not accept a retired shared row for schema v13", () => {
|
|
const stateDir = makeStateDir();
|
|
writeSharedDatabase(stateDir, {
|
|
legacyStoreJson: '{"version":1,"schema":12}',
|
|
storeJson: "",
|
|
});
|
|
|
|
expect(readSharedAuthProfileStoreText(stateDir)).toBe("");
|
|
});
|
|
|
|
it("returns empty when the shared database or table is absent", () => {
|
|
const stateDir = makeStateDir();
|
|
|
|
expect(readSharedAuthProfileStoreText(stateDir)).toBe("");
|
|
const dbPath = path.join(stateDir, "state", "openclaw.sqlite");
|
|
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
new DatabaseSync(dbPath).close();
|
|
expect(readSharedAuthProfileStoreText(stateDir)).toBe("");
|
|
});
|
|
|
|
it("fails closed for a corrupt shared database", () => {
|
|
const stateDir = makeStateDir();
|
|
const dbPath = path.join(stateDir, "state", "openclaw.sqlite");
|
|
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
writeFileSync(dbPath, "not sqlite");
|
|
|
|
expect(() => readSharedAuthProfileStoreText(stateDir)).toThrow(
|
|
"could not read the shared auth profile store",
|
|
);
|
|
});
|
|
|
|
it("fails closed when the shared auth table is replaced by a view", () => {
|
|
const stateDir = makeStateDir();
|
|
writeSharedDatabase(stateDir, { asView: true });
|
|
|
|
expect(() => readSharedAuthProfileStoreText(stateDir)).toThrow(
|
|
"config_machine_state is view, not a table",
|
|
);
|
|
});
|
|
|
|
it("permits unrelated main-agent auth rows", () => {
|
|
const stateDir = makeStateDir();
|
|
writeAgentDatabase(stateDir, {
|
|
stateKeys: ["last-good"],
|
|
storeKeys: ["workspace"],
|
|
});
|
|
|
|
expect(() => assertNoLegacyPrimaryAuthRows(stateDir)).not.toThrow();
|
|
});
|
|
|
|
it.each(["auth_profile_store", "auth_profile_state"] as const)(
|
|
"rejects a retired primary row in %s",
|
|
(table) => {
|
|
const stateDir = makeStateDir();
|
|
writeAgentDatabase(stateDir, {
|
|
stateKeys: table === "auth_profile_state" ? ["primary"] : [],
|
|
storeKeys: table === "auth_profile_store" ? ["primary"] : [],
|
|
});
|
|
|
|
expect(() => assertNoLegacyPrimaryAuthRows(stateDir)).toThrow(
|
|
`onboard preserved a retired primary row in ${table}`,
|
|
);
|
|
},
|
|
);
|
|
|
|
it("fails closed for a corrupt main-agent database", () => {
|
|
const stateDir = makeStateDir();
|
|
const dbPath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite");
|
|
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
writeFileSync(dbPath, "not sqlite");
|
|
|
|
expect(() => assertNoLegacyPrimaryAuthRows(stateDir)).toThrow(
|
|
"could not validate the main-agent auth database",
|
|
);
|
|
});
|
|
|
|
it("fails closed when a retired auth table is replaced by a view", () => {
|
|
const stateDir = makeStateDir();
|
|
writeAgentDatabase(stateDir, { storeAsView: true });
|
|
|
|
expect(() => assertNoLegacyPrimaryAuthRows(stateDir)).toThrow(
|
|
"auth_profile_store is view, not a table",
|
|
);
|
|
});
|
|
});
|