|
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
|
|
PluginDoctorStateMigrationContext,
|
|
|
|
|
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
|
|
|
|
import { getSessionEntry, upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
|
|
|
|
import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing";
|
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
|
import {
|
|
|
|
|
legacyConfigRules,
|
|
|
|
@@ -59,6 +60,16 @@ function openBindingStore(env: NodeJS.ProcessEnv) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function removeCodexDoctorFixture(stateDir: string): Promise<void> {
|
|
|
|
|
// Doctor migrations open per-agent databases and leave the shared state database open under
|
|
|
|
|
// the temporary state dir; both must be released before removal or Windows keeps the files
|
|
|
|
|
// locked and the removal fails with EBUSY. Agent close first: it releases leases through
|
|
|
|
|
// shared state, so the reverse order can reopen it.
|
|
|
|
|
closeOpenClawAgentDatabasesForTest();
|
|
|
|
|
resetPluginStateStoreForTests();
|
|
|
|
|
await fs.rm(stateDir, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createBindingMigrationFixture(options: {
|
|
|
|
|
binding?: Record<string, unknown>;
|
|
|
|
|
legacySharedRoot?: boolean;
|
|
|
|
@@ -272,7 +283,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
}),
|
|
|
|
|
).toMatchObject({ agentHarnessId: "codex" });
|
|
|
|
|
} finally {
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -357,7 +368,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
fs.readFile(fixture.storePath, "utf8").then(JSON.parse),
|
|
|
|
|
).resolves.not.toHaveProperty("agent:main:session-1.agentHarnessId");
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
@@ -396,7 +407,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).rejects.toThrow();
|
|
|
|
|
await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("migrates a shared-root binding to the configured system agent", async () => {
|
|
|
|
@@ -448,7 +459,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
).toMatchObject({ agentHarnessId: "codex" });
|
|
|
|
|
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined();
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps an agent-scoped shared-root binding with its explicit owner", async () => {
|
|
|
|
@@ -492,7 +503,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
).resolves.toMatchObject({ sessionId: "explicit-ops-owner" });
|
|
|
|
|
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined();
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("bounds oversized legacy fingerprints before plugin-state import", async () => {
|
|
|
|
@@ -560,7 +571,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
warnings: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("normalizes a partial raw conversation import before copying the session row", async () => {
|
|
|
|
@@ -638,7 +649,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
warnings: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("normalizes retained raw conversation and session rows before comparison", async () => {
|
|
|
|
@@ -710,7 +721,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
warnings: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects an explicit session file locator outside the session directory", async () => {
|
|
|
|
@@ -743,7 +754,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
).toBeUndefined();
|
|
|
|
|
await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("deduplicates session-store aliases before classifying binding ownership", async () => {
|
|
|
|
@@ -795,7 +806,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
expect(configuredIndex["agent:main:aliased-store"]).not.toHaveProperty("agentHarnessId");
|
|
|
|
|
expect(targetIndex["agent:main:aliased-store"]).not.toHaveProperty("agentHarnessId");
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("resolves relative session files from a symlinked store path", async () => {
|
|
|
|
@@ -847,7 +858,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
`${sessionKey}.agentHarnessId`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
@@ -929,7 +940,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
retired: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
@@ -986,7 +997,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
retired: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not resurrect a retired session generation from its legacy sidecar", async () => {
|
|
|
|
@@ -1041,7 +1052,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
fs.readFile(path.join(fixture.sessionsDir, "sessions.json"), "utf8").then(JSON.parse),
|
|
|
|
|
).resolves.not.toHaveProperty(`${sessionKey}.agentHarnessId`);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each(["active", "cleared"] as const)(
|
|
|
|
@@ -1081,7 +1092,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
warnings: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
@@ -1106,7 +1117,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
await expect(fs.access(fixture.sidecarPath)).rejects.toThrow();
|
|
|
|
|
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined();
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("retains a zero-owner sidecar when canonical plugin state is malformed", async () => {
|
|
|
|
@@ -1135,7 +1146,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
await expect(fs.access(fixture.sidecarPath)).resolves.toBeUndefined();
|
|
|
|
|
await expect(store.lookup(bindingKey)).resolves.toEqual(malformed);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("retains mixed Codex and foreign ambiguous binding owners", async () => {
|
|
|
|
@@ -1164,7 +1175,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
await expect(fs.access(fixture.sidecarPath)).resolves.toBeUndefined();
|
|
|
|
|
await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("retains a sidecar owned by a foreign harness without importing plugin state", async () => {
|
|
|
|
@@ -1188,7 +1199,7 @@ describe("codex doctor contract", () => {
|
|
|
|
|
await expect(fs.access(fixture.sidecarPath)).resolves.toBeUndefined();
|
|
|
|
|
await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]);
|
|
|
|
|
|
|
|
|
|
await fs.rm(fixture.stateDir, { recursive: true, force: true });
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
@@ -1230,10 +1241,8 @@ describe("codex doctor contract", () => {
|
|
|
|
|
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).rejects.toThrow();
|
|
|
|
|
await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]);
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
fs.rm(fixture.stateDir, { recursive: true, force: true }),
|
|
|
|
|
fs.rm(externalDir, { recursive: true, force: true }),
|
|
|
|
|
]);
|
|
|
|
|
await removeCodexDoctorFixture(fixture.stateDir);
|
|
|
|
|
await fs.rm(externalDir, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not scan above stateDir or follow escaped external store locators", async () => {
|
|
|
|
@@ -1284,10 +1293,8 @@ describe("codex doctor contract", () => {
|
|
|
|
|
|
|
|
|
|
await expect(migration.detectLegacyState(params)).resolves.toBeNull();
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
fs.rm(outerDir, { recursive: true, force: true }),
|
|
|
|
|
fs.rm(outsideDir, { recursive: true, force: true }),
|
|
|
|
|
]);
|
|
|
|
|
await removeCodexDoctorFixture(outerDir);
|
|
|
|
|
await fs.rm(outsideDir, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renames old approval-routed destructive plugin policy values", () => {
|
|
|
|
|