Files
openclaw/extensions/memory-wiki/src/mutation-coordinator.test.ts
mushuiyu886 f5eacacea5 fix(memory-wiki): stop duplicate bridge imports under polling (#91828)
* fix(memory-wiki): coalesce concurrent source syncs

Co-authored-by: mushuiyu_xydt <yang.haoyu@xydigit.com>

* test(memory-wiki): track alias fixture cleanup

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-10 19:20:15 -07:00

130 lines
4.2 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { withMemoryWikiVaultMutation } from "./mutation-coordinator.js";
import { createMemoryWikiTestHarness } from "./test-helpers.js";
const { createTempDir } = createMemoryWikiTestHarness();
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>((done) => {
resolve = done;
});
return { promise, resolve };
}
describe("withMemoryWikiVaultMutation", () => {
it("serializes mutations for one vault and permits nested work", async () => {
const firstEntered = deferred();
const releaseFirst = deferred();
const order: string[] = [];
const first = withMemoryWikiVaultMutation("/tmp/wiki-a", async () => {
order.push("first:start");
firstEntered.resolve();
await withMemoryWikiVaultMutation("/tmp/wiki-a", async () => {
order.push("first:nested");
});
await releaseFirst.promise;
order.push("first:end");
});
await firstEntered.promise;
const second = withMemoryWikiVaultMutation("/tmp/wiki-a", async () => {
order.push("second");
});
await Promise.resolve();
await Promise.resolve();
expect(order).toEqual(["first:start", "first:nested"]);
releaseFirst.resolve();
await Promise.all([first, second]);
expect(order).toEqual(["first:start", "first:nested", "first:end", "second"]);
});
it("allows distinct agent vaults to mutate in parallel", async () => {
const firstEntered = deferred();
const secondEntered = deferred();
const releaseFirst = deferred();
const first = withMemoryWikiVaultMutation("/tmp/wiki/support", async () => {
firstEntered.resolve();
await releaseFirst.promise;
});
await firstEntered.promise;
const second = withMemoryWikiVaultMutation("/tmp/wiki/marketing", async () => {
secondEntered.resolve();
});
await secondEntered.promise;
releaseFirst.resolve();
await Promise.all([first, second]);
});
it("serializes physical aliases of one vault", async () => {
const root = await createTempDir("memory-wiki-vault-alias-");
const vaultPath = path.join(root, "vault");
const aliasPath = path.join(root, "vault-alias");
await fs.mkdir(vaultPath);
await fs.symlink(vaultPath, aliasPath, process.platform === "win32" ? "junction" : "dir");
const firstEntered = deferred();
const releaseFirst = deferred();
let aliasEntered = false;
let first: Promise<void> | undefined;
let alias: Promise<void> | undefined;
try {
first = withMemoryWikiVaultMutation(vaultPath, async () => {
firstEntered.resolve();
await releaseFirst.promise;
});
await firstEntered.promise;
alias = withMemoryWikiVaultMutation(aliasPath, async () => {
aliasEntered = true;
});
await Promise.resolve();
await Promise.resolve();
expect(aliasEntered).toBe(false);
releaseFirst.resolve();
await Promise.all([first, alias]);
expect(aliasEntered).toBe(true);
} finally {
releaseFirst.resolve();
await Promise.allSettled([first, alias].filter((item) => item !== undefined));
}
});
it("queues detached work after its inherited transaction has ended", async () => {
const releaseDetached = deferred();
const holderEntered = deferred();
const releaseHolder = deferred();
let detachedEntered = false;
let detached!: Promise<void>;
await withMemoryWikiVaultMutation("/tmp/wiki-detached", async () => {
detached = (async () => {
await releaseDetached.promise;
await withMemoryWikiVaultMutation("/tmp/wiki-detached", async () => {
detachedEntered = true;
});
})();
});
const holder = withMemoryWikiVaultMutation("/tmp/wiki-detached", async () => {
holderEntered.resolve();
await releaseHolder.promise;
});
await holderEntered.promise;
releaseDetached.resolve();
await Promise.resolve();
await Promise.resolve();
expect(detachedEntered).toBe(false);
releaseHolder.resolve();
await Promise.all([holder, detached]);
expect(detachedEntered).toBe(true);
});
});