mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
test(doctor): cover copied-state migration sequence (#126508)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
// Regression for the copied shared-state upgrade reported from 2026.6.1-beta.1.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { OPENCLAW_STATE_SCHEMA_SQL } from "../src/state/openclaw-state-schema.js";
|
||||
import { createOpenClawTestInstance } from "./helpers/openclaw-test-instance.js";
|
||||
|
||||
const HISTORICAL_DEVICE_BOOTSTRAP_TOKENS_SQL = `
|
||||
CREATE TABLE device_bootstrap_tokens (
|
||||
token_key TEXT NOT NULL PRIMARY KEY,
|
||||
token TEXT NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
device_id TEXT,
|
||||
public_key TEXT,
|
||||
profile_json TEXT,
|
||||
redeemed_profile_json TEXT,
|
||||
pending_profile_json TEXT,
|
||||
issued_at_ms INTEGER NOT NULL,
|
||||
last_used_at_ms INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX idx_device_bootstrap_tokens_ts
|
||||
ON device_bootstrap_tokens(ts);
|
||||
`;
|
||||
|
||||
const HISTORICAL_OPERATOR_APPROVALS_SQL = `
|
||||
CREATE TABLE operator_approvals (
|
||||
approval_id TEXT NOT NULL PRIMARY KEY CHECK (
|
||||
length(approval_id) > 0 AND approval_id NOT IN ('.', '..')
|
||||
),
|
||||
resolution_ref TEXT NOT NULL CHECK (
|
||||
length(resolution_ref) = 43 AND resolution_ref NOT GLOB '*[^A-Za-z0-9_-]*'
|
||||
),
|
||||
kind TEXT NOT NULL CHECK (kind IN ('exec', 'plugin')),
|
||||
status TEXT NOT NULL CHECK (status IN ('pending', 'allowed', 'denied', 'expired', 'cancelled')),
|
||||
presentation_json TEXT NOT NULL,
|
||||
requested_by_device_id TEXT,
|
||||
requested_by_client_id TEXT,
|
||||
requested_by_device_token_auth INTEGER NOT NULL DEFAULT 0,
|
||||
reviewer_device_ids_json TEXT NOT NULL,
|
||||
source_agent_id TEXT,
|
||||
source_session_key TEXT,
|
||||
source_session_id TEXT,
|
||||
source_run_id TEXT,
|
||||
source_tool_call_id TEXT,
|
||||
source_tool_name TEXT,
|
||||
audience_session_keys_json TEXT NOT NULL,
|
||||
runtime_epoch TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
expires_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
decision TEXT CHECK (decision IN ('allow-once', 'allow-always', 'deny')),
|
||||
terminal_reason TEXT CHECK (
|
||||
terminal_reason IN (
|
||||
'user',
|
||||
'timeout',
|
||||
'malformed-verdict',
|
||||
'no-route',
|
||||
'run-aborted',
|
||||
'gateway-restart',
|
||||
'storage-corrupt'
|
||||
)
|
||||
),
|
||||
resolved_at_ms INTEGER,
|
||||
resolver_kind TEXT CHECK (resolver_kind IN ('device', 'channel', 'runtime', 'system')),
|
||||
resolver_id TEXT,
|
||||
consumed_at_ms INTEGER,
|
||||
consumed_by TEXT,
|
||||
CHECK (expires_at_ms >= created_at_ms),
|
||||
CHECK (updated_at_ms >= created_at_ms),
|
||||
CHECK (resolved_at_ms IS NULL OR resolved_at_ms >= created_at_ms),
|
||||
CHECK (resolved_at_ms IS NULL OR resolved_at_ms <= updated_at_ms),
|
||||
CHECK (consumed_at_ms IS NULL OR consumed_at_ms >= resolved_at_ms),
|
||||
CHECK (consumed_at_ms IS NULL OR consumed_at_ms <= updated_at_ms),
|
||||
CHECK (requested_by_device_token_auth IN (0, 1)),
|
||||
CHECK (
|
||||
(
|
||||
status = 'pending'
|
||||
AND decision IS NULL
|
||||
AND terminal_reason IS NULL
|
||||
AND resolved_at_ms IS NULL
|
||||
AND resolver_kind IS NULL
|
||||
AND resolver_id IS NULL
|
||||
AND consumed_at_ms IS NULL
|
||||
AND consumed_by IS NULL
|
||||
)
|
||||
OR (
|
||||
status = 'allowed'
|
||||
AND decision IN ('allow-once', 'allow-always')
|
||||
AND terminal_reason = 'user'
|
||||
AND resolved_at_ms IS NOT NULL
|
||||
AND resolver_kind IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
status = 'denied'
|
||||
AND decision = 'deny'
|
||||
AND terminal_reason IN ('user', 'malformed-verdict', 'no-route', 'storage-corrupt')
|
||||
AND resolved_at_ms IS NOT NULL
|
||||
AND resolver_kind IS NOT NULL
|
||||
AND consumed_at_ms IS NULL
|
||||
AND consumed_by IS NULL
|
||||
)
|
||||
OR (
|
||||
status = 'expired'
|
||||
AND decision = 'deny'
|
||||
AND terminal_reason = 'timeout'
|
||||
AND resolved_at_ms IS NOT NULL
|
||||
AND resolver_kind IS NOT NULL
|
||||
AND consumed_at_ms IS NULL
|
||||
AND consumed_by IS NULL
|
||||
)
|
||||
OR (
|
||||
status = 'cancelled'
|
||||
AND decision = 'deny'
|
||||
AND terminal_reason IN ('run-aborted', 'gateway-restart')
|
||||
AND resolved_at_ms IS NOT NULL
|
||||
AND resolver_kind IS NOT NULL
|
||||
AND consumed_at_ms IS NULL
|
||||
AND consumed_by IS NULL
|
||||
)
|
||||
),
|
||||
CHECK (
|
||||
(consumed_at_ms IS NULL AND consumed_by IS NULL)
|
||||
OR (
|
||||
status = 'allowed'
|
||||
AND decision = 'allow-once'
|
||||
AND consumed_at_ms IS NOT NULL
|
||||
AND consumed_by IS NOT NULL
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_operator_approvals_status_expiry
|
||||
ON operator_approvals(status, expires_at_ms, approval_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_operator_approvals_resolution_ref
|
||||
ON operator_approvals(resolution_ref);
|
||||
|
||||
CREATE INDEX idx_operator_approvals_source_session_created
|
||||
ON operator_approvals(source_session_key, created_at_ms DESC, approval_id);
|
||||
|
||||
CREATE INDEX idx_operator_approvals_resolved
|
||||
ON operator_approvals(resolved_at_ms, approval_id)
|
||||
WHERE resolved_at_ms IS NOT NULL;
|
||||
|
||||
CREATE INDEX idx_operator_approvals_runtime_pending
|
||||
ON operator_approvals(runtime_epoch, approval_id)
|
||||
WHERE status = 'pending';
|
||||
`;
|
||||
|
||||
function writeHistoricalCopiedStateFixture(stateDir: string): void {
|
||||
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
|
||||
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||
const database = new DatabaseSync(databasePath);
|
||||
try {
|
||||
database.exec(OPENCLAW_STATE_SCHEMA_SQL);
|
||||
database.exec(`
|
||||
DROP TABLE device_bootstrap_tokens;
|
||||
${HISTORICAL_DEVICE_BOOTSTRAP_TOKENS_SQL}
|
||||
DROP TABLE operator_approvals;
|
||||
${HISTORICAL_OPERATOR_APPROVALS_SQL}
|
||||
PRAGMA user_version = 2;
|
||||
INSERT INTO schema_meta (
|
||||
meta_key, role, schema_version, agent_id, app_version, created_at, updated_at
|
||||
) VALUES ('primary', 'global', 2, NULL, NULL, 0, 0);
|
||||
INSERT INTO device_bootstrap_tokens (token_key, token, ts, issued_at_ms)
|
||||
VALUES ('fixture-bootstrap', 'fixture-token', 1000, 1000);
|
||||
`);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
describe("doctor copied-state migration", () => {
|
||||
it(
|
||||
"repairs the retained 2026.6.1-beta.1 shared state before gateway readiness",
|
||||
{ timeout: 180_000 },
|
||||
async () => {
|
||||
const instance = await createOpenClawTestInstance({ name: "doctor-copied-state" });
|
||||
try {
|
||||
writeHistoricalCopiedStateFixture(instance.stateDir);
|
||||
|
||||
const doctor = await instance.cli(
|
||||
["doctor", "--fix", "--non-interactive", "--yes", "--no-workspace-suggestions"],
|
||||
{ timeoutMs: 120_000 },
|
||||
);
|
||||
|
||||
expect(doctor.code, `${doctor.stdout}\n${doctor.stderr}`).toBe(0);
|
||||
expect(`${doctor.stdout}\n${doctor.stderr}`).not.toContain(
|
||||
"Failed migrating shared state database schema",
|
||||
);
|
||||
await instance.startGateway();
|
||||
} finally {
|
||||
await instance.cleanup();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user