mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
1ea2640f54
* refactor(state): make cron and subagent rows JSON-canonical * refactor(state): make gateway origin device tokens canonical at v13 The lazy ensure predates the table joining the canonical schema; at the v13 bump the schema owns creation, so the feature-local DDL, WeakSet dedupe, and lazy-list entry retire. The legacy-file guard the ensure carried stays at each call site. * test: drop obsolete lazy-ensure coverage for origin device tokens The table is canonical at v13; same-version lazy creation no longer exists to protect. Origin CRUD, isolation, and rotation coverage remains in the surviving cases. * refactor(state): fold installed_plugin_index into config_machine_state The singleton index row becomes one JSON value under plugins.installedIndex with its rollback-fencing revision inside the value; reads, CAS restore, and the lease-held write transactions use direct Kysely on config_machine_state so the state_leases assertion stays in-transaction. The v13 migration imports the row and drops the table; the additive workspace_dir entry folds with it. Doctor guidance, docker staging, and the e2e probes name the machine-state row. * refactor(state): merge workspace_attestations into workspace_setup_state One row per workspace now carries both setup milestones and the attestation clock: nullable setup columns represent attestation-only workspaces (replaceWorkspaceAttestation can precede any setup write) and setupExists derives from a non-null version. The bootstrap-hash FK repoints to the merged table; migration receipts keep the historical workspace_attestations discriminator string. The v13 migration grows and rebuilds the table, merges attestation rows (orphans without a path alias drop — their hashes re-derive at the next bootstrap attestation), and the consolidation kind is renamed state-consolidation-v13 to cover the batch. * test(state): cover the workspace merge and consolidation fallout The v12-to-v13 regression seeds merged, attestation-only, and orphan attestation workspaces; the 13-to-12 downgrade fixture recreates workspace_attestations and installed_plugin_index from the folded data; the fold-in migration gates the additive workspace_dir column for pre-additive rows; the workspace merge now triggers on the setup table's own shape so stable-era databases without an attestations table still reshape; the consolidation applied-message covers the batch. * refactor(state): fold shared auth profile singletons into config_machine_state The shared-state auth_profile_stores/auth_profile_state rows (fixed key 'shared') become authProfiles.store/authProfiles.state machine-state values; the agent-DB tables of the same names are untouched. Git-backup redaction moves from table-drop to the authProfiles. secret prefix with seeded-secret absence proof; migration receipts keep the historical table-name discriminators; the shared-auth relocation and receipt verification project the KV cells back to the receipt-era row shapes so persisted digests stay byte-compatible. mcp_oauth_stores stays a table — its multi-key fold is a named follow-up. * test(state): finish shared-auth fold coverage and annotate boundary casts Auth seeders and assertions across the e2e/scripts/secrets suites target the authProfiles machine-state cells; the v12-to-v13 regression proves payload-byte fidelity, non-shared-row drop, and insert-if-absent precedence; the downgrade fixture recreates and repopulates both v12 tables. Boundary type assertions in the plugin-index store carry SAFETY invariants per the ratchet. * chore: shrink assertion-safety baseline for plugin-index store * refactor(doctor): delete the dead onboarding-recommendations migration Its input — the unscoped 'primary' onboarding row — existed only between9a93a52a8aand473962b7de, a two-day beta window; no shipped stable can produce it and the runtime table folded away at v12. The audit backup list keeps recognizing system-agent.jsonl artifacts because beta installs that ran that import may still carry its backups. * docs: sync the 13-to-12 downgrade example with the executable fixture * style: format the synced downgrade example * style: drop unused import and duplicate union constituent * fix(state): keep orphan attestations across the v13 workspace merge The merged workspace_setup_state required a workspace path, but legacy orphan hashed-key attestations never recorded one. workspace_path is now nullable (setup rows still enforce it via CHECK), the v13 migration and the doctor file import keep orphans with a NULL path that heals on the next live access, and the 13-to-12 downgrade keeps attestation-owned hashes. Doctor test seeds move to the folded KV row. * perf(state): retire unused cron indexes * fix(state): preserve v13 migration recovery * fix(state): preserve v12 lazy-table upgrade * docs(state): document v13 auth relocation --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
235 lines
7.2 KiB
TypeScript
235 lines
7.2 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
|
|
|
|
const HELPER = path.resolve("scripts/e2e/lib/release-plugin-marketplace/lifecycle-assertions.mjs");
|
|
const tempDirs = new Set<string>();
|
|
|
|
afterEach(() => {
|
|
cleanupTempDirs(tempDirs);
|
|
});
|
|
|
|
function writeIndex(
|
|
stateDir: string,
|
|
pluginId: string,
|
|
record: Record<string, unknown>,
|
|
packageVersion: string,
|
|
) {
|
|
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
|
|
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
|
|
const db = new DatabaseSync(databasePath);
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS config_machine_state (
|
|
state_key TEXT NOT NULL PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at_ms INTEGER NOT NULL
|
|
);
|
|
`);
|
|
const now = Date.now();
|
|
const valueJson = JSON.stringify({
|
|
revision: now,
|
|
index: {
|
|
version: 1,
|
|
hostContractVersion: "test",
|
|
compatRegistryVersion: "test",
|
|
migrationVersion: 1,
|
|
policyHash: "test",
|
|
generatedAtMs: now,
|
|
refreshReason: "source-changed",
|
|
installRecords: { [pluginId]: record },
|
|
plugins: [{ pluginId, packageVersion }],
|
|
diagnostics: [],
|
|
},
|
|
});
|
|
db.prepare(
|
|
`
|
|
INSERT INTO config_machine_state (state_key, value_json, updated_at_ms)
|
|
VALUES ('plugins.installedIndex', ?, ?)
|
|
ON CONFLICT(state_key) DO UPDATE SET
|
|
value_json = excluded.value_json,
|
|
updated_at_ms = excluded.updated_at_ms
|
|
`,
|
|
).run(valueJson, now);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
function runHelper(home: string, args: string[]) {
|
|
const stateDir = path.join(home, ".openclaw");
|
|
return spawnSync(process.execPath, [HELPER, ...args], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
HOME: home,
|
|
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
|
|
OPENCLAW_STATE_DIR: stateDir,
|
|
},
|
|
});
|
|
}
|
|
|
|
function writeMarketplaceState(home: string, version: string) {
|
|
const pluginId = "release-marketplace-plugin";
|
|
const stateDir = path.join(home, ".openclaw");
|
|
const installPath = path.join(stateDir, "extensions", pluginId);
|
|
fs.mkdirSync(installPath, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(installPath, "package.json"),
|
|
`${JSON.stringify({ name: `@openclaw/${pluginId}`, version })}\n`,
|
|
"utf8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(stateDir, "openclaw.json"),
|
|
`${JSON.stringify({
|
|
plugins: { entries: { [pluginId]: { enabled: true } } },
|
|
})}\n`,
|
|
"utf8",
|
|
);
|
|
writeIndex(
|
|
stateDir,
|
|
pluginId,
|
|
{
|
|
source: "marketplace",
|
|
installPath,
|
|
version,
|
|
marketplaceName: "Release Fixture Marketplace",
|
|
marketplaceSource: "release-fixtures",
|
|
marketplacePlugin: pluginId,
|
|
},
|
|
version,
|
|
);
|
|
return { installPath, pluginId };
|
|
}
|
|
|
|
function clearMarketplaceIndex(home: string) {
|
|
const databasePath = path.join(home, ".openclaw", "state", "openclaw.sqlite");
|
|
const db = new DatabaseSync(databasePath);
|
|
try {
|
|
db.prepare(
|
|
`
|
|
UPDATE config_machine_state
|
|
SET value_json = json_set(
|
|
value_json,
|
|
'$.index.installRecords', json('{}'),
|
|
'$.index.plugins', json('[]')
|
|
),
|
|
updated_at_ms = ?
|
|
WHERE state_key = 'plugins.installedIndex'
|
|
`,
|
|
).run(Date.now());
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
describe("release plugin marketplace lifecycle assertions", () => {
|
|
it("checks canonical marketplace metadata and stable managed install paths", () => {
|
|
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
|
|
const { installPath, pluginId } = writeMarketplaceState(home, "0.0.1");
|
|
const installPathFile = path.join(home, "install-path.txt");
|
|
|
|
const initial = runHelper(home, [
|
|
"assert-marketplace-state",
|
|
pluginId,
|
|
"0.0.1",
|
|
"release-fixtures",
|
|
pluginId,
|
|
installPathFile,
|
|
]);
|
|
expect(initial.stderr).toBe("");
|
|
expect(initial.status).toBe(0);
|
|
expect(fs.readFileSync(installPathFile, "utf8").trim()).toBe(installPath);
|
|
|
|
writeMarketplaceState(home, "0.0.2");
|
|
const updated = runHelper(home, [
|
|
"assert-marketplace-state",
|
|
pluginId,
|
|
"0.0.2",
|
|
"release-fixtures",
|
|
pluginId,
|
|
installPathFile,
|
|
]);
|
|
expect(updated.stderr).toBe("");
|
|
expect(updated.status).toBe(0);
|
|
});
|
|
|
|
it("rejects marketplace state with the wrong persisted version", () => {
|
|
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
|
|
const { pluginId } = writeMarketplaceState(home, "0.0.1");
|
|
|
|
const result = runHelper(home, [
|
|
"assert-marketplace-state",
|
|
pluginId,
|
|
"0.0.2",
|
|
"release-fixtures",
|
|
pluginId,
|
|
path.join(home, "install-path.txt"),
|
|
]);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("expected install record version 0.0.2, got 0.0.1");
|
|
});
|
|
|
|
it("seeds uninstall state and verifies complete cleanup with sentinels preserved", () => {
|
|
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
|
|
const { installPath, pluginId } = writeMarketplaceState(home, "0.0.2");
|
|
const sentinelPluginId = "release-marketplace-other";
|
|
const sentinelPath = path.join(home, "marketplace", sentinelPluginId);
|
|
const installPathFile = path.join(home, "install-path.txt");
|
|
fs.mkdirSync(sentinelPath, { recursive: true });
|
|
|
|
const seeded = runHelper(home, [
|
|
"seed-marketplace-uninstall-state",
|
|
pluginId,
|
|
sentinelPluginId,
|
|
sentinelPath,
|
|
installPathFile,
|
|
]);
|
|
expect(seeded.stderr).toBe("");
|
|
expect(seeded.status).toBe(0);
|
|
|
|
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
expect(config.plugins.allow).toEqual([pluginId, sentinelPluginId]);
|
|
expect(config.plugins.deny).toEqual([pluginId, sentinelPluginId]);
|
|
expect(config.plugins.load.paths).toEqual([installPath, sentinelPath]);
|
|
|
|
delete config.plugins.entries[pluginId];
|
|
config.plugins.allow = [sentinelPluginId];
|
|
config.plugins.deny = [sentinelPluginId];
|
|
config.plugins.load.paths = [sentinelPath];
|
|
fs.writeFileSync(configPath, `${JSON.stringify(config)}\n`, "utf8");
|
|
fs.rmSync(installPath, { recursive: true });
|
|
clearMarketplaceIndex(home);
|
|
|
|
const verified = runHelper(home, [
|
|
"assert-marketplace-uninstalled",
|
|
pluginId,
|
|
sentinelPluginId,
|
|
sentinelPath,
|
|
installPathFile,
|
|
]);
|
|
expect(verified.stderr).toBe("");
|
|
expect(verified.status).toBe(0);
|
|
});
|
|
|
|
it("checks the exact dry-run and update outcome text", () => {
|
|
const home = makeTempDir(tempDirs, "openclaw-marketplace-lifecycle-");
|
|
const logPath = path.join(home, "update.log");
|
|
fs.writeFileSync(logPath, "Would update release-marketplace-plugin: 0.0.1 -> 0.0.2.\n", "utf8");
|
|
|
|
const result = runHelper(home, [
|
|
"assert-update-log",
|
|
logPath,
|
|
"Would update release-marketplace-plugin: 0.0.1 -> 0.0.2.",
|
|
]);
|
|
|
|
expect(result.stderr).toBe("");
|
|
expect(result.status).toBe(0);
|
|
});
|
|
});
|