openclaw / src /acp /runtime /session-meta.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw History Blame Contribute Delete
34.5 kB
/** Tests ACP session metadata persistence, joins, and migration helpers. */
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { retainLegacyDefaultAgentId } from "../../config/legacy.default-agent-owner.js";
import { loadSessionEntry, replaceSessionEntry } from "../../config/sessions/session-accessor.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import { claimOpenClawStateOwnership } from "../../state/openclaw-state-ownership-operations.js";
import { withTestDir } from "../../test-helpers/temp-dir.js";
import {
listAcpSessionEntries,
readAcpSessionEntry,
readAcpSessionMeta,
readAcpSessionMetaBatch,
readAcpSessionMetaForEntry,
repairAcpSessionMetaKeyForMigration,
upsertAcpSessionMeta,
writeAcpSessionMetaForMigration,
} from "./session-meta.js";
const ACP_AGENT_ID = "codex";
async function seedAcpSessionEntry(params: {
storePath: string;
sessionKey: string;
entry: SessionEntry;
}): Promise<void> {
await replaceSessionEntry(
{
agentId: ACP_AGENT_ID,
storePath: params.storePath,
sessionKey: params.sessionKey,
},
params.entry,
);
}
function readStoredAcpSessionEntry(params: {
storePath: string;
sessionKey: string;
}): SessionEntry | undefined {
return loadSessionEntry({
agentId: ACP_AGENT_ID,
storePath: params.storePath,
sessionKey: params.sessionKey,
});
}
describe("ACP session metadata SQLite store", () => {
afterEach(() => {
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
});
it("persists bare global metadata under a configured fixed-store owner", async () => {
await withTestDir({ prefix: "openclaw-acp-global-owner-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const cfg = {
session: { scope: "global", store: storePath },
agents: {
ownership: "explicit",
defaults: { sessionStore: { agentId: "ops" } },
entries: { ops: {}, research: {} },
},
} satisfies OpenClawConfig;
const databasePath = path.join(dir, "state", "openclaw.sqlite");
await replaceSessionEntry(
{
agentId: "ops",
storePath,
sessionKey: "global",
},
{ sessionId: "ops-global", updatedAt: 100, sessionStartedAt: 100 },
);
const mutate = () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "global",
mode: "persistent" as const,
state: "idle" as const,
lastActivityAt: 123,
});
const persisted = await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: "global",
mutate,
});
expect(persisted?.acp?.runtimeSessionName).toBe("global");
expect(
readAcpSessionMeta({
cfg,
databasePath,
sessionKey: "global",
})?.runtimeSessionName,
).toBe("global");
const conflictingMutate = vi.fn(mutate);
await expect(
upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: "global",
agentId: "research",
mutate: conflictingMutate,
}),
).rejects.toMatchObject({ code: "AGENT_SELECTION_REQUIRED" });
expect(conflictingMutate).not.toHaveBeenCalled();
const ownerlessCfg = {
...cfg,
agents: { ownership: "explicit", entries: { ops: {}, research: {} } },
} satisfies OpenClawConfig;
const ownerlessMutate = vi.fn(mutate);
await expect(
upsertAcpSessionMeta({
cfg: ownerlessCfg,
databasePath,
sessionKey: "ownerless-global",
mutate: ownerlessMutate,
}),
).rejects.toMatchObject({ code: "AGENT_SELECTION_REQUIRED" });
expect(ownerlessMutate).not.toHaveBeenCalled();
});
});
it("reads metadata under external state ownership without write admission", async () => {
await withTestDir({ prefix: "openclaw-acp-read-only-owner-" }, async (dir) => {
const env = { OPENCLAW_STATE_DIR: dir };
const externalEnv = { ...env, OPENCLAW_SUPERVISOR_MODE: "external" };
const cfg = {
agents: { ownership: "explicit", entries: { main: {} } },
} satisfies OpenClawConfig;
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const storePath = path.join(dir, "agents", "main", "agent", "openclaw-agent.sqlite");
const sessionKey = "agent:main:proof";
await replaceSessionEntry(
{ agentId: "main", storePath, sessionKey },
{ sessionId: "proof-session", updatedAt: 100 },
);
await upsertAcpSessionMeta({
cfg,
databasePath,
env,
sessionKey,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "proof-runtime",
mode: "persistent",
state: "idle",
lastActivityAt: 100,
}),
});
closeOpenClawStateDatabaseForTest();
claimOpenClawStateOwnership("test-supervisor", { env: externalEnv });
closeOpenClawStateDatabaseForTest();
const before = fs.readFileSync(databasePath);
expect(readAcpSessionMeta({ cfg, databasePath, env, sessionKey })).toMatchObject({
runtimeSessionName: "proof-runtime",
});
expect(fs.readFileSync(databasePath)).toEqual(before);
});
});
it("keeps identical bare keys isolated by explicit agent owner", async () => {
await withTestDir({ prefix: "openclaw-acp-pair-owner-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = {
session: { store: storePath },
agents: { ownership: "explicit", entries: { research: {}, ops: {} } },
} satisfies OpenClawConfig;
for (const agentId of ["research", "ops"]) {
await replaceSessionEntry(
{ agentId, storePath, sessionKey: "global" },
{ sessionId: `${agentId}-global`, updatedAt: 100 },
);
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: "global",
agentId,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: agentId,
mode: "persistent",
state: "idle",
lastActivityAt: 123,
}),
});
}
expect(
readAcpSessionMeta({ cfg, databasePath, sessionKey: "global", agentId: "research" })
?.runtimeSessionName,
).toBe("research");
expect(
readAcpSessionMeta({ cfg, databasePath, sessionKey: "global", agentId: "ops" })
?.runtimeSessionName,
).toBe("ops");
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: "global",
agentId: "research",
mutate: () => null,
});
expect(
readAcpSessionMeta({ cfg, databasePath, sessionKey: "global", agentId: "ops" })
?.runtimeSessionName,
).toBe("ops");
});
});
it.each(["persisted", "sole", "retained"] as const)(
"batch-loads legacy bare metadata without rekeying during a read (%s owner)",
async (ownerKind) => {
await withTestDir({ prefix: "openclaw-acp-batch-owner-" }, async (dir) => {
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg: OpenClawConfig = {
session: { store: path.join(dir, "sessions.json") },
agents: {
ownership: "explicit",
defaults:
ownerKind === "persisted"
? { sessionStore: { agentId: "ops" } }
: ownerKind === "retained"
? { systemAgent: { agentId: "research" } }
: undefined,
entries: ownerKind === "sole" ? { ops: {} } : { ops: {}, research: {} },
},
};
retainLegacyDefaultAgentId(cfg, ownerKind === "retained" ? "ops" : undefined);
const entry: SessionEntry = {
sessionId: "ops-global",
lifecycleRevision: "ops-revision",
updatedAt: 100,
};
const staleEntry: SessionEntry = { ...entry, lifecycleRevision: "ops-next" };
const otherOwnerEntry: SessionEntry = { ...entry };
writeAcpSessionMetaForMigration({
databasePath,
sessionKey: "global",
lifecycleRevision: "ops-revision",
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "legacy-global",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
},
});
const batch = readAcpSessionMetaBatch({
cfg,
databasePath,
entries: [
{ sessionKey: "global", agentId: "ops", entry },
{ sessionKey: "global", agentId: "ops", entry: staleEntry },
{ sessionKey: "global", agentId: "research", entry: otherOwnerEntry },
],
});
expect([
batch.get(entry)?.runtimeSessionName,
batch.get(otherOwnerEntry)?.runtimeSessionName,
]).toEqual(["legacy-global", undefined]);
expect(batch.get(staleEntry)).toBeUndefined();
expect(
["ops", "research"].map(
(agentId) =>
readAcpSessionMetaForEntry({
cfg,
databasePath,
sessionKey: "global",
agentId,
entry,
})?.runtimeSessionName,
),
).toEqual(["legacy-global", undefined]);
expect(
readAcpSessionMetaForEntry({
cfg,
databasePath,
sessionKey: "global",
agentId: "ops",
entry: staleEntry,
}),
).toBeUndefined();
expect(
readAcpSessionMetaForEntry({ databasePath, sessionKey: "global", entry })
?.runtimeSessionName,
).toBe("legacy-global");
expect(
readAcpSessionMetaForEntry({ databasePath, sessionKey: "global", entry: staleEntry }),
).toBeUndefined();
const database = new DatabaseSync(databasePath, {
readOnly: true,
});
try {
expect(
database.prepare("SELECT session_key FROM acp_sessions ORDER BY session_key").all(),
).toEqual([{ session_key: "global" }]);
} finally {
database.close();
}
});
},
);
it("deletes the legacy row selected by fallback when metadata is cleared", async () => {
await withTestDir({ prefix: "openclaw-acp-clear-legacy-owner-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = {
session: { scope: "global", store: storePath },
agents: {
ownership: "explicit",
defaults: { sessionStore: { agentId: "ops" } },
entries: { ops: {}, research: {} },
},
} satisfies OpenClawConfig;
const entry: SessionEntry = {
sessionId: "ops-global",
lifecycleRevision: "ops-revision",
updatedAt: 100,
};
await replaceSessionEntry({ agentId: "ops", storePath, sessionKey: "global" }, entry);
writeAcpSessionMetaForMigration({
databasePath,
sessionKey: "global",
lifecycleRevision: "ops-revision",
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "legacy-global",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: "global",
mutate: (current) => {
expect(current?.runtimeSessionName).toBe("legacy-global");
return null;
},
});
expect(readAcpSessionMeta({ cfg, databasePath, sessionKey: "global" })).toBeUndefined();
});
});
it("escapes composite identities from legacy raw keys that use the old prefix", async () => {
await withTestDir({ prefix: "openclaw-acp-prefix-collision-" }, async (dir) => {
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const rawSessionKey = "@agent:research:foo";
const rawEntry: SessionEntry = {
sessionId: "raw-session",
lifecycleRevision: "raw-revision",
updatedAt: 100,
};
writeAcpSessionMetaForMigration({
databasePath,
sessionKey: rawSessionKey,
lifecycleRevision: "raw-revision",
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "literal-prefix-key",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
},
});
const batch = readAcpSessionMetaBatch({
databasePath,
entries: [{ sessionKey: rawSessionKey, entry: rawEntry }],
});
expect(batch.get(rawEntry)?.runtimeSessionName).toBe("literal-prefix-key");
expect(
readAcpSessionMetaForEntry({ databasePath, sessionKey: rawSessionKey, entry: rawEntry })
?.runtimeSessionName,
).toBe("literal-prefix-key");
expect(
readAcpSessionMetaForEntry({
databasePath,
sessionKey: "foo",
agentId: "research",
entry: {
sessionId: "other-session",
lifecycleRevision: "other-revision",
},
}),
).toBeUndefined();
});
});
it("persists ACP metadata in SQLite without writing sessions.json acp blocks", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const sessionKey = "agent:codex:acp:binding:discord:default:feedface";
await seedAcpSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-acp",
updatedAt: 100,
},
});
const result = await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey,
now: () => 200,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-discord",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
cwd: "/repo",
}),
});
expect(result?.acp?.runtimeSessionName).toBe("codex-discord");
expect(fs.existsSync(storePath)).toBe(false);
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey,
})?.acp,
).toMatchObject({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-discord",
mode: "persistent",
state: "idle",
cwd: "/repo",
});
});
});
it("clears legacy embedded ACP metadata through the session accessor", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const sessionKey = "agent:codex:acp:binding:discord:default:feedface";
await seedAcpSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-acp",
updatedAt: 100,
acp: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "legacy-embedded",
mode: "persistent",
state: "idle",
lastActivityAt: 120,
},
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey,
now: () => 200,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-sqlite",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
}),
});
expect(readStoredAcpSessionEntry({ storePath, sessionKey })?.acp).toBeUndefined();
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey,
})?.acp?.runtimeSessionName,
).toBe("codex-sqlite");
});
});
it("creates a session-store row for new SQLite ACP sessions without embedding ACP metadata", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const sessionKey = "agent:codex:acp:new-session";
const result = await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey,
now: () => 200,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-new",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
}),
});
expect(result?.sessionId).toEqual(expect.any(String));
expect(result?.acp?.runtimeSessionName).toBe("codex-new");
const storedEntry = readStoredAcpSessionEntry({ storePath, sessionKey });
expect(storedEntry?.sessionId).toEqual(expect.any(String));
expect(storedEntry?.updatedAt).toEqual(expect.any(Number));
expect(storedEntry?.sessionStartedAt).toBeGreaterThan(200);
expect(storedEntry?.acp).toBeUndefined();
expect(readAcpSessionEntry({ cfg, databasePath, sessionKey })?.acp?.runtimeSessionName).toBe(
"codex-new",
);
expect(readAcpSessionMeta({ cfg, databasePath, sessionKey })?.runtimeSessionName).toBe(
"codex-new",
);
expect(await listAcpSessionEntries({ cfg, databasePath })).toHaveLength(1);
});
});
it("normalizes ACP metadata lookups and writes to the resolved session-store key", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const storeSessionKey = "agent:codex:acp:binding:discord:default:feedface";
const rawSessionKey = storeSessionKey.toUpperCase();
await seedAcpSessionEntry({
storePath,
sessionKey: storeSessionKey,
entry: {
sessionId: "sess-acp",
updatedAt: 100,
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: rawSessionKey,
now: () => 200,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-normalized",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
}),
});
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey: rawSessionKey,
})?.acp?.runtimeSessionName,
).toBe("codex-normalized");
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey: storeSessionKey,
})?.acp?.runtimeSessionName,
).toBe("codex-normalized");
expect(
readAcpSessionMeta({ cfg, databasePath, sessionKey: ` ${rawSessionKey} ` })
?.runtimeSessionName,
).toBe("codex-normalized");
expect(fs.existsSync(storePath)).toBe(false);
const legacyEmbeddedEntry = readStoredAcpSessionEntry({
storePath,
sessionKey: storeSessionKey,
});
expect(legacyEmbeddedEntry).toBeDefined();
if (!legacyEmbeddedEntry) {
throw new Error("expected normalized ACP session entry");
}
await seedAcpSessionEntry({
storePath,
sessionKey: storeSessionKey,
entry: {
...legacyEmbeddedEntry,
acp: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "legacy-embedded",
mode: "persistent",
state: "idle",
lastActivityAt: 120,
},
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: rawSessionKey,
mutate: (current) => {
expect(current?.runtimeSessionName).toBe("codex-normalized");
return null;
},
});
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey: storeSessionKey,
})?.acp,
).toBeUndefined();
expect(
readStoredAcpSessionEntry({ storePath, sessionKey: storeSessionKey })?.acp,
).toBeUndefined();
});
});
it("keeps SQLite ACP metadata visible when legacy store keys are canonicalized", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const legacyStoreSessionKey = "agent:CODEX:acp:legacy-runtime";
const canonicalSessionKey = "agent:codex:acp:legacy-runtime";
await seedAcpSessionEntry({
storePath,
sessionKey: legacyStoreSessionKey,
entry: {
sessionId: "sess-acp",
updatedAt: 100,
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: canonicalSessionKey,
now: () => 200,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-canonicalized",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
}),
});
expect(
readStoredAcpSessionEntry({ storePath, sessionKey: canonicalSessionKey })?.sessionId,
).toBe("sess-acp");
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey: canonicalSessionKey,
})?.acp?.runtimeSessionName,
).toBe("codex-canonicalized");
expect(await listAcpSessionEntries({ cfg, databasePath })).toHaveLength(1);
});
});
it("binds ACP metadata to the final accessor-selected entry for alias writes", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const canonicalSessionKey = "agent:codex:acp:alias-runtime";
const legacyStoreSessionKey = "agent:CODEX:acp:alias-runtime";
await seedAcpSessionEntry({
storePath,
sessionKey: canonicalSessionKey,
entry: {
sessionId: "sess-canonical",
updatedAt: 100,
},
});
await seedAcpSessionEntry({
storePath,
sessionKey: legacyStoreSessionKey,
entry: {
sessionId: "sess-legacy",
updatedAt: 150,
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey: legacyStoreSessionKey,
now: () => 200,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-alias",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
}),
});
expect(
readStoredAcpSessionEntry({ storePath, sessionKey: canonicalSessionKey })?.sessionId,
).toBe("sess-legacy");
expect(
readAcpSessionEntry({
cfg,
databasePath,
sessionKey: canonicalSessionKey,
})?.acp?.runtimeSessionName,
).toBe("codex-alias");
expect(await listAcpSessionEntries({ cfg, databasePath })).toHaveLength(1);
});
});
it("ignores SQLite ACP metadata rows from an older lifecycle revision", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const sessionKey = "agent:codex:acp:binding:discord:default:feedface";
await seedAcpSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-new",
lifecycleRevision: "revision-new",
updatedAt: 100,
},
});
writeAcpSessionMetaForMigration({
databasePath,
sessionKey,
lifecycleRevision: "revision-old",
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-stale",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
},
});
expect(readAcpSessionEntry({ cfg, databasePath, sessionKey })?.acp).toBeUndefined();
expect(readAcpSessionMeta({ cfg, databasePath, sessionKey })).toBeUndefined();
expect(await listAcpSessionEntries({ cfg, databasePath })).toHaveLength(0);
writeAcpSessionMetaForMigration({
databasePath,
sessionKey,
lifecycleRevision: "revision-new",
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-current",
mode: "persistent",
state: "idle",
lastActivityAt: 124,
},
});
expect(readAcpSessionEntry({ cfg, databasePath, sessionKey })?.acp?.runtimeSessionName).toBe(
"codex-current",
);
expect(await listAcpSessionEntries({ cfg, databasePath })).toHaveLength(1);
});
});
it("reads ACP metadata rows written with the legacy session-id binding", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const sessionKey = "agent:codex:acp:binding:discord:default:legacy";
await seedAcpSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-existing",
lifecycleRevision: "revision-existing",
sessionStartedAt: 50,
updatedAt: 100,
},
});
// Simulate the pre-boundary layout, where session_id stored the logical id.
writeAcpSessionMetaForMigration({
databasePath,
sessionKey,
lifecycleRevision: "sess-existing",
now: () => 100,
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-legacy",
mode: "persistent",
state: "idle",
lastActivityAt: 123,
},
});
expect(readAcpSessionEntry({ cfg, databasePath, sessionKey })?.acp?.runtimeSessionName).toBe(
"codex-legacy",
);
expect(await listAcpSessionEntries({ cfg, databasePath })).toHaveLength(1);
expect(
readAcpSessionMetaForEntry({
databasePath,
sessionKey,
entry: {
sessionId: "sess-existing",
lifecycleRevision: "revision-next",
sessionStartedAt: 150,
},
}),
).toBeUndefined();
const staleKey = `${sessionKey}:stale`;
await seedAcpSessionEntry({
storePath,
sessionKey: staleKey,
entry: {
sessionId: "sess-stale",
lifecycleRevision: "revision-after-reset",
sessionStartedAt: 150,
updatedAt: 150,
},
});
writeAcpSessionMetaForMigration({
databasePath,
sessionKey: staleKey,
lifecycleRevision: "sess-stale",
now: () => 100,
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-stale-legacy",
mode: "persistent",
state: "idle",
lastActivityAt: 100,
},
});
expect(readAcpSessionEntry({ cfg, databasePath, sessionKey: staleKey })?.acp).toBeUndefined();
});
});
it("keeps a session-id fence when ACP metadata is written before a lifecycle revision", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const sessionKey = "agent:codex:acp:pre-revision";
writeAcpSessionMetaForMigration({
databasePath,
sessionKey,
sessionId: "sess-pre-revision",
now: () => 100,
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-pre-revision",
mode: "persistent",
state: "idle",
lastActivityAt: 100,
},
});
expect(
readAcpSessionMetaForEntry({
databasePath,
sessionKey,
entry: {
sessionId: "sess-pre-revision",
lifecycleRevision: undefined,
sessionStartedAt: 50,
},
})?.runtimeSessionName,
).toBe("codex-pre-revision");
expect(
readAcpSessionMetaForEntry({
databasePath,
sessionKey,
entry: {
sessionId: "sess-pre-revision",
lifecycleRevision: "revision-after-reset",
sessionStartedAt: 150,
},
}),
).toBeUndefined();
});
});
it("repairs ACP metadata rows when session-store keys are canonicalized", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const legacyKey = "agent:CODEX:acp:legacy-runtime";
const canonicalKey = "agent:codex:acp:legacy-runtime";
await seedAcpSessionEntry({
storePath,
sessionKey: canonicalKey,
entry: {
sessionId: "sess-acp",
lifecycleRevision: "revision-acp",
updatedAt: 100,
},
});
writeAcpSessionMetaForMigration({
databasePath,
sessionKey: legacyKey,
lifecycleRevision: "revision-acp",
meta: {
backend: "acpx",
agent: "codex",
runtimeSessionName: legacyKey,
mode: "persistent",
state: "idle",
lastActivityAt: 123,
},
});
expect(
repairAcpSessionMetaKeyForMigration({
databasePath,
sessionKey: canonicalKey,
entry: { lifecycleRevision: "revision-acp" },
now: () => 200,
}),
).toBe(true);
expect(
readAcpSessionMetaForEntry({
databasePath,
sessionKey: legacyKey,
entry: { lifecycleRevision: "revision-acp" },
}),
).toBeUndefined();
expect(
readAcpSessionEntry({ cfg, databasePath, sessionKey: canonicalKey })?.acp
?.runtimeSessionName,
).toBe(legacyKey);
});
});
it("lists SQLite ACP rows while joining current session-store entries", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const storePath = path.join(dir, "sessions.json");
const databasePath = path.join(dir, "state", "openclaw.sqlite");
const cfg = { session: { store: storePath } } as OpenClawConfig;
const sessionKey = "agent:codex:acp:s1";
await seedAcpSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-acp",
updatedAt: 100,
model: "gpt-5.5",
},
});
await upsertAcpSessionMeta({
cfg,
databasePath,
sessionKey,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-s1",
mode: "oneshot",
state: "running",
lastActivityAt: 321,
}),
});
const entries = await listAcpSessionEntries({ cfg, databasePath, clone: false });
expect(entries).toHaveLength(1);
expect(entries[0]).toMatchObject({
cfg,
storePath,
sessionKey,
storeSessionKey: sessionKey,
entry: {
sessionId: "sess-acp",
model: "gpt-5.5",
},
acp: {
backend: "acpx",
runtimeSessionName: "codex-s1",
mode: "oneshot",
state: "running",
},
});
});
});
it("honors OPENCLAW_STATE_DIR when joining listed SQLite rows to session stores", async () => {
await withTestDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
const env = { ...process.env, OPENCLAW_STATE_DIR: dir } as NodeJS.ProcessEnv;
const cfg = {} as OpenClawConfig;
const sessionKey = "agent:codex:acp:s1";
const storePath = path.join(dir, "agents", "codex", "sessions", "sessions.json");
await seedAcpSessionEntry({
storePath,
sessionKey,
entry: {
sessionId: "sess-acp",
updatedAt: 100,
},
});
await upsertAcpSessionMeta({
cfg,
env,
sessionKey,
mutate: () => ({
backend: "acpx",
agent: "codex",
runtimeSessionName: "codex-s1",
mode: "persistent",
state: "idle",
lastActivityAt: 321,
}),
});
const entries = await listAcpSessionEntries({ cfg, env });
expect(entries).toHaveLength(1);
expect(entries[0]?.storePath).toBe(storePath);
expect(entries[0]?.entry?.sessionId).toBe("sess-acp");
});
});
});