Download src/acp/runtime/session-meta.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 34.5 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/acp/runtime/session-meta.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/acp/runtime/session-meta.test.ts
-
curl -L -o session-meta.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/acp/runtime/session-meta.test.ts
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"); | |
| }); | |
| }); | |
| }); | |