Files
openclaw/src/plugins/runtime/runtime-agent.test.ts
T
Peter Steinberger 8616c0c374 refactor: finish shared test helper migrations (#120996)
* test: finish shared helper migrations

* test: fix helper migration CI

* style: fix test import ordering

* test(acpx): restore deferred void types

* test: fix helper migrations after rebase
2026-08-09 06:00:06 -07:00

1002 lines
34 KiB
TypeScript

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<typeof createRuntimeAgent>["session"]["createSessionEntry"]
>[0];
const invalidRecoveryInitializer = {
cfg: {},
key: "type-contract-only",
recoverMatchingInitialEntry: true as const,
initialEntry: { agentHarnessId: "codex" },
afterCreate: async () => {},
};
expectTypeOf(invalidRecoveryInitializer).not.toMatchTypeOf<CreateSessionParams>();
});
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",
initialEntry: {
agentHarnessId: "codex",
modelSelectionLocked: true,
pluginExtensions: initialPluginExtensions,
},
afterCreate: async (initialized) => {
callbackSessionId = initialized.sessionId;
expect(initialized.entry.initializationPending).toBe(true);
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",
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,
initialEntry: { agentHarnessId: "codex" },
}),
).rejects.toThrow("trusted initial session state requires a new session");
expect(
runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" }),
).toEqual(created.entry);
});
});
it("creates a plugin-owned locked CLI session with a seeded fork binding", async () => {
await withOpenClawTestState({ label: "plugin-runtime-cli-session-create" }, async () => {
const runtime = createRuntimeAgent();
const key = "agent:main:catalog-adopt:claude:source";
const created = await runtime.session.createSessionEntry({
cfg: {},
key,
execNode: "node-a",
execCwd: "/work/on-node",
initialEntry: {
cliBackendId: "claude-cli",
model: "claude-opus-4-8",
modelSelectionLocked: true,
pluginOwnerId: "anthropic",
cliSessionBinding: {
sessionId: "claude-source",
forceReuse: true,
forkNextResume: true,
},
},
});
expect(created.entry).toMatchObject({
createdVia: "plugin",
createdActor: { type: "system", id: "anthropic" },
createdAt: expect.any(Number),
pluginOwnerId: "anthropic",
providerOverride: "claude-cli",
modelOverride: "claude-opus-4-8",
modelOverrideRouteResolution: "resolved",
modelSelectionLocked: true,
execHost: "node",
execNode: "node-a",
execCwd: "/work/on-node",
cliSessionBindings: {
"claude-cli": {
sessionId: "claude-source",
forceReuse: true,
forkNextResume: true,
},
},
});
});
});
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,
initialEntry: {
agentHarnessId: "codex",
modelSelectionLocked: true,
},
afterCreate: async () => {
throw new Error("native binding failed");
},
}),
).rejects.toThrow("native binding failed");
expect(runtime.session.getSessionEntry({ sessionKey: key, readConsistency: "latest" })).toBe(
undefined,
);
});
});
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<void>((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("recovers an exact persisted initializer and returns its finalized generation", async () => {
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: {
sessionId,
sessionFile,
updatedAt: Date.now(),
initializationPending: true,
agentHarnessId: "codex",
modelSelectionLocked: true,
pluginExtensions: persistedPluginExtensions,
spawnedCwd: "/workspace/project",
},
});
const recovered = await runtime.session.createSessionEntry({
cfg: {},
key,
spawnedCwd: "/workspace/project",
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.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("preserves a created entry claimed before finalization", async () => {
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: () => ({ label: "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,
label: "claimed concurrently",
agentHarnessId: "codex",
modelSelectionLocked: true,
});
},
);
});
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("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<void>((resolve) => {
signal.addEventListener("abort", () => resolve(), { once: true });
});
});
await workStarted.promise;
await interruptSessionWorkAdmissions({
scope: storePath,
identities: [sessionKey, sessionId],
});
await work;
expect(admittedSignal?.aborted).toBe(true);
});
});