Skip to content

Commit 163d5c4

Browse files
raw34claude
andcommitted
fix(test): share workDir across reflection-bypass-hook tests to fix singleton state issue
The plugin's singleton state (_singletonState) is initialized once on the first register() call. Subsequent register() calls with different dbPath are silently ignored via the WeakSet idempotent guard, leaving hooks pointing at a stale/deleted temp directory. Fix: use before/after (suite-level) instead of beforeEach/afterEach, seed all agent reflection data into a shared workDir, and register the plugin exactly once. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9a6bf0f commit 163d5c4

File tree

1 file changed

+36
-40
lines changed

1 file changed

+36
-40
lines changed

test/reflection-bypass-hook.test.mjs

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, beforeEach, afterEach } from "node:test";
1+
import { describe, it, before, after } from "node:test";
22
import assert from "node:assert/strict";
33
import { mkdtempSync, rmSync } from "node:fs";
44
import { tmpdir } from "node:os";
@@ -105,47 +105,50 @@ async function seedReflection(dbPath, agentId, runAt = Date.now() - 2 * DAY_MS)
105105
});
106106
}
107107

108-
async function invokeReflectionHooks({ workDir, agentId, explicitAgentId = agentId }) {
109-
const pluginConfig = makePluginConfig(workDir);
110-
await seedReflection(pluginConfig.dbPath, agentId);
111-
112-
const harness = createPluginApiHarness({
113-
resolveRoot: workDir,
114-
pluginConfig,
115-
});
116-
117-
memoryLanceDBProPlugin.register(harness.api);
118-
119-
const promptHooks = harness.eventHandlers.get("before_prompt_build") || [];
120-
121-
assert.equal(promptHooks.length, 2, "expected exactly two before_prompt_build hooks (invariants + derived)");
122-
123-
// Sort by priority: lower priority value runs first (invariants=12, derived=15)
124-
const sorted = [...promptHooks].sort((a, b) => (a.meta?.priority ?? 99) - (b.meta?.priority ?? 99));
125-
const ctx = { sessionKey: `agent:${agentId}:test`, agentId: explicitAgentId };
126-
const startResult = await sorted[0].handler({}, ctx); // invariants (priority 12)
127-
const promptResult = await sorted[1].handler({}, ctx); // derived (priority 15)
128-
129-
return { harness, startResult, promptResult };
130-
}
131-
132108
describe("reflection hooks tolerate bypass scope filters", () => {
109+
// Share a single workDir across all tests to avoid plugin singleton
110+
// re-registration issues: _singletonState is initialized once on the
111+
// first register() call and subsequent calls with different dbPath
112+
// are silently ignored, leaving hooks pointing at a stale/deleted DB.
133113
let workDir;
114+
let harness;
134115

135-
beforeEach(() => {
116+
before(async () => {
136117
workDir = mkdtempSync(path.join(tmpdir(), "reflection-bypass-hook-"));
118+
const pluginConfig = makePluginConfig(workDir);
119+
120+
// Seed reflection data for all agents used by tests
121+
for (const agentId of ["system", "undefined", "main"]) {
122+
await seedReflection(pluginConfig.dbPath, agentId);
123+
}
124+
125+
// Register plugin exactly once
126+
harness = createPluginApiHarness({
127+
resolveRoot: workDir,
128+
pluginConfig,
129+
});
130+
memoryLanceDBProPlugin.register(harness.api);
137131
});
138132

139-
afterEach(() => {
133+
after(() => {
140134
rmSync(workDir, { recursive: true, force: true });
141135
});
142136

137+
async function invokeHooks(agentId, explicitAgentId = agentId) {
138+
const promptHooks = harness.eventHandlers.get("before_prompt_build") || [];
139+
assert.equal(promptHooks.length, 2, "expected exactly two before_prompt_build hooks (invariants + derived)");
140+
141+
const sorted = [...promptHooks].sort((a, b) => (a.meta?.priority ?? 99) - (b.meta?.priority ?? 99));
142+
const ctx = { sessionKey: `agent:${agentId}:test`, agentId: explicitAgentId };
143+
const startResult = await sorted[0].handler({}, ctx);
144+
const promptResult = await sorted[1].handler({}, ctx);
145+
146+
return { startResult, promptResult };
147+
}
148+
143149
["system", "undefined"].forEach((reservedAgentId) => {
144150
it(`injects inherited and derived reflection context for bypass agentId=${reservedAgentId}`, async () => {
145-
const { harness, startResult, promptResult } = await invokeReflectionHooks({
146-
workDir,
147-
agentId: reservedAgentId,
148-
});
151+
const { startResult, promptResult } = await invokeHooks(reservedAgentId);
149152

150153
assert.match(startResult?.prependContext || "", /<inherited-rules>/);
151154
assert.match(startResult?.prependContext || "", new RegExp(`Always verify reflection hook coverage for ${reservedAgentId}\\.`));
@@ -160,10 +163,7 @@ describe("reflection hooks tolerate bypass scope filters", () => {
160163
});
161164

162165
it("injects reflection context for a normal non-bypass agent id", async () => {
163-
const { harness, startResult, promptResult } = await invokeReflectionHooks({
164-
workDir,
165-
agentId: "main",
166-
});
166+
const { startResult, promptResult } = await invokeHooks("main");
167167

168168
assert.match(startResult?.prependContext || "", /<inherited-rules>/);
169169
assert.match(startResult?.prependContext || "", /Always verify reflection hook coverage for main\./);
@@ -177,11 +177,7 @@ describe("reflection hooks tolerate bypass scope filters", () => {
177177
});
178178

179179
it("resolves reflection agent id from sessionKey when ctx.agentId is missing", async () => {
180-
const { harness, startResult, promptResult } = await invokeReflectionHooks({
181-
workDir,
182-
agentId: "main",
183-
explicitAgentId: undefined,
184-
});
180+
const { startResult, promptResult } = await invokeHooks("main", undefined);
185181

186182
assert.match(startResult?.prependContext || "", /Always verify reflection hook coverage for main\./);
187183
assert.match(promptResult?.prependContext || "", /Next run exercise the reflection injection path for main\./);

0 commit comments

Comments
 (0)