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>
189 lines
5.7 KiB
TypeScript
189 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
type CommandWord,
|
|
classifyWrapperWords,
|
|
executedUnitOf,
|
|
} from "#src/access-intent/bash/wrapper-analysis";
|
|
|
|
/**
|
|
* Split a command unit into words the way the AST walk does: whitespace
|
|
* separated, but a quoted span is one word carrying its quotes — tree-sitter
|
|
* emits a `string`/`raw_string` argument as a single named child.
|
|
*
|
|
* `sync-commands.test.ts` pins the real node adapter end to end (wrapperKind
|
|
* and executedUnit over a warm tree-sitter parse); this stands in for it so
|
|
* the extraction rules can be exercised without a parse.
|
|
*/
|
|
function words(unitText: string): CommandWord[] {
|
|
const out: CommandWord[] = [];
|
|
const pattern = /"[^"]*"|'[^']*'|\S+/g;
|
|
let match = pattern.exec(unitText);
|
|
while (match !== null) {
|
|
out.push({ text: match[0], offset: match.index });
|
|
match = pattern.exec(unitText);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe("classifyWrapperWords", () => {
|
|
describe("opaque payloads", () => {
|
|
it.each([
|
|
"eval rm",
|
|
"bash -c rm",
|
|
"sh -c rm",
|
|
"dash -c rm",
|
|
"zsh -c rm",
|
|
"ksh -c rm",
|
|
"bash -ec rm",
|
|
"bash -xc rm",
|
|
"/bin/bash -c rm",
|
|
])("flags %s", (unit) => {
|
|
expect(classifyWrapperWords(words(unit))).toBe("opaque-payload");
|
|
});
|
|
|
|
it("does not flag a shell running a script file", () => {
|
|
expect(classifyWrapperWords(words("bash script.sh"))).toBeUndefined();
|
|
});
|
|
|
|
it("does not flag a -c cluster after the end-of-options marker", () => {
|
|
expect(classifyWrapperWords(words("bash -- -c"))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("indirection wrappers", () => {
|
|
it.each([
|
|
"sudo aws s3 ls",
|
|
"env FOO=bar aws",
|
|
"xargs grep foo",
|
|
"timeout 10 grep foo",
|
|
"nice -n 5 make",
|
|
"doas ls",
|
|
"flock /tmp/lock ls",
|
|
])("flags %s", (unit) => {
|
|
expect(classifyWrapperWords(words(unit))).toBe("indirection");
|
|
});
|
|
|
|
it.each([
|
|
"find . -exec grep foo {} ;",
|
|
"find . -execdir rm {} ;",
|
|
"fd -x rm",
|
|
"fd --exec-batch rm",
|
|
])("flags the exec-conditional %s", (unit) => {
|
|
expect(classifyWrapperWords(words(unit))).toBe("indirection");
|
|
});
|
|
|
|
it("does not flag a bare search", () => {
|
|
expect(classifyWrapperWords(words("find . -name x"))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("ordinary commands", () => {
|
|
it.each([
|
|
"ls -la",
|
|
"grep -c foo file",
|
|
"git status",
|
|
])("does not flag %s", (unit) => {
|
|
expect(classifyWrapperWords(words(unit))).toBeUndefined();
|
|
});
|
|
|
|
it("does not flag an empty word list", () => {
|
|
expect(classifyWrapperWords([])).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("executedUnitOf", () => {
|
|
/** Extract from a unit spelled as plain whitespace-separated words. */
|
|
function executedUnit(unitText: string): string | null {
|
|
return executedUnitOf(unitText, words(unitText));
|
|
}
|
|
|
|
describe("opaque payloads", () => {
|
|
it.each([
|
|
['bash -c "rm -rf /"', "rm -rf /"],
|
|
["bash -c 'rm -rf /'", "rm -rf /"],
|
|
['sh -ec "make build"', "make build"],
|
|
['/bin/bash -c "ls"', "ls"],
|
|
['eval "rm x"', "rm x"],
|
|
])("names the inner program of %s", (unit, expected) => {
|
|
expect(executedUnit(unit)).toBe(expected);
|
|
});
|
|
|
|
it("returns null when the payload argument is missing", () => {
|
|
expect(executedUnit("bash -c")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("indirection wrappers", () => {
|
|
it.each([
|
|
["sudo aws s3 rm", "aws s3 rm"],
|
|
["sudo -u root aws s3 rm", "aws s3 rm"],
|
|
["sudo -- ls -la", "ls -la"],
|
|
["xargs grep foo", "grep foo"],
|
|
["xargs -0 -n1 grep foo", "grep foo"],
|
|
["xargs -I{} rm {}", "rm {}"],
|
|
["timeout 10 grep foo", "grep foo"],
|
|
["timeout -s KILL 10 grep foo", "grep foo"],
|
|
["nice -n 5 make build", "make build"],
|
|
["env FOO=bar grep foo", "grep foo"],
|
|
["flock /tmp/lock aws s3 ls", "aws s3 ls"],
|
|
["watch -n 2 ls", "ls"],
|
|
])("names the inner command of %s", (unit, expected) => {
|
|
expect(executedUnit(unit)).toBe(expected);
|
|
});
|
|
|
|
it("preserves the inner command's original spacing and quoting", () => {
|
|
expect(executedUnit("sudo grep 'a b' x")).toBe("grep 'a b' x");
|
|
});
|
|
|
|
it.each([
|
|
"xargs",
|
|
"sudo",
|
|
"sudo -u root",
|
|
"timeout 10",
|
|
])("returns null when %s names no inner command", (unit) => {
|
|
expect(executedUnit(unit)).toBeNull();
|
|
});
|
|
|
|
it("returns null rather than guessing past an unknown trailing option", () => {
|
|
expect(executedUnit("xargs --unknown-opt")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("exec-conditional wrappers", () => {
|
|
it.each([
|
|
["find . -name x -exec grep foo {} ;", "grep foo {}"],
|
|
["find . -exec rm {} +", "rm {}"],
|
|
["find . -execdir grep foo {} ;", "grep foo {}"],
|
|
["fd -x rm", "rm"],
|
|
["fd --exec-batch rm -f", "rm -f"],
|
|
])("names the per-result command of %s", (unit, expected) => {
|
|
expect(executedUnit(unit)).toBe(expected);
|
|
});
|
|
|
|
it("returns null when the exec flag ends the command", () => {
|
|
expect(executedUnit("find . -exec")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("nested wrappers", () => {
|
|
it.each([
|
|
["sudo timeout 5 xargs grep foo", "grep foo"],
|
|
["sudo bash -c 'rm x'", "rm x"],
|
|
["timeout 10 sudo -u root aws s3 rm", "aws s3 rm"],
|
|
])("unwraps %s to its innermost command", (unit, expected) => {
|
|
expect(executedUnit(unit)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("nothing to add", () => {
|
|
it("returns null for an ordinary command", () => {
|
|
expect(executedUnit("grep foo")).toBeNull();
|
|
});
|
|
|
|
it("returns null for an empty word list", () => {
|
|
expect(executedUnitOf("", [])).toBeNull();
|
|
});
|
|
});
|
|
});
|