Files
openclaw/src/plugins/runtime-workspace-state.test.ts
lsr911 583b7195b4 fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting (#93719)
* fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting

sessions.list was O(rows) slow under concurrent agent/cron load because
each row read a process-global active plugin-registry workspace dir
that was mutated by other turns between rows. The per-row memo key
changed every time, so loadPluginMetadataSnapshot scanned fresh
(~100ms per row).

Fix:
1. Add AsyncLocalStorage-based workspace dir pinning to
   runtime-workspace-state.ts — withPinnedActivePluginRegistryWorkspaceDir()
   snapshots the current workspace dir for the duration of a callback.
2. Wrap listSessionsFromStoreAsync body in the pin so all per-row
   metadata lookups use a stable memo key.

Fixes #90814

* test(plugins): cover request-scoped workspace pins

* fix(plugins): pin canonical runtime workspace reads

* fix(plugins): preserve workspace pins across reloads

---------

Co-authored-by: lsr911 <lsr911@github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-17 03:33:46 +08:00

113 lines
4.0 KiB
TypeScript

// Verifies request-scoped plugin workspace pins under concurrent registry mutation.
import { afterEach, describe, expect, it } from "vitest";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
import { withPinnedActivePluginRegistryWorkspaceDir } from "./runtime-workspace-state.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
function setActiveWorkspace(workspaceDir: string): void {
setActivePluginRegistry(
createEmptyPluginRegistry(),
workspaceDir,
"gateway-bindable",
workspaceDir,
);
}
function createDeferred(): { promise: Promise<void>; resolve: () => void } {
let resolve: (() => void) | undefined;
const promise = new Promise<void>((done) => {
resolve = done;
});
if (!resolve) {
throw new Error("Expected deferred resolver");
}
return { promise, resolve };
}
describe("runtime workspace state pin", () => {
afterEach(() => {
resetPluginRuntimeStateForTest();
});
it("reads the live global workspace dir when no pin is active", () => {
setActiveWorkspace("/workspace/a");
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/a");
setActiveWorkspace("/workspace/b");
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/b");
});
it("isolates overlapping pins and restores live state after success", async () => {
const firstPinned = createDeferred();
const resumeFirst = createDeferred();
const firstObserved: Array<string | undefined> = [];
setActiveWorkspace("/workspace/a");
const firstRequest = withPinnedActivePluginRegistryWorkspaceDir(async () => {
firstObserved.push(getActivePluginRegistryWorkspaceDirFromState());
firstPinned.resolve();
await resumeFirst.promise;
firstObserved.push(getActivePluginRegistryWorkspaceDirFromState());
});
await firstPinned.promise;
setActiveWorkspace("/workspace/b");
const secondObserved = await withPinnedActivePluginRegistryWorkspaceDir(async () => {
const observed = [getActivePluginRegistryWorkspaceDirFromState()];
setActiveWorkspace("/workspace/c");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
observed.push(getActivePluginRegistryWorkspaceDirFromState());
return observed;
});
resumeFirst.resolve();
await firstRequest;
expect(firstObserved).toEqual(["/workspace/a", "/workspace/a"]);
expect(secondObserved).toEqual(["/workspace/b", "/workspace/b"]);
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/c");
});
it("reuses the outer pin for nested scopes", async () => {
setActiveWorkspace("/workspace/a");
await withPinnedActivePluginRegistryWorkspaceDir(async () => {
setActiveWorkspace("/workspace/b");
await withPinnedActivePluginRegistryWorkspaceDir(async () => {
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/a");
});
});
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/b");
});
it("preserves the pin across module reloads", async () => {
setActiveWorkspace("/workspace/a");
await withPinnedActivePluginRegistryWorkspaceDir(async () => {
setActiveWorkspace("/workspace/b");
const reloaded = await import(
new URL("./runtime-workspace-state.ts?workspace-pin-reload", import.meta.url).href
);
expect(reloaded.getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/a");
});
});
it("propagates rejections without leaking the pin", async () => {
setActiveWorkspace("/workspace/a");
await expect(
withPinnedActivePluginRegistryWorkspaceDir(async () => {
setActiveWorkspace("/workspace/b");
throw new Error("workspace request failed");
}),
).rejects.toThrow("workspace request failed");
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/b");
});
});