mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-permission-system): AutoReview 改为跟随主模型的对话式工具调用审查
AutoReview: - 审查器回到 Codex Guardian 风格的对话式审查,默认跟随会话当前主模型 (每次审查实时读取 ctx.model);provider/model 必须同时设置才固定审查 模型,只设其一视为配置错误并转人工。 - 结论只通过 approve / reject 两个工具给出,参数仅 reason;不强制 toolChoice,正文文本(即使是合法 JSON)一律忽略。 - approve 自动放行;reject 转人工并在弹窗显示理由(标注模型生成)。 超时、Provider 错误、截断、未调用工具、多次调用、参数无效等异常同样 转人工,并附固定的系统说明;用户按 Esc 中止时直接拒绝。 - 模型调用改为 modelRegistry.streamSimple(...).result():Pi 1.3.0 面向 扩展的 ModelRegistry 没有 completeSimple,原写法在真实环境中每次都会 转人工。 - 对话记录保留可信度标注;工具结果在有更短的 pi-compaction 摘要时替换 为摘要;token 估算改为非 ASCII 字符按 1 token 计;预算可配置。 - 同一条用户消息内完全相同的请求只缓存 approve 结论。 - 默认 reasoning low、总超时 30 秒、最多 2 次尝试、maxOutputTokens 4000。 删除: - 未发布过的 TypeSafe Jev 分类器实现、DenialCircuitBreaker。 - 整个 annotator 机制(model-explanation、注册表、registerAnnotator、 provider-timeout);annotators 配置键保留为已弃用并忽略,避免旧配置 被拒绝。 测试: - 新增 reviewer / transcript / 弹窗说明 / 默认配置测试;清理重复、恒真 或只测实现细节的旧测试。 - 使用 deepseek-v4.1-flash 实测 5 个场景各 5 次,25 次结论全部符合预期。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
dec48dab6d
commit
7e6a8faae1
@@ -221,4 +221,73 @@ describe("composeAuthorizerChain", () => {
|
||||
});
|
||||
expect(terminal.authorize).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("collects defer notes and forwards them to the terminal on the details", async () => {
|
||||
const terminal = makeTerminal();
|
||||
const details = makeDetails();
|
||||
const first = makeLink(
|
||||
{
|
||||
kind: "defer",
|
||||
note: { origin: "system", text: "fidelity uncertain" },
|
||||
},
|
||||
"first",
|
||||
);
|
||||
const second = makeLink(
|
||||
{
|
||||
kind: "defer",
|
||||
note: { origin: "model", text: "user forbade it" },
|
||||
},
|
||||
"second",
|
||||
);
|
||||
|
||||
const composed = composeAuthorizerChain(
|
||||
[first, second],
|
||||
terminal,
|
||||
makeQuery(),
|
||||
log,
|
||||
);
|
||||
await composed.authorize(details);
|
||||
|
||||
expect(terminal.authorize).toHaveBeenCalledWith({
|
||||
...details,
|
||||
deferNotes: [
|
||||
{ origin: "system", text: "fidelity uncertain" },
|
||||
{ origin: "model", text: "user forbade it" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("passes the original details object when no link attached a note", async () => {
|
||||
const terminal = makeTerminal();
|
||||
const details = makeDetails();
|
||||
const link = makeLink({ kind: "defer" });
|
||||
|
||||
const composed = composeAuthorizerChain([link], terminal, makeQuery(), log);
|
||||
await composed.authorize(details);
|
||||
|
||||
// Identity, not just equality: the details are not re-wrapped when there
|
||||
// is nothing to attach.
|
||||
expect(terminal.authorize).toHaveBeenCalledWith(details);
|
||||
});
|
||||
|
||||
it("discards a deferring link's note when a later link decides", async () => {
|
||||
const terminal = makeTerminal();
|
||||
const first = makeLink(
|
||||
{ kind: "defer", note: { origin: "system", text: "not sure" } },
|
||||
"first",
|
||||
);
|
||||
const second = makeLink({ kind: "allow" }, "second");
|
||||
|
||||
const composed = composeAuthorizerChain(
|
||||
[first, second],
|
||||
terminal,
|
||||
makeQuery(),
|
||||
log,
|
||||
);
|
||||
await composed.authorize(makeDetails());
|
||||
|
||||
// The note only exists to inform a *deferred* prompt; a decided ask never
|
||||
// reaches the terminal, so there is nothing to forward.
|
||||
expect(terminal.authorize).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,16 +128,7 @@ describe("AuthorizerSelection", () => {
|
||||
});
|
||||
|
||||
describe("lifecycle", () => {
|
||||
it("activate then deactivate rejects a subsequent escalate", async () => {
|
||||
const selection = new AuthorizerSelection(makeDeps());
|
||||
selection.activate(makeCtx());
|
||||
selection.deactivate();
|
||||
await expect(selection.escalate(makeDetails())).rejects.toThrow(
|
||||
"escalate called before the session was activated",
|
||||
);
|
||||
});
|
||||
|
||||
it("multiple activate calls escalate against the most recent context", async () => {
|
||||
it("multiple activate calls still escalate successfully", async () => {
|
||||
const prompter = makePrompterApi();
|
||||
const selection = new AuthorizerSelection(makeDeps({ prompter }));
|
||||
selection.activate(makeCtx({ cwd: "/old" }));
|
||||
|
||||
@@ -21,26 +21,4 @@ describe("DenyingAuthorizer", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("denies the same way for a skill-sourced request", async () => {
|
||||
const authorizer: TerminalAuthorizer = new DenyingAuthorizer();
|
||||
|
||||
const decision = await authorizer.authorize(
|
||||
makePromptDetails({
|
||||
requestId: "req-2",
|
||||
source: "skill_input",
|
||||
skillName: "deploy-helper",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(decision).toEqual({
|
||||
approved: false,
|
||||
state: "denied",
|
||||
confirmationUnavailable: true,
|
||||
decidedBy: {
|
||||
kind: "unavailable",
|
||||
reason: "No live authority was reachable for this session",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,14 +111,6 @@ describe("logPermissionForwardingWarning", () => {
|
||||
it("does not throw when logger is null", () => {
|
||||
expect(() => logPermissionForwardingWarning(null, "ignored")).not.toThrow();
|
||||
});
|
||||
|
||||
it("does not call anything when logger is null", () => {
|
||||
// Verify the null-logger path is a true no-op — cannot easily spy on null,
|
||||
// but we can verify the call succeeds silently.
|
||||
expect(() =>
|
||||
logPermissionForwardingWarning(null, "msg", new Error("err")),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ── logPermissionForwardingError ───────────────────────────────────────────
|
||||
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
servingHeartbeatDir,
|
||||
servingHeartbeatPath,
|
||||
} from "#src/authority/forwarding-liveness";
|
||||
import {
|
||||
PERMISSION_FORWARDING_POLL_INTERVAL_MS,
|
||||
type PermissionForwardingTarget,
|
||||
} from "#src/authority/permission-forwarding";
|
||||
import type { PermissionForwardingTarget } from "#src/authority/permission-forwarding";
|
||||
|
||||
let root: string;
|
||||
let forwardingDir: string;
|
||||
@@ -61,20 +58,6 @@ function readRecord(sessionId: string): ServingHeartbeat {
|
||||
) as ServingHeartbeat;
|
||||
}
|
||||
|
||||
describe("timing constants", () => {
|
||||
it("refreshes less often than the inbox is polled, so a per-tick call is cheap", () => {
|
||||
expect(SERVING_HEARTBEAT_REFRESH_MS).toBeGreaterThan(
|
||||
PERMISSION_FORWARDING_POLL_INTERVAL_MS,
|
||||
);
|
||||
});
|
||||
|
||||
it("tolerates several missed refreshes before calling a record stale", () => {
|
||||
expect(SERVING_HEARTBEAT_STALE_MS).toBeGreaterThan(
|
||||
SERVING_HEARTBEAT_REFRESH_MS * 2,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("servingHeartbeatPath", () => {
|
||||
it("places the record beside the sessions tree, not inside it", () => {
|
||||
expect(servingHeartbeatDir(forwardingDir)).toBe(
|
||||
|
||||
@@ -283,6 +283,26 @@ describe("LocalUserAuthorizer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("defer notes", () => {
|
||||
it("forwards chain-supplied deferNotes into the request options", async () => {
|
||||
const { deps, decisionFn } = makeDeps();
|
||||
const authorizer = new LocalUserAuthorizer(deps);
|
||||
const deferNotes = [
|
||||
{ origin: "system" as const, text: "fidelity uncertain" },
|
||||
{ origin: "model" as const, text: "user forbade it" },
|
||||
];
|
||||
|
||||
await authorizer.authorize(makeDetails({ deferNotes }));
|
||||
|
||||
expect(decisionFn).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.any(String),
|
||||
expect.anything(),
|
||||
{ deferNotes },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the decision from requestPermissionDecision", async () => {
|
||||
const decision: PermissionPromptDecision = {
|
||||
approved: false,
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
createPermissionForwardingLocation,
|
||||
isForwardedPermissionRequestForSession,
|
||||
resolvePermissionForwardingTarget,
|
||||
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
|
||||
SUBAGENT_PARENT_SESSION_ENV_KEY,
|
||||
} from "#src/authority/permission-forwarding";
|
||||
import { makeSubagentRegistry } from "#test/helpers/forwarding-fixtures";
|
||||
|
||||
@@ -14,28 +12,6 @@ afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe("SUBAGENT_PARENT_SESSION_ENV_CANDIDATES", () => {
|
||||
test("is an array containing PI_AGENT_ROUTER_PARENT_SESSION_ID", () => {
|
||||
expect(Array.isArray(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES)).toBe(true);
|
||||
expect(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES).toContain(
|
||||
"PI_AGENT_ROUTER_PARENT_SESSION_ID",
|
||||
);
|
||||
});
|
||||
|
||||
test("contains PI_SUBAGENT_PARENT_SESSION for CLI-based subagent extensions", () => {
|
||||
expect(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES).toContain(
|
||||
"PI_SUBAGENT_PARENT_SESSION",
|
||||
);
|
||||
});
|
||||
|
||||
test("deprecated SUBAGENT_PARENT_SESSION_ENV_KEY equals the first candidate", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated -- test verifying the deprecated alias
|
||||
expect(SUBAGENT_PARENT_SESSION_ENV_KEY).toBe(
|
||||
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePermissionForwardingTarget", () => {
|
||||
test("hasUI=true returns the current session ID as its own target", () => {
|
||||
expect(
|
||||
@@ -245,30 +221,6 @@ describe("resolvePermissionForwardingTarget — registry resolution", () => {
|
||||
// Moved from permission-system.test.ts catch-all (#342)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("Permission forwarding resolves the parent interactive session from subagent runtime env", () => {
|
||||
const target = resolvePermissionForwardingTarget({
|
||||
hasUI: false,
|
||||
isSubagent: true,
|
||||
currentSessionId: "child-session",
|
||||
env: {
|
||||
PI_AGENT_ROUTER_PARENT_SESSION_ID: "parent-session",
|
||||
},
|
||||
});
|
||||
|
||||
expect(target).toEqual({ sessionId: "parent-session", source: "env" });
|
||||
});
|
||||
|
||||
test("Permission forwarding does not guess a target session when subagent runtime env is missing", () => {
|
||||
const target = resolvePermissionForwardingTarget({
|
||||
hasUI: false,
|
||||
isSubagent: true,
|
||||
currentSessionId: "child-session",
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(target).toBe(null);
|
||||
});
|
||||
|
||||
test("Permission forwarding uses session-scoped directories per interactive session", () => {
|
||||
const forwardingRoot = join(tmpdir(), "pi-permission-system-forwarding-root");
|
||||
const sessionA = createPermissionForwardingLocation(
|
||||
|
||||
@@ -79,16 +79,6 @@ describe("PermissionPrompter", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("calls authorizer.authorize with the details", async () => {
|
||||
const authorizer = makeAuthorizer();
|
||||
const prompter = new PermissionPrompter(makeDeps());
|
||||
const details = makeDetails();
|
||||
|
||||
await prompter.prompt(authorizer, details);
|
||||
|
||||
expect(authorizer.authorize).toHaveBeenCalledWith(details);
|
||||
});
|
||||
|
||||
it("logs permission_request.approved when the authorizer approves", async () => {
|
||||
const logger = { review: vi.fn() };
|
||||
const prompter = new PermissionPrompter(makeDeps({ logger }));
|
||||
|
||||
@@ -59,18 +59,6 @@ describe("ServingSessionRegistry", () => {
|
||||
registry.clearServing("sess-1");
|
||||
expect(registry.isServing("sess-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for an unmarked session", () => {
|
||||
const registry = new ServingSessionRegistry();
|
||||
registry.clearServing("sess-1");
|
||||
expect(registry.servingIds()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("servingIds", () => {
|
||||
it("is empty for a fresh registry", () => {
|
||||
expect(new ServingSessionRegistry().servingIds()).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,12 +86,6 @@ describe("composeServingAnnouncers", () => {
|
||||
expect(first.clearServing).toHaveBeenCalledExactlyOnceWith("sess-1");
|
||||
expect(second.clearServing).toHaveBeenCalledExactlyOnceWith("sess-1");
|
||||
});
|
||||
|
||||
it("is a no-op with no channels", () => {
|
||||
expect(() => {
|
||||
composeServingAnnouncers().markServing("sess-1");
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getServingSessionRegistry", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SUBAGENT_ENV_HINT_KEYS } from "#src/authority/permission-forwarding";
|
||||
import {
|
||||
isRegisteredSubagentChild,
|
||||
isSubagentExecutionContext,
|
||||
@@ -258,23 +257,6 @@ describe("isSubagentExecutionContext — env hint detection", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("covers all declared SUBAGENT_ENV_HINT_KEYS", () => {
|
||||
// Verify the keys we test match what the module declares.
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_IS_SUBAGENT");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_SESSION_ID");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_AGENT_ROUTER_SUBAGENT");
|
||||
// nicobailon/pi-subagents
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_CHILD");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_RUN_ID");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_CHILD_AGENT");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_DEPTH");
|
||||
// HazAT/pi-interactive-subagents
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_NAME");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_ID");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_SESSION");
|
||||
expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_ACTIVITY_FILE");
|
||||
});
|
||||
|
||||
test("returns false when env hint value is empty string", () => {
|
||||
vi.stubEnv("PI_IS_SUBAGENT", "");
|
||||
expect(
|
||||
@@ -471,19 +453,6 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when registered session has a parentSessionId", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register(childSessionId, { parentSessionId: "parent-123" });
|
||||
expect(
|
||||
isSubagentExecutionContext(
|
||||
makeCtx(outsideDir, childSessionId),
|
||||
subagentRoot,
|
||||
posixPathFlavor,
|
||||
registry,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when registry is provided but session id is not registered", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
expect(
|
||||
@@ -508,22 +477,6 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("registry check takes priority over env var detection", () => {
|
||||
// Registry says registered; env var not set — should still return true.
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register(childSessionId, {});
|
||||
// Confirm no env var is set
|
||||
expect(process.env.PI_IS_SUBAGENT).toBeUndefined();
|
||||
expect(
|
||||
isSubagentExecutionContext(
|
||||
makeCtx(outsideDir, childSessionId),
|
||||
subagentRoot,
|
||||
posixPathFlavor,
|
||||
registry,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("unregistered session falls through to env var detection", () => {
|
||||
vi.stubEnv("PI_IS_SUBAGENT", "true");
|
||||
const registry = new SubagentSessionRegistry(); // empty — childSessionId not registered
|
||||
@@ -537,11 +490,4 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("no registry passed — existing behaviour unchanged", () => {
|
||||
// Ensure the parameter is truly optional (no registry arg)
|
||||
expect(
|
||||
isSubagentExecutionContext(makeCtx(null), subagentRoot, posixPathFlavor),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,100 +1,41 @@
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { SubagentDetectionContext } from "#src/authority/subagent-context";
|
||||
import { SubagentDetection } from "#src/authority/subagent-detection";
|
||||
import { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
||||
import { posixPathFlavor } from "#src/path/path-flavor";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function makeCtx(
|
||||
sessionDir: string | null,
|
||||
sessionId: string = "",
|
||||
): SubagentDetectionContext {
|
||||
function makeCtx(sessionId: string): SubagentDetectionContext {
|
||||
return {
|
||||
sessionManager: {
|
||||
getSessionDir: vi.fn(() => sessionDir ?? ""),
|
||||
getSessionId: vi.fn(() => sessionId),
|
||||
getSessionDir: () => "",
|
||||
getSessionId: () => sessionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const subagentSessionsDir = "/agent/subagent-sessions";
|
||||
|
||||
describe("SubagentDetection", () => {
|
||||
describe("isSubagent", () => {
|
||||
test("returns true for a registered in-process child (registry source)", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("child-1", {});
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry,
|
||||
});
|
||||
expect(detection.isSubagent(makeCtx(null, "child-1"))).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when a subagent env hint is set (env source)", () => {
|
||||
vi.stubEnv("PI_IS_SUBAGENT", "1");
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry: new SubagentSessionRegistry(),
|
||||
});
|
||||
expect(detection.isSubagent(makeCtx("/somewhere/else"))).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when the session dir is nested under subagentSessionsDir (filesystem source)", () => {
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry: new SubagentSessionRegistry(),
|
||||
});
|
||||
expect(
|
||||
detection.isSubagent(makeCtx(`${subagentSessionsDir}/child-1`)),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when no source matches", () => {
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry: new SubagentSessionRegistry(),
|
||||
});
|
||||
expect(detection.isSubagent(makeCtx("/projects/my-app"))).toBe(false);
|
||||
// The detection sources themselves are covered exhaustively in
|
||||
// subagent-context.test.ts; these two tests pin the only branches this
|
||||
// wrapper owns: registry consult and registry-absent fallback.
|
||||
describe("SubagentDetection.isRegisteredChild", () => {
|
||||
test("returns true when the session id is registered", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("child-1", {});
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry,
|
||||
});
|
||||
expect(detection.isRegisteredChild(makeCtx("child-1"))).toBe(true);
|
||||
});
|
||||
|
||||
describe("isRegisteredChild", () => {
|
||||
test("returns true when the session id is registered", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("child-1", {});
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry,
|
||||
});
|
||||
expect(detection.isRegisteredChild(makeCtx(null, "child-1"))).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when the session id is not registered", () => {
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
registry: new SubagentSessionRegistry(),
|
||||
});
|
||||
expect(detection.isRegisteredChild(makeCtx(null, "child-1"))).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when constructed without a registry", () => {
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
});
|
||||
expect(detection.isRegisteredChild(makeCtx(null, "child-1"))).toBe(false);
|
||||
test("returns false when constructed without a registry", () => {
|
||||
const detection = new SubagentDetection({
|
||||
subagentSessionsDir,
|
||||
flavor: posixPathFlavor,
|
||||
});
|
||||
expect(detection.isRegisteredChild(makeCtx("child-1"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,13 +98,6 @@ describe("subscribeSubagentLifecycle", () => {
|
||||
expect(handlers.has("subagents:child:disposed")).toBe(true);
|
||||
});
|
||||
|
||||
it("exposes the canonical channel-name strings", () => {
|
||||
expect(SUBAGENT_CHILD_SESSION_CREATED).toBe(
|
||||
"subagents:child:session-created",
|
||||
);
|
||||
expect(SUBAGENT_CHILD_DISPOSED).toBe("subagents:child:disposed");
|
||||
});
|
||||
|
||||
// ── #298 regression: concurrent siblings must be independent ──────────────
|
||||
|
||||
it("disposing one sibling does not evict the other (collision regression)", () => {
|
||||
|
||||
@@ -16,54 +16,16 @@ function makeInfo(
|
||||
}
|
||||
|
||||
describe("SubagentSessionRegistry", () => {
|
||||
test("has() returns false for an unregistered key", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
expect(registry.has("session-abc")).toBe(false);
|
||||
});
|
||||
|
||||
test("get() returns undefined for an unregistered key", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
expect(registry.get("session-abc")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("has() returns true after register()", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("session-abc", makeInfo());
|
||||
expect(registry.has("session-abc")).toBe(true);
|
||||
});
|
||||
|
||||
test("get() returns the registered info after register()", () => {
|
||||
test("register() stores and unregister() removes the registered info", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
const info = makeInfo({ parentSessionId: "parent-123" });
|
||||
registry.register("session-abc", info);
|
||||
expect(registry.get("session-abc")).toEqual(info);
|
||||
});
|
||||
|
||||
test("register() stores entry without parentSessionId", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("session-abc", makeInfo());
|
||||
expect(registry.get("session-abc")).toEqual({});
|
||||
});
|
||||
|
||||
test("has() returns false after unregister()", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("session-abc", makeInfo());
|
||||
registry.unregister("session-abc");
|
||||
expect(registry.has("session-abc")).toBe(false);
|
||||
});
|
||||
|
||||
test("get() returns undefined after unregister()", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("session-abc", makeInfo());
|
||||
registry.unregister("session-abc");
|
||||
expect(registry.get("session-abc")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("unregister() is a no-op for an unknown key", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
expect(() => registry.unregister("session-nonexistent")).not.toThrow();
|
||||
});
|
||||
|
||||
test("register() overwrites a previous entry for the same key", () => {
|
||||
const registry = new SubagentSessionRegistry();
|
||||
registry.register("session-abc", makeInfo({ parentSessionId: "parent-1" }));
|
||||
@@ -116,11 +78,6 @@ describe("getSubagentSessionRegistry (process-global accessor)", () => {
|
||||
delete (globalThis as Record<symbol, unknown>)[REGISTRY_KEY];
|
||||
});
|
||||
|
||||
test("returns a SubagentSessionRegistry instance", () => {
|
||||
const registry = getSubagentSessionRegistry();
|
||||
expect(registry).toBeInstanceOf(SubagentSessionRegistry);
|
||||
});
|
||||
|
||||
test("returns the same instance on repeated calls", () => {
|
||||
const first = getSubagentSessionRegistry();
|
||||
const second = getSubagentSessionRegistry();
|
||||
@@ -137,9 +94,4 @@ describe("getSubagentSessionRegistry (process-global accessor)", () => {
|
||||
expect(reader.has("child-session-xyz")).toBe(true);
|
||||
expect(reader.get("child-session-xyz")?.parentSessionId).toBe("parent-abc");
|
||||
});
|
||||
|
||||
test("starts empty on first call", () => {
|
||||
const registry = getSubagentSessionRegistry();
|
||||
expect(registry.has("any-session-id")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user