mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
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>
494 lines
14 KiB
TypeScript
494 lines
14 KiB
TypeScript
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import {
|
|
isRegisteredSubagentChild,
|
|
isSubagentExecutionContext,
|
|
normalizeFilesystemPath,
|
|
type SubagentDetectionContext,
|
|
} from "#src/authority/subagent-context";
|
|
import { SubagentSessionRegistry } from "#src/authority/subagent-registry";
|
|
import { posixPathFlavor, win32PathFlavor } from "#src/path/path-flavor";
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function makeCtx(
|
|
sessionDir: string | null,
|
|
sessionId: string = "",
|
|
): SubagentDetectionContext {
|
|
return {
|
|
sessionManager: {
|
|
getSessionDir: vi.fn(() => sessionDir ?? ""),
|
|
getSessionId: vi.fn(() => sessionId),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("isRegisteredSubagentChild", () => {
|
|
const childSessionId = "child-session-abc";
|
|
|
|
test("returns true when the session id is registered", () => {
|
|
const registry = new SubagentSessionRegistry();
|
|
registry.register(childSessionId, {});
|
|
expect(
|
|
isRegisteredSubagentChild(makeCtx(null, childSessionId), registry),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns false when the session id is not registered", () => {
|
|
const registry = new SubagentSessionRegistry();
|
|
expect(
|
|
isRegisteredSubagentChild(makeCtx(null, childSessionId), registry),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when the session id is empty", () => {
|
|
const registry = new SubagentSessionRegistry();
|
|
registry.register("", {});
|
|
expect(isRegisteredSubagentChild(makeCtx(null, ""), registry)).toBe(false);
|
|
});
|
|
|
|
test("returns false when getSessionId throws", () => {
|
|
const registry = new SubagentSessionRegistry();
|
|
registry.register(childSessionId, {});
|
|
const ctx: SubagentDetectionContext = {
|
|
sessionManager: {
|
|
getSessionDir: vi.fn(() => ""),
|
|
getSessionId: vi.fn(() => {
|
|
throw new Error("session id unavailable");
|
|
}),
|
|
},
|
|
};
|
|
expect(isRegisteredSubagentChild(ctx, registry)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("normalizeFilesystemPath", () => {
|
|
test("normalizes a simple absolute path", () => {
|
|
expect(normalizeFilesystemPath("/projects/my-app", posixPathFlavor)).toBe(
|
|
"/projects/my-app",
|
|
);
|
|
});
|
|
|
|
test("collapses redundant separators", () => {
|
|
expect(normalizeFilesystemPath("/projects//my-app", posixPathFlavor)).toBe(
|
|
"/projects/my-app",
|
|
);
|
|
});
|
|
|
|
test("resolves . and .. segments", () => {
|
|
expect(
|
|
normalizeFilesystemPath("/projects/my-app/../other", posixPathFlavor),
|
|
).toBe("/projects/other");
|
|
});
|
|
|
|
test("win32: lowercases and normalizes with win32 separators", () => {
|
|
expect(
|
|
normalizeFilesystemPath("C:\\Projects\\My-App", win32PathFlavor),
|
|
).toBe("c:\\projects\\my-app");
|
|
});
|
|
|
|
test("posix: leaves case untouched", () => {
|
|
expect(normalizeFilesystemPath("/Projects/My-App", posixPathFlavor)).toBe(
|
|
"/Projects/My-App",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("isSubagentExecutionContext — injected platform (#510)", () => {
|
|
test("win32: detects a subagent session dir case-insensitively", () => {
|
|
const subagentRoot = "C:\\Sessions\\Subagents";
|
|
const sessionDir = "c:\\sessions\\subagents\\child";
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
win32PathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("posix: the same mixed-case dir is not a subagent context", () => {
|
|
const subagentRoot = "/Sessions/Subagents";
|
|
const sessionDir = "/sessions/subagents/child";
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isSubagentExecutionContext — env hint detection", () => {
|
|
test("returns true when PI_IS_SUBAGENT is set", () => {
|
|
vi.stubEnv("PI_IS_SUBAGENT", "true");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_SESSION_ID is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_SESSION_ID", "abc123");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_AGENT_ROUTER_SUBAGENT is set", () => {
|
|
vi.stubEnv("PI_AGENT_ROUTER_SUBAGENT", "1");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
// nicobailon/pi-subagents keys
|
|
test("returns true when PI_SUBAGENT_CHILD is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_CHILD", "1");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_RUN_ID is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_RUN_ID", "run-abc");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_CHILD_AGENT is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_CHILD_AGENT", "worker");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_DEPTH is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_DEPTH", "1");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_DEPTH is zero (depth-0 is still a subagent context)", () => {
|
|
vi.stubEnv("PI_SUBAGENT_DEPTH", "0");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
// HazAT/pi-interactive-subagents keys
|
|
test("returns true when PI_SUBAGENT_NAME is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_NAME", "my-agent");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_ID is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_ID", "id-xyz");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_SESSION is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_SESSION", "session-xyz");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when PI_SUBAGENT_ACTIVITY_FILE is set", () => {
|
|
vi.stubEnv("PI_SUBAGENT_ACTIVITY_FILE", "/tmp/activity.json");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns false when env hint value is empty string", () => {
|
|
vi.stubEnv("PI_IS_SUBAGENT", "");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when env hint value is whitespace only", () => {
|
|
vi.stubEnv("PI_IS_SUBAGENT", " ");
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null),
|
|
"/sessions/subagents",
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isSubagentExecutionContext — session dir detection", () => {
|
|
const subagentRoot = "/home/user/.pi/agent/sessions/subagents";
|
|
|
|
test("returns true when session dir is within subagent root", () => {
|
|
const sessionDir = `${subagentRoot}/session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when session dir equals subagent root", () => {
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(subagentRoot),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns false when session dir is outside subagent root", () => {
|
|
const sessionDir = "/home/user/.pi/agent/sessions/main-session";
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when session dir is a sibling with shared prefix", () => {
|
|
// "/sessions/subagents-extra" should not match root "/sessions/subagents"
|
|
const sessionDir = `${subagentRoot}-extra/session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when a `..` segment escapes the subagent root", () => {
|
|
// Normalizes to /home/user/.pi/agent/sessions/evil/session-abc — outside.
|
|
const sessionDir = `${subagentRoot}/../evil/session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns true when a `..` segment resolves back inside the root", () => {
|
|
// Normalizes to /home/user/.pi/agent/sessions/subagents/session-abc — inside.
|
|
const sessionDir = `${subagentRoot}/nested/../session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns false when session dir is under a different root", () => {
|
|
const sessionDir = "/var/other/subagents/session-abc";
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when getSessionDir returns null", () => {
|
|
expect(
|
|
isSubagentExecutionContext(makeCtx(null), subagentRoot, posixPathFlavor),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when getSessionDir returns empty string", () => {
|
|
expect(
|
|
isSubagentExecutionContext(makeCtx(""), subagentRoot, posixPathFlavor),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isSubagentExecutionContext — session dir detection (win32 flavor)", () => {
|
|
const subagentRoot = "C:\\Users\\dev\\.pi\\agent\\sessions\\subagents";
|
|
|
|
test("returns true when session dir is within subagent root", () => {
|
|
const sessionDir = `${subagentRoot}\\session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
win32PathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns true when session dir equals subagent root (case-insensitive)", () => {
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(subagentRoot.toUpperCase()),
|
|
subagentRoot,
|
|
win32PathFlavor,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("returns false when session dir is a sibling with shared prefix", () => {
|
|
const sessionDir = `${subagentRoot}-extra\\session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
win32PathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when a `..` segment escapes the subagent root", () => {
|
|
const sessionDir = `${subagentRoot}\\..\\evil\\session-abc`;
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
win32PathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when session dir is on a different drive", () => {
|
|
const sessionDir =
|
|
"D:\\Users\\dev\\.pi\\agent\\sessions\\subagents\\session-abc";
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(sessionDir),
|
|
subagentRoot,
|
|
win32PathFlavor,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isSubagentExecutionContext — registry detection", () => {
|
|
const subagentRoot = "/home/user/.pi/agent/sessions/subagents";
|
|
const outsideDir =
|
|
"/home/user/projects/my-app/.pi/agent/sessions/parent/tasks";
|
|
const childSessionId = "child-session-abc";
|
|
|
|
test("returns true when session id is registered (no env vars, dir outside filesystem root)", () => {
|
|
const registry = new SubagentSessionRegistry();
|
|
registry.register(childSessionId, {});
|
|
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(
|
|
isSubagentExecutionContext(
|
|
makeCtx(outsideDir, childSessionId),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
registry,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("returns false when session id is empty and registry has no matching entry", () => {
|
|
const registry = new SubagentSessionRegistry();
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(null, ""),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
registry,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("unregistered session falls through to env var detection", () => {
|
|
vi.stubEnv("PI_IS_SUBAGENT", "true");
|
|
const registry = new SubagentSessionRegistry(); // empty — childSessionId not registered
|
|
// Env var present → still true even without registry entry
|
|
expect(
|
|
isSubagentExecutionContext(
|
|
makeCtx(outsideDir, childSessionId),
|
|
subagentRoot,
|
|
posixPathFlavor,
|
|
registry,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|