import fs from "node:fs/promises"; import path from "node:path"; import { createPluginRegistryFixture, registerTestPlugin, } from "openclaw/plugin-sdk/plugin-test-contracts"; // Session entry projection contract tests cover plugin session entry projection behavior. import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; import { listSessionEntriesCore, replaceSessionEntry, } from "../../config/sessions/session-accessor.js"; import { withTempConfig } from "../../gateway/test-temp-config.js"; import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { createPluginHostRegistryRetirement, runPluginHostCleanup } from "../host-hook-cleanup.js"; import { clearPluginHostRuntimeState } from "../host-hook-runtime.js"; import { patchPluginSessionExtension } from "../host-hook-state.js"; import type { PluginJsonValue } from "../host-hooks.js"; import { createEmptyPluginRegistry } from "../registry-empty.js"; import { setActivePluginRegistry } from "../runtime.js"; import { createPluginRecord } from "../status.test-fixtures.js"; import { runTrustedToolPolicies } from "../trusted-tool-policy.js"; const requireRecord = createRequireRecord("object", "expected-label"); async function expectOkResult(promise: Promise, label: string) { const result = requireRecord(await promise, label); expect(result.ok).toBe(true); return result; } async function expectNoCleanupFailures(promise: Promise, label: string) { const result = requireRecord(await promise, label); expect(result.failures).toEqual([]); return result; } function extensionNamespace(entry: Record, pluginId: string, namespace: string) { const extensions = requireRecord(entry.pluginExtensions, "plugin extensions"); const pluginExtensions = requireRecord(extensions[pluginId], `${pluginId} extensions`); return requireRecord(pluginExtensions[namespace], `${pluginId}.${namespace} state`); } function loadSessionStore( storePath: string, _options?: { skipCache?: boolean }, ): Record { return Object.fromEntries( listSessionEntriesCore({ agentId: "main", storePath }).map(({ sessionKey, entry }) => [ sessionKey, entry, ]), ); } async function updateSessionStore( storePath: string, update: (store: Record) => void, ): Promise { const store: Record = {}; update(store); for (const [sessionKey, entry] of Object.entries(store)) { await replaceSessionEntry({ sessionKey, storePath }, entry); } } async function withProjectionSessionStore( prefix: string, run: (fixture: { storePath: string; tempConfig: { session: { store: string } }; }) => Promise, ): Promise { const stateDir = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), prefix)); const storePath = path.join(stateDir, "sessions.json"); const tempConfig = { agents: { entries: { main: { default: true } } }, session: { store: storePath }, }; try { return await withEnvAsync( { OPENCLAW_STATE_DIR: stateDir }, async () => await withTempConfig({ cfg: tempConfig, run: async () => await run({ storePath, tempConfig }), }), ); } finally { await fs.rm(stateDir, { recursive: true, force: true }); } } describe("plugin session extension SessionEntry projection", () => { beforeEach(() => { setActivePluginRegistry(createEmptyPluginRegistry()); clearPluginHostRuntimeState(); }); afterEach(() => { setActivePluginRegistry(createEmptyPluginRegistry()); clearPluginHostRuntimeState(); }); it("mirrors projected values to SessionEntry[slotKey] and clears them on unset", async () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "promoted-plugin", name: "Promoted" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", sessionEntrySlotSchema: { type: "object" }, project: (ctx) => { if (!ctx.state || typeof ctx.state !== "object" || Array.isArray(ctx.state)) { return undefined; } const state = ctx.state as Record; return { state: state.state ?? null, title: state.title ?? null }; }, }); }, }); setActivePluginRegistry(registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); const patchResult = await patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "promoted-plugin", namespace: "workflow", value: { state: "executing", title: "Deploy approval", internal: 7 }, }); expect(patchResult.ok).toBe(true); const afterPatch = loadSessionStore(storePath, { skipCache: true }); expect( (afterPatch["agent:main:main"] as unknown as Record).approvalSnapshot, ).toEqual({ state: "executing", title: "Deploy approval" }); const unsetResult = await patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "promoted-plugin", namespace: "workflow", unset: true, }); expect(unsetResult.ok).toBe(true); const afterUnset = loadSessionStore(storePath, { skipCache: true }); expect( (afterUnset["agent:main:main"] as unknown as Record).approvalSnapshot, ).toBeUndefined(); }, ); }); it("clears promoted SessionEntry slots when projectors fail", async () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "failing-promoted-plugin", name: "Failing" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", sessionEntrySlotSchema: { type: "object" }, project: (ctx) => { const state = ctx.state as Record; if (state.fail === "throw") { throw new Error("projection failed"); } if (state.fail === "promise") { return Promise.resolve({ state: "async" }) as never; } return { state: state.state ?? null }; }, }); }, }); setActivePluginRegistry(registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-projector-fail-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "failing-promoted-plugin", namespace: "workflow", value: { state: "ready" }, }), "ready patch result", ); expect( ( loadSessionStore(storePath, { skipCache: true })[ "agent:main:main" ] as unknown as Record ).approvalSnapshot, ).toEqual({ state: "ready" }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "failing-promoted-plugin", namespace: "workflow", value: { state: "bad", fail: "throw" }, }), "throwing projector patch result", ); const afterThrow = loadSessionStore(storePath, { skipCache: true })[ "agent:main:main" ] as unknown as Record; expect(afterThrow.approvalSnapshot).toBeUndefined(); expect(extensionNamespace(afterThrow, "failing-promoted-plugin", "workflow")).toEqual({ state: "bad", fail: "throw", }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "failing-promoted-plugin", namespace: "workflow", value: { state: "ready-again" }, }), "ready-again patch result", ); expect( ( loadSessionStore(storePath, { skipCache: true })[ "agent:main:main" ] as unknown as Record ).approvalSnapshot, ).toEqual({ state: "ready-again" }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "failing-promoted-plugin", namespace: "workflow", value: { state: "async-bad", fail: "promise" }, }), "promise projector patch result", ); const afterPromise = loadSessionStore(storePath, { skipCache: true })[ "agent:main:main" ] as unknown as Record; expect(afterPromise.approvalSnapshot).toBeUndefined(); expect(extensionNamespace(afterPromise, "failing-promoted-plugin", "workflow")).toEqual({ state: "async-bad", fail: "promise", }); }, ); }); it("rejects sessionEntrySlotKey values that collide with SessionEntry fields", () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "slot-collision", name: "Slot Collision" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "bad slot", sessionEntrySlotKey: "updatedAt", }); api.registerSessionExtension({ namespace: "main-recovery", description: "bad main recovery slot", sessionEntrySlotKey: "mainRestartRecovery", }); api.registerSessionExtension({ namespace: "recovery", description: "bad fresh-main slot", sessionEntrySlotKey: "subagentRecovery", }); api.registerSessionExtension({ namespace: "run-error", description: "bad run error slot", sessionEntrySlotKey: "lastRunError", }); api.registerSessionExtension({ namespace: "transcript-path", description: "retired transcript locator", sessionEntrySlotKey: "transcriptPath", }); api.registerSessionExtension({ namespace: "custom-icon", description: "reserved custom icon", sessionEntrySlotKey: "icon", }); api.registerSessionExtension({ namespace: "context-window-source", description: "reserved context window provenance", sessionEntrySlotKey: "contextTokensSource", }); api.registerSessionExtension({ namespace: "sandbox-policy", description: "reserved creation-only sandbox requirement", sessionEntrySlotKey: "sandbox", }); api.registerSessionExtension({ namespace: "pending-final-text", description: "retired pending-final field", sessionEntrySlotKey: "pendingFinalDeliveryText", }); for (const field of ["execSecurity", "execAsk"]) { api.registerSessionExtension({ namespace: `retired-${field.toLowerCase()}`, description: "retired session exec policy", sessionEntrySlotKey: field, }); } }, }); expect(registry.registry.sessionExtensions).toHaveLength(0); expect( registry.registry.diagnostics.map(({ pluginId, message }) => ({ pluginId, message })), ).toStrictEqual([ { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: updatedAt", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: mainRestartRecovery", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: subagentRecovery", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: lastRunError", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: transcriptPath", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: icon", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: contextTokensSource", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: sandbox", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: pendingFinalDeliveryText", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: execSecurity", }, { pluginId: "slot-collision", message: "sessionEntrySlotKey is reserved by SessionEntry: execAsk", }, ]); }); it("rejects sessionEntrySlotKey values inherited from Object.prototype", () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "object-slot-collision", name: "Object Slot Collision" }), register(api) { api.registerSessionExtension({ namespace: "to-string", description: "bad object slot", sessionEntrySlotKey: "toString", }); api.registerSessionExtension({ namespace: "has-own", description: "bad object slot", sessionEntrySlotKey: "hasOwnProperty", }); api.registerSessionExtension({ namespace: "value-of", description: "bad object slot", sessionEntrySlotKey: "valueOf", }); }, }); expect(registry.registry.sessionExtensions).toHaveLength(0); const diagnostics = registry.registry.diagnostics.map(({ pluginId, message }) => ({ pluginId, message, })); expect(diagnostics).toStrictEqual([ { pluginId: "object-slot-collision", message: "sessionEntrySlotKey is reserved by Object: toString", }, { pluginId: "object-slot-collision", message: "sessionEntrySlotKey is reserved by Object: hasOwnProperty", }, { pluginId: "object-slot-collision", message: "sessionEntrySlotKey is reserved by Object: valueOf", }, ]); }); it("rejects duplicate promoted SessionEntry slot keys across registrations", () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "slot-owner", name: "Slot Owner" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "first promoted slot", sessionEntrySlotKey: "approvalSnapshot", }); api.registerSessionExtension({ namespace: "recovery", description: "same plugin duplicate slot", sessionEntrySlotKey: " approvalSnapshot ", }); }, }); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "slot-colliding-plugin", name: "Slot Colliding" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "cross-plugin duplicate slot", sessionEntrySlotKey: "approvalSnapshot", }); }, }); expect(registry.registry.sessionExtensions).toHaveLength(1); expect(registry.registry.sessionExtensions[0]?.extension.sessionEntrySlotKey).toBe( "approvalSnapshot", ); const diagnostics = registry.registry.diagnostics.map(({ pluginId, message }) => ({ pluginId, message, })); expect(diagnostics).toStrictEqual([ { pluginId: "slot-owner", message: "sessionEntrySlotKey already registered: approvalSnapshot", }, { pluginId: "slot-colliding-plugin", message: "sessionEntrySlotKey already registered: approvalSnapshot", }, ]); }); it("clears promoted SessionEntry slots with plugin-owned session state", async () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "cleanup-promoted-plugin", name: "Cleanup" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); }, }); setActivePluginRegistry(registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-cleanup-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "cleanup-promoted-plugin", namespace: "workflow", value: { state: "waiting" }, }), "cleanup patch result", ); await expectNoCleanupFailures( runPluginHostCleanup({ cfg: tempConfig as never, registry: registry.registry, pluginId: "cleanup-promoted-plugin", reason: "delete", }), "cleanup result", ); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.pluginExtensions).toBeUndefined(); expect(entry.approvalSnapshot).toBeUndefined(); }, ); }); it("uses the active registry to clear promoted slots when cleanup omits registry", async () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "active-cleanup-promoted-plugin", name: "Cleanup" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); }, }); setActivePluginRegistry(registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-active-cleanup-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "active-cleanup-promoted-plugin", namespace: "workflow", value: { state: "waiting" }, }), "active cleanup patch result", ); await expectNoCleanupFailures( runPluginHostCleanup({ cfg: tempConfig as never, pluginId: "active-cleanup-promoted-plugin", reason: "delete", }), "active cleanup result", ); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.pluginExtensions).toBeUndefined(); expect(entry.approvalSnapshot).toBeUndefined(); }, ); }); it("clears stale promoted SessionEntry slots on plugin restart without deleting extension state", async () => { const previousFixture = createPluginRegistryFixture(); registerTestPlugin({ registry: previousFixture.registry, config: previousFixture.config, record: createPluginRecord({ id: "restart-promoted-plugin", name: "Restart" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); }, }); const nextFixture = createPluginRegistryFixture(); registerTestPlugin({ registry: nextFixture.registry, config: nextFixture.config, record: createPluginRecord({ id: "restart-promoted-plugin", name: "Restart" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", }); }, }); setActivePluginRegistry(previousFixture.registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-restart-cleanup-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "restart-promoted-plugin", namespace: "workflow", value: { state: "waiting" }, }), "restart patch result", ); await expectNoCleanupFailures( createPluginHostRegistryRetirement({ cfg: tempConfig as never, previousRegistry: previousFixture.registry.registry, nextRegistry: nextFixture.registry.registry, })(), "restart cleanup result", ); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.approvalSnapshot).toBeUndefined(); expect(entry.pluginExtensionSlotKeys).toBeUndefined(); expect(entry.pluginExtensions).toEqual({ "restart-promoted-plugin": { workflow: { state: "waiting" }, }, }); }, ); }); it("clears only stale promoted SessionEntry slots on mixed plugin restart", async () => { const previousFixture = createPluginRegistryFixture(); registerTestPlugin({ registry: previousFixture.registry, config: previousFixture.config, record: createPluginRecord({ id: "restart-mixed-plugin", name: "Restart" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); api.registerSessionExtension({ namespace: "legacy", description: "legacy promoted workflow", sessionEntrySlotKey: "legacyApprovalSnapshot", }); }, }); const nextFixture = createPluginRegistryFixture(); registerTestPlugin({ registry: nextFixture.registry, config: nextFixture.config, record: createPluginRecord({ id: "restart-mixed-plugin", name: "Restart" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); api.registerSessionExtension({ namespace: "legacy", description: "legacy workflow", }); }, }); setActivePluginRegistry(previousFixture.registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-restart-mixed-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "restart-mixed-plugin", namespace: "workflow", value: { state: "waiting" }, }), "mixed restart workflow patch result", ); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "restart-mixed-plugin", namespace: "legacy", value: { state: "legacy" }, }), "mixed restart legacy patch result", ); await expectNoCleanupFailures( createPluginHostRegistryRetirement({ cfg: tempConfig as never, previousRegistry: previousFixture.registry.registry, nextRegistry: nextFixture.registry.registry, })(), "mixed restart cleanup result", ); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.approvalSnapshot).toEqual({ state: "waiting" }); expect(entry.legacyApprovalSnapshot).toBeUndefined(); expect(entry.pluginExtensionSlotKeys).toEqual({ "restart-mixed-plugin": { workflow: "approvalSnapshot", }, }); expect(entry.pluginExtensions).toEqual({ "restart-mixed-plugin": { workflow: { state: "waiting" }, legacy: { state: "legacy" }, }, }); }, ); }); it("preserves promoted SessionEntry slots on plugin restart when the slot is still declared", async () => { const previousFixture = createPluginRegistryFixture(); registerTestPlugin({ registry: previousFixture.registry, config: previousFixture.config, record: createPluginRecord({ id: "restart-preserved-plugin", name: "Restart" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); }, }); const nextFixture = createPluginRegistryFixture(); registerTestPlugin({ registry: nextFixture.registry, config: nextFixture.config, record: createPluginRecord({ id: "restart-preserved-plugin", name: "Restart" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "promoted workflow", sessionEntrySlotKey: "approvalSnapshot", }); }, }); setActivePluginRegistry(previousFixture.registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-restart-preserve-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "restart-preserved-plugin", namespace: "workflow", value: { state: "waiting" }, }), "preserved restart patch result", ); await expectNoCleanupFailures( createPluginHostRegistryRetirement({ cfg: tempConfig as never, previousRegistry: previousFixture.registry.registry, nextRegistry: nextFixture.registry.registry, })(), "preserved restart cleanup result", ); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.approvalSnapshot).toEqual({ state: "waiting" }); expect(entry.pluginExtensionSlotKeys).toEqual({ "restart-preserved-plugin": { workflow: "approvalSnapshot", }, }); expect(entry.pluginExtensions).toEqual({ "restart-preserved-plugin": { workflow: { state: "waiting" }, }, }); }, ); }); it("clears persisted promoted slots when registry metadata is unavailable", async () => { setActivePluginRegistry(createEmptyPluginRegistry()); await withProjectionSessionStore( "openclaw-host-hooks-slot-metadata-cleanup-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), pluginExtensions: { "removed-promoted-plugin": { workflow: { state: "stale" }, }, }, pluginExtensionSlotKeys: { "removed-promoted-plugin": { workflow: "approvalSnapshot", }, }, approvalSnapshot: { state: "stale" }, } as unknown as SessionEntry; }); await expectNoCleanupFailures( runPluginHostCleanup({ cfg: tempConfig as never, pluginId: "removed-promoted-plugin", reason: "delete", }), "metadata cleanup result", ); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.approvalSnapshot).toBeUndefined(); expect(entry.pluginExtensionSlotKeys).toBeUndefined(); expect(entry.pluginExtensions).toBeUndefined(); }, ); }); it("exposes scoped session extension reads to trusted tool policies", async () => { const seen: unknown[] = []; const seenConfig: unknown[] = []; const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "policy-plugin", name: "Policy Plugin", origin: "bundled", }), register(api) { api.registerSessionExtension({ namespace: "policy", description: "policy state", }); api.registerSessionExtension({ namespace: "second", description: "second policy state", }); api.registerTrustedToolPolicy({ id: "inspect-session-state", description: "inspect session extension", evaluate(eventValue, ctx) { seen.push(ctx.getSessionExtension?.("policy")); seen.push(ctx.getSessionExtension?.("second")); seen.push(ctx.getSessionExtension?.("missing")); seenConfig.push((ctx as { config?: unknown }).config); return undefined; }, }); }, }); setActivePluginRegistry(registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-policy-read-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "policy-plugin", namespace: "policy", value: { gate: "open" }, }), "policy patch result", ); await expectOkResult( patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "policy-plugin", namespace: "second", value: { gate: "second" }, }), "second policy patch result", ); await expect( runTrustedToolPolicies( { toolName: "apply_patch", params: {} }, { toolName: "apply_patch", sessionKey: "agent:main:main", }, { config: tempConfig as never }, ), ).resolves.toBeUndefined(); await expect( runTrustedToolPolicies( { toolName: "apply_patch", params: {} }, { toolName: "apply_patch", sessionKey: "agent:main:main", }, ), ).resolves.toBeUndefined(); }, ); expect(seen).toEqual([ { gate: "open" }, { gate: "second" }, undefined, { gate: "open" }, { gate: "second" }, undefined, ]); expect(seenConfig).toEqual([undefined, undefined]); }); it("does not touch top-level SessionEntry slots when sessionEntrySlotKey is omitted", async () => { const { config, registry } = createPluginRegistryFixture(); registerTestPlugin({ registry, config, record: createPluginRecord({ id: "non-promoted-plugin", name: "Non" }), register(api) { api.registerSessionExtension({ namespace: "workflow", description: "non-promoted workflow", }); }, }); setActivePluginRegistry(registry.registry); await withProjectionSessionStore( "openclaw-host-hooks-slot-noop-", async ({ storePath, tempConfig }) => { await updateSessionStore(storePath, (store) => { store["agent:main:main"] = { sessionId: "session-id", updatedAt: Date.now(), } as unknown as SessionEntry; }); const result = await patchPluginSessionExtension({ cfg: tempConfig as never, sessionKey: "agent:main:main", pluginId: "non-promoted-plugin", namespace: "workflow", value: { state: "executing" }, }); expect(result.ok).toBe(true); const stored = loadSessionStore(storePath, { skipCache: true }); const entry = stored["agent:main:main"] as unknown as Record; expect(entry.approvalSnapshot).toBeUndefined(); }, ); }); });