import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from "vitest"; import { createDeferred } from "../../../test/helpers/promise.js"; import { loadTranscriptEvents } from "../../config/sessions/session-accessor.js"; import { createGatewaySession } from "../../gateway/session-create-service.js"; import { interruptSessionWorkAdmissions, isSessionLifecycleMutationActive, runExclusiveSessionLifecycleMutation, } from "../../sessions/session-lifecycle-admission.js"; import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; import { createRuntimeAgent } from "./runtime-agent.js"; describe("plugin runtime session creation", () => { it("resolves synchronous session catalog targets through agent model policy", () => { const runtime = createRuntimeAgent(); const config = { agents: { defaults: { models: { "anthropic/claude-opus-4-8": { agentRuntime: { id: "claude-cli" } }, }, }, }, }; expect( runtime.resolveSessionCatalogCreateTarget({ config, provider: "anthropic", modelIds: ["claude-opus-5", "claude-opus-4-8"], agentRuntime: "claude-cli", }), ).toEqual({ model: "anthropic/claude-opus-4-8", agentRuntime: "claude-cli", }); }); it("skips routed catalog targets denied by agent model policy", () => { const runtime = createRuntimeAgent(); const config = { agents: { defaults: { model: { primary: "anthropic/claude-opus-4-8" }, models: { "anthropic/claude-opus-5": { agentRuntime: { id: "claude-cli" } }, "anthropic/claude-opus-4-8": { agentRuntime: { id: "claude-cli" } }, }, modelPolicy: { allow: ["anthropic/claude-opus-4-8"] }, }, }, }; expect( runtime.resolveSessionCatalogCreateTarget({ config, provider: "anthropic", modelIds: ["claude-opus-5", "claude-opus-4-8"], agentRuntime: "claude-cli", }), ).toEqual({ model: "anthropic/claude-opus-4-8", agentRuntime: "claude-cli", }); }); it("requires recovery initialization to return the final trusted patch", () => { type CreateSessionParams = Parameters< ReturnType["session"]["createSessionEntry"] >[0]; const invalidRecoveryInitializer = { cfg: {}, key: "type-contract-only", recoverMatchingInitialEntry: true as const, initialEntry: { agentHarnessId: "codex" }, afterCreate: async () => {}, }; expectTypeOf(invalidRecoveryInitializer).not.toMatchTypeOf(); }); it("creates a canonical transcript with trusted initial session state", async () => { await withOpenClawTestState({ label: "plugin-runtime-session-create" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:harness:codex:supervision:codex-native-thread"; const initialPluginExtensions = { codex: { supervision: { initializing: true, modelLocked: true, }, }, }; const finalPluginExtensions = { codex: { supervision: { nativeThreadId: "thread-native-1", modelLocked: true, }, }, }; let callbackSessionId: string | undefined; const created = await runtime.session.createSessionEntry({ cfg: {}, key, label: "Native Codex thread", displayName: " Native display title ", initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: initialPluginExtensions, }, afterCreate: async (initialized) => { callbackSessionId = initialized.sessionId; expect(initialized.entry.initializationPending).toBe(true); expect(initialized.entry.displayName).toBe("Native display title"); expect(runtime.session.getSessionEntry({ sessionKey: key })?.displayName).toBe( "Native display title", ); initialized.entry.displayName = "Callback clone cannot rename the session"; return { pluginExtensions: finalPluginExtensions }; }, }); initialPluginExtensions.codex.supervision.initializing = false; finalPluginExtensions.codex.supervision.nativeThreadId = "mutated-after-create"; expect(callbackSessionId).toBe(created.sessionId); expect(created.entry.initializationPending).toBeUndefined(); expect(created).toMatchObject({ key, agentId: "main", sessionId: created.entry.sessionId, entry: { agentHarnessId: "codex", delivery: { kind: "none" }, modelSelectionLocked: true, label: "Native Codex thread", displayName: "Native display title", pluginExtensions: { codex: { supervision: { nativeThreadId: "thread-native-1", modelLocked: true, }, }, }, }, }); const stored = runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest", }); expect(stored).toEqual(created.entry); await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "other" }, }), ).rejects.toThrow("Session key namespace is reserved for agent harness-owned sessions."); await expect( runtime.session.createSessionEntry({ cfg: {}, key, displayName: "Do not replace the stored title", initialEntry: { agentHarnessId: "codex" }, }), ).rejects.toThrow("trusted initial session state requires a new session"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toEqual(created.entry); }); }); // Plugin-owned CLI fork creation with colors lives in runtime-agent.session-color.test.ts. it("rolls back the exact created entry and transcript when initialization fails", async () => { await withOpenClawTestState({ label: "plugin-runtime-session-create-rollback" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-binding-failure"; await expect( runtime.session.createSessionEntry({ cfg: {}, key, displayName: "Failed title snapshot", initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, }, afterCreate: async (created) => { expect(created.entry.displayName).toBe("Failed title snapshot"); throw new Error("native binding failed"); }, }), ).rejects.toThrow("native binding failed"); expect(runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" })).toBe( undefined, ); const retried = await runtime.session.createSessionEntry({ cfg: {}, key, displayName: "Retry title snapshot", initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true }, afterCreate: async () => ({ pluginExtensions: {} }), }); expect(retried.entry.displayName).toBe("Retry title snapshot"); expect(retried.entry.initializationPending).toBeUndefined(); }); }); it("rolls back a plugin-owned locked CLI session when initialization fails", async () => { await withOpenClawTestState({ label: "plugin-runtime-cli-session-rollback" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:catalog-adopt:claude:rollback"; const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); let sessionId: string | undefined; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { cliBackendId: "claude-cli", model: "claude-opus-4-8", modelSelectionLocked: true, pluginOwnerId: "anthropic", cliSessionBinding: { sessionId: "claude-source", forceReuse: true, forkNextResume: true, }, }, afterCreate: async (created) => { sessionId = created.sessionId; throw new Error("history import failed"); }, }), ).rejects.toThrow("history import failed"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toBeUndefined(); await expect( loadTranscriptEvents({ agentId: "main", sessionId: sessionId ?? "", sessionKey: key, storePath, }), ).resolves.toEqual([]); }); }); it("rolls back an unlocked harness entry through the ordinary lifecycle path", async () => { await withOpenClawTestState( { label: "plugin-runtime-unlocked-session-create-rollback" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:unlocked-binding-failure"; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex" }, afterCreate: async () => { throw new Error("unlocked native binding failed"); }, }), ).rejects.toThrow("unlocked native binding failed"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toBeUndefined(); }, ); }); it("does not run initialization when the durable initial row cannot be written", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-initial-write-failure" }, async (state) => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-initial-write-failure"; fs.mkdirSync(path.join(state.agentDir(), "openclaw-agent.sqlite"), { recursive: true }); let initializerRan = false; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex", pluginExtensions: { codex: { supervision: { initializing: true } }, }, }, afterCreate: async () => { initializerRan = true; return { pluginExtensions: {} }; }, }), ).rejects.toThrow(); expect(initializerRan).toBe(false); }, ); }); it("rolls back the original entry and transcript when final patch persistence fails", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-final-patch-rollback" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-final-patch-failure"; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { supervision: { initializing: true } }, }, }, afterCreate: async () => { return { pluginExtensions: { codex: { supervision: { invalidJsonValue: 1n as never } }, }, }; }, }), ).rejects.toThrow(); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toBeUndefined(); }, ); }); it("rolls back an unlocked harness entry when final patch persistence fails", async () => { await withOpenClawTestState( { label: "plugin-runtime-unlocked-final-patch-rollback" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:unlocked-final-patch-failure"; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex" }, afterCreate: async () => { return { pluginExtensions: { codex: { supervision: { invalidJsonValue: 1n as never } }, }, }; }, }), ).rejects.toThrow(); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toBeUndefined(); }, ); }); it("fences work admission until trusted initialization completes", async () => { await withOpenClawTestState({ label: "plugin-runtime-session-create-fence" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-binding-fence"; const callbackStarted = createDeferred(); const releaseCallback = createDeferred(); const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); const creation = runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { supervision: { initializing: true } }, }, }, afterCreate: async () => { callbackStarted.resolve(); await releaseCallback.promise; return { pluginExtensions: { codex: { supervision: { modelLocked: true } }, }, }; }, }); await callbackStarted.promise; expect(isSessionLifecycleMutationActive(storePath, [key])).toBe(true); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toMatchObject({ initializationPending: true, pluginExtensions: { codex: { supervision: { initializing: true } }, }, }); let workRan = false; const work = runtime.session.runWithWorkAdmission( { storePath, sessionKey: key }, async () => { workRan = true; }, ); await new Promise((resolve) => { setImmediate(resolve); }); expect(workRan).toBe(false); releaseCallback.resolve(); const created = await creation; await work; expect(workRan).toBe(true); expect(isSessionLifecycleMutationActive(storePath, [key])).toBe(false); expect(created.entry.pluginExtensions).toEqual({ codex: { supervision: { modelLocked: true } }, }); expect(created.entry.initializationPending).toBeUndefined(); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toEqual(created.entry); }); }); it("rejects an ordinary same-key create while trusted initialization is pending", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-ordinary-race" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-initialization-race"; const callbackStarted = createDeferred(); const releaseCallback = createDeferred(); const creation = runtime.session.createSessionEntry({ cfg: {}, key, label: "Trusted initializer", initialEntry: { agentHarnessId: "codex", pluginExtensions: { codex: { supervision: { initializing: true } } }, }, afterCreate: async () => { callbackStarted.resolve(); await releaseCallback.promise; return { pluginExtensions: { codex: { supervision: { modelLocked: true } } }, }; }, }); await callbackStarted.promise; const raced = await createGatewaySession({ cfg: {}, key, label: "Public overwrite", commandSource: "test", }); expect(raced).toMatchObject({ ok: false, error: { message: expect.stringContaining("is still initializing") }, }); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toMatchObject({ initializationPending: true, label: "Trusted initializer", pluginExtensions: { codex: { supervision: { initializing: true } } }, }); releaseCallback.resolve(); const created = await creation; expect(created.entry.initializationPending).toBeUndefined(); expect(created.entry).toMatchObject({ label: "Trusted initializer", pluginExtensions: { codex: { supervision: { modelLocked: true } } }, }); }, ); }); it("rejects creation while pre-existing session work is admitted", async () => { await withOpenClawTestState({ label: "plugin-runtime-session-create-active" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-binding-active"; const workStarted = createDeferred(); const releaseWork = createDeferred(); const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); const work = runtime.session.runWithWorkAdmission( { storePath, sessionKey: key }, async () => { workStarted.resolve(); await releaseWork.promise; }, ); await workStarted.promise; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, }, }), ).rejects.toThrow(`Session "${key}" is still active; retry creation later.`); expect(runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" })).toBe( undefined, ); releaseWork.resolve(); await work; }); }); it.each([ { label: "Operator label", displayName: "Original title snapshot" }, { label: "Older automatically promoted label", displayName: undefined }, ])("recovers an exact initializer without replacing $label or its title", async (naming) => { await withOpenClawTestState( { label: "plugin-runtime-session-create-recovery" }, async (state) => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-recovery"; const sessionId = "interrupted-initializer"; const sessionFile = path.join(state.sessionsDir(), `${sessionId}.jsonl`); const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); const initialPluginExtensions = { codex: { supervision: { sourceThreadId: "source-1", initializing: true } }, }; const persistedPluginExtensions = { codex: { supervision: { initializing: true, sourceThreadId: "source-1" } }, }; fs.mkdirSync(path.dirname(sessionFile), { recursive: true }); fs.writeFileSync( sessionFile, `${JSON.stringify({ type: "session", version: 3, id: sessionId })}\n`, ); await runtime.session.upsertSessionEntry({ storePath, sessionKey: key, entry: { ...naming, sessionId, sessionFile, updatedAt: Date.now(), initializationPending: true, agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: persistedPluginExtensions, spawnedCwd: "/workspace/project", sessionRoot: "/workspace", permissionMode: "guarded", }, }); const recovered = await runtime.session.createSessionEntry({ cfg: {}, key, label: "Replacement label must be ignored", displayName: "Renamed upstream title must be ignored", spawnedCwd: "/workspace/project", sessionRoot: "/workspace", permissionMode: "guarded", recoverMatchingInitialEntry: true, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: initialPluginExtensions, }, afterCreate: async (created) => { expect(created.sessionId).toBe(sessionId); expect(created.entry.initializationPending).toBe(true); return { pluginExtensions: { codex: { supervision: { sourceThreadId: "source-1", modelLocked: true } }, }, }; }, }); expect(recovered.sessionId).toBe(sessionId); expect(recovered.entry.label).toBe(naming.label); expect(recovered.entry.displayName).toBe(naming.displayName); expect(recovered.entry.initializationPending).toBeUndefined(); expect(recovered.entry.pluginExtensions).toEqual({ codex: { supervision: { sourceThreadId: "source-1", modelLocked: true } }, }); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toEqual(recovered.entry); }, ); }); it("does not recover an initializer from a different spawned workspace", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-recovery-cwd-mismatch" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-recovery-cwd-mismatch"; const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); const existing = { sessionId: "foreign-workspace-initializer", updatedAt: Date.now(), delivery: { kind: "none" as const }, initializationPending: true as const, agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { supervision: { sourceThreadId: "source-1", initializing: true } }, }, spawnedCwd: "/workspace/other", }; await runtime.session.upsertSessionEntry({ storePath, sessionKey: key, entry: existing, }); await expect( runtime.session.createSessionEntry({ cfg: {}, key, spawnedCwd: "/workspace/project", recoverMatchingInitialEntry: true, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { supervision: { sourceThreadId: "source-1", initializing: true } }, }, }, afterCreate: async () => ({ pluginExtensions: {} }), }), ).rejects.toThrow("does not match its trusted recovery state"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toEqual(existing); }, ); }); it("does not recover an initializing row with different trusted ownership", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-recovery-mismatch" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-recovery-mismatch"; const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); const existing = { sessionId: "foreign-initializer", updatedAt: Date.now(), delivery: { kind: "none" as const }, initializationPending: true as const, agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { supervision: { sourceThreadId: "different-source", initializing: true } }, }, }; await runtime.session.upsertSessionEntry({ storePath, sessionKey: key, entry: existing, }); await expect( runtime.session.createSessionEntry({ cfg: {}, key, recoverMatchingInitialEntry: true, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, pluginExtensions: { codex: { supervision: { sourceThreadId: "source-1", initializing: true } }, }, }, afterCreate: async () => ({ pluginExtensions: {} }), }), ).rejects.toThrow("does not match its trusted recovery state"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toEqual(existing); }, ); }); it("does not recover or roll back a locked CLI row owned by another plugin", async () => { await withOpenClawTestState({ label: "plugin-runtime-cli-recovery-owner" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:catalog-adopt:claude:foreign"; const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); const cliSessionBinding = { sessionId: "claude-source", forceReuse: true, forkNextResume: true, } as const; const existing = { sessionId: "foreign-initializer", updatedAt: Date.now(), delivery: { kind: "none" as const }, initializationPending: true as const, modelSelectionLocked: true, pluginOwnerId: "other-plugin", providerOverride: "claude-cli", modelOverride: "claude-opus-4-8", cliSessionBindings: { "claude-cli": cliSessionBinding }, }; await runtime.session.upsertSessionEntry({ storePath, sessionKey: key, entry: existing }); await expect( runtime.session.createSessionEntry({ cfg: {}, key, recoverMatchingInitialEntry: true, initialEntry: { cliBackendId: "claude-cli", model: "claude-opus-4-8", modelSelectionLocked: true, pluginOwnerId: "anthropic", cliSessionBinding, }, afterCreate: async () => { throw new Error("must not run"); }, }), ).rejects.toThrow("does not match its trusted recovery state"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toEqual(existing); }); }); it("rejects work for a persisted initializer without an active process fence", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-restart-admission" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-restart-pending"; const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); await runtime.session.upsertSessionEntry({ storePath, sessionKey: key, entry: { sessionId: "interrupted-initializer", updatedAt: Date.now(), initializationPending: true, }, }); expect(isSessionLifecycleMutationActive(storePath, [key])).toBe(false); let workRan = false; await expect( runtime.session.runWithWorkAdmission({ storePath, sessionKey: key }, async () => { workRan = true; }), ).rejects.toThrow("is still initializing"); expect(workRan).toBe(false); }, ); }); it.each(["label", "displayName"] as const)( "preserves a concurrent %s change before finalization", async (field) => { await withOpenClawTestState( { label: "plugin-runtime-session-create-rollback-race" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:codex-binding-race"; let sessionId: string | undefined; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, }, afterCreate: async (created) => { sessionId = created.sessionId; await runtime.session.patchSessionEntry({ sessionKey: created.key, update: () => ({ [field]: "claimed concurrently" }), }); return { pluginExtensions: { codex: { supervision: { modelLocked: true } }, }, }; }, }), ).rejects.toThrow("guarded rollback did not complete"); expect( runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }), ).toMatchObject({ sessionId, [field]: "claimed concurrently", agentHarnessId: "codex", modelSelectionLocked: true, }); }, ); }, ); it("rejects title mutation in the pluginExtensions-only final patch", async () => { await withOpenClawTestState({ label: "plugin-runtime-title-final-patch" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:title-final-patch"; await expect( runtime.session.createSessionEntry({ cfg: {}, key, displayName: "Initial title", initialEntry: { agentHarnessId: "codex" }, afterCreate: async () => ({ pluginExtensions: {}, displayName: "Invalid replacement" }), }), ).rejects.toThrow("session creation final patch may only contain pluginExtensions"); expect(runtime.session.getSessionEntry({ sessionKey: key })).toBeUndefined(); }); }); it("rejects an empty harness initializer without leaving a session entry", async () => { await withOpenClawTestState({ label: "plugin-runtime-session-create-invalid" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:dashboard:invalid-harness"; await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: " " }, }), ).rejects.toThrow("initial agentHarnessId must be non-empty"); expect(runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" })).toBe( undefined, ); }); }); it("does not initialize over an existing placeholder entry", async () => { await withOpenClawTestState( { label: "plugin-runtime-session-create-placeholder" }, async () => { const runtime = createRuntimeAgent(); const key = "agent:main:metadata"; const updatedAt = Date.now(); const storePath = runtime.session.resolveStorePath(undefined, { agentId: "main" }); await runtime.session.upsertSessionEntry({ storePath, sessionKey: key, entry: { sessionId: key, updatedAt, groupActivation: "always" }, }); expect( runtime.session.getSessionEntry({ sessionKey: key, storePath, readConsistency: "latest", }), ).toMatchObject({ sessionId: key, updatedAt, groupActivation: "always" }); await expect( runtime.session.createSessionEntry({ cfg: {}, key, initialEntry: { agentHarnessId: "codex", modelSelectionLocked: true, }, }), ).rejects.toThrow("trusted initial session state requires a new session"); expect( runtime.session.getSessionEntry({ sessionKey: key, storePath, readConsistency: "latest", }), ).toMatchObject({ sessionId: key, updatedAt, groupActivation: "always" }); }, ); }); }); describe("plugin runtime session work admission", () => { let tempDir: string; let storePath: string; const sessionKey = "agent:main:voice:caller"; const sessionId = "voice-session-id"; beforeEach(async () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-session-admission-")); storePath = path.join(tempDir, "sessions.json"); await createRuntimeAgent().session.upsertSessionEntry({ storePath, sessionKey, entry: { sessionId, updatedAt: Date.now() }, }); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); it("rejects an archived session before running admitted work", async () => { const runtime = createRuntimeAgent(); await runtime.session.patchSessionEntry({ storePath, sessionKey, update: () => ({ archivedAt: Date.now() }), }); let ran = false; await expect( runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async () => { ran = true; }), ).rejects.toThrow(`Session "${sessionKey}" is archived`); expect(ran).toBe(false); }); it("waits for a queued archive mutation and rejects the stale start", async () => { const runtime = createRuntimeAgent(); const mutationStarted = createDeferred(); const releaseMutation = createDeferred(); const mutation = runExclusiveSessionLifecycleMutation({ scope: storePath, identities: [sessionKey, sessionId], prepare: async () => { mutationStarted.resolve(); await releaseMutation.promise; }, run: async () => { await runtime.session.patchSessionEntry({ storePath, sessionKey, update: () => ({ archivedAt: Date.now() }), }); }, }); await mutationStarted.promise; const work = runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async () => {}); releaseMutation.resolve(); await mutation; await expect(work).rejects.toThrow(`Session "${sessionKey}" is archived`); }); it("rejects a session replaced while work waits for lifecycle admission", async () => { const runtime = createRuntimeAgent(); const mutationStarted = createDeferred(); const releaseMutation = createDeferred(); const mutation = runExclusiveSessionLifecycleMutation({ scope: storePath, identities: [sessionKey, sessionId], prepare: async () => { mutationStarted.resolve(); await releaseMutation.promise; }, run: async () => { await runtime.session.upsertSessionEntry({ storePath, sessionKey, entry: { sessionId: "replacement-session", updatedAt: Date.now() }, }); }, }); await mutationStarted.promise; const work = runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async () => {}); releaseMutation.resolve(); await mutation; await expect(work).rejects.toMatchObject({ code: "SESSION_WORK_START_CHANGED" }); }); it("admits fresh work and protects session creation inside the callback", async () => { const runtime = createRuntimeAgent(); const freshKey = "agent:main:voice:fresh"; const freshId = "fresh-session-id"; await runtime.session.runWithWorkAdmission({ storePath, sessionKey: freshKey }, async () => { await runtime.session.upsertSessionEntry({ storePath, sessionKey: freshKey, entry: { sessionId: freshId, updatedAt: Date.now() }, }); }); expect(runtime.session.getSessionEntry({ storePath, sessionKey: freshKey })?.sessionId).toBe( freshId, ); }); it("holds admission through the callback and relays lifecycle interruption", async () => { const runtime = createRuntimeAgent(); const workStarted = createDeferred(); let admittedSignal: AbortSignal | undefined; const work = runtime.session.runWithWorkAdmission({ storePath, sessionKey }, async (signal) => { admittedSignal = signal; workStarted.resolve(); await new Promise((resolve) => { signal.addEventListener("abort", () => resolve(), { once: true }); }); }); await workStarted.promise; await interruptSessionWorkAdmissions({ scope: storePath, identities: [sessionKey, sessionId], }); await work; expect(admittedSignal?.aborted).toBe(true); }); });