progmune-runtime 3.3.8 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 12: Agent Loop Controller 测试
4
+ *
5
+ * 覆盖 M1 验收要点:
6
+ * - 首次尝试即过全部验证门 → 成功出口,retries=0
7
+ * - 执行失败 → 反馈注入 → 重试成功(验证 feedback 注入到下一次意图)
8
+ * - 写盘后验证门失败(编译不过)→ 触发重试而非误报成功
9
+ * - 全部重试耗尽 → 失败出口,attempts = iterations × retries,审计轨迹完整
10
+ *
11
+ * 不触碰真实文件系统 / LLM / git —— 全部依赖 mock(仓库测试惯例)。
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const vitest_1 = require("vitest");
15
+ const execute_1 = require("./execute");
16
+ const goal_planner_1 = require("./goal-planner");
17
+ const child_process_1 = require("child_process");
18
+ const agent_perception_1 = require("./agent-perception");
19
+ const agent_supervision_1 = require("./agent-supervision");
20
+ const agent_loop_1 = require("./agent-loop");
21
+ vitest_1.vi.mock("./execute", () => ({
22
+ execute: vitest_1.vi.fn(),
23
+ verifyCompiles: vitest_1.vi.fn(),
24
+ verifyFileMarker: vitest_1.vi.fn(),
25
+ }));
26
+ vitest_1.vi.mock("./goal-planner", () => ({
27
+ expandGoalActions: vitest_1.vi.fn(),
28
+ }));
29
+ vitest_1.vi.mock("child_process", () => ({
30
+ execSync: vitest_1.vi.fn(),
31
+ }));
32
+ vitest_1.vi.mock("./agent-perception", () => ({
33
+ collectGitContext: vitest_1.vi.fn(),
34
+ extractIRWithDelta: vitest_1.vi.fn(),
35
+ }));
36
+ vitest_1.vi.mock("./agent-supervision", () => ({
37
+ runProjectTests: vitest_1.vi.fn(),
38
+ }));
39
+ const mockExecute = vitest_1.vi.mocked(execute_1.execute);
40
+ const mockVerifyCompiles = vitest_1.vi.mocked(execute_1.verifyCompiles);
41
+ const mockVerifyFileMarker = vitest_1.vi.mocked(execute_1.verifyFileMarker);
42
+ const mockExpandGoals = vitest_1.vi.mocked(goal_planner_1.expandGoalActions);
43
+ const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
44
+ const mockCollectGitContext = vitest_1.vi.mocked(agent_perception_1.collectGitContext);
45
+ const mockExtractIRWithDelta = vitest_1.vi.mocked(agent_perception_1.extractIRWithDelta);
46
+ const mockRunProjectTests = vitest_1.vi.mocked(agent_supervision_1.runProjectTests);
47
+ /** 构造一个成功的 ExecuteResult */
48
+ function okResult(overrides = {}) {
49
+ return {
50
+ success: true,
51
+ code: "/**\n * @progmune-generated session=sess_test timestamp=2026-08-21T00:00:00.000Z\n */\nexport function f() { return 1; }",
52
+ sessionId: "sess_test",
53
+ hash: "fp1234567890abcd",
54
+ ruleHash: "rulehash123",
55
+ irFunctionCount: 3,
56
+ protocolRuleCount: 10,
57
+ violations: 0,
58
+ degraded: false,
59
+ repairApplied: false,
60
+ repairCount: 0,
61
+ repairBranchIds: [],
62
+ ...overrides,
63
+ };
64
+ }
65
+ /** 构造一个失败的 ExecuteResult */
66
+ function failResult(error) {
67
+ return {
68
+ success: false,
69
+ code: "",
70
+ sessionId: "",
71
+ hash: "",
72
+ ruleHash: "",
73
+ irFunctionCount: 0,
74
+ protocolRuleCount: 0,
75
+ violations: 0,
76
+ degraded: false,
77
+ repairApplied: false,
78
+ repairCount: 0,
79
+ repairBranchIds: [],
80
+ error,
81
+ };
82
+ }
83
+ (0, vitest_1.beforeEach)(() => {
84
+ vitest_1.vi.clearAllMocks();
85
+ mockExpandGoals.mockReturnValue([]);
86
+ mockVerifyCompiles.mockReturnValue({ pass: true, errors: [] });
87
+ mockVerifyFileMarker.mockReturnValue({ marked: true });
88
+ mockExecSync.mockReturnValue("mock diff");
89
+ mockExtractIRWithDelta.mockReturnValue({
90
+ ir: [{ name: "verify_password" }, { name: "main" }],
91
+ delta: { added: [], removed: [], functionCount: 2 },
92
+ });
93
+ mockRunProjectTests.mockReturnValue({ ran: false, pass: true, failures: [], command: "(无测试脚本)" });
94
+ mockCollectGitContext.mockReturnValue({
95
+ available: true,
96
+ branch: "main",
97
+ recentCommits: ["abc123 feat: login"],
98
+ changedFiles: ["src/auth.ts"],
99
+ sourceFiles: ["src/auth.ts"],
100
+ });
101
+ });
102
+ (0, vitest_1.describe)("agent-loop", () => {
103
+ (0, vitest_1.it)("首次尝试通过全部验证门 → 成功出口,retries=0,审计轨迹完整", async () => {
104
+ mockExecute.mockResolvedValue(okResult());
105
+ const r = await (0, agent_loop_1.runAgentLoop)({
106
+ projectPath: "/tmp/fake-project",
107
+ intent: "实现验证函数",
108
+ filePath: "out.ts",
109
+ });
110
+ (0, vitest_1.expect)(r.success).toBe(true);
111
+ (0, vitest_1.expect)(r.retries).toBe(0);
112
+ (0, vitest_1.expect)(r.iterations).toBe(1);
113
+ (0, vitest_1.expect)(r.attempts).toHaveLength(1);
114
+ (0, vitest_1.expect)(r.fingerprint).toBe("fp1234567890abcd");
115
+ (0, vitest_1.expect)(r.filePath).toBe("out.ts");
116
+ const events = r.auditTrail.map((e) => e.event);
117
+ (0, vitest_1.expect)(events).toContain("loop:start");
118
+ (0, vitest_1.expect)(events).toContain("attempt:start");
119
+ (0, vitest_1.expect)(events).toContain("attempt:ok");
120
+ (0, vitest_1.expect)(events).toContain("loop:success");
121
+ (0, vitest_1.expect)(r.auditTrail.every((e) => !!e.timestamp)).toBe(true);
122
+ });
123
+ (0, vitest_1.it)("执行失败 → 反馈注入下一次意图 → 重试成功", async () => {
124
+ mockExecute
125
+ .mockResolvedValueOnce(failResult("Planning failed: LLM 超时"))
126
+ .mockResolvedValueOnce(okResult());
127
+ const r = await (0, agent_loop_1.runAgentLoop)({
128
+ projectPath: "/tmp/fake-project",
129
+ intent: "实现支付函数",
130
+ filePath: "pay.ts",
131
+ });
132
+ (0, vitest_1.expect)(r.success).toBe(true);
133
+ (0, vitest_1.expect)(r.retries).toBe(1);
134
+ (0, vitest_1.expect)(r.attempts).toHaveLength(2);
135
+ // 第二次尝试的意图必须包含注入的失败反馈
136
+ (0, vitest_1.expect)(r.attempts[1].intent).toContain("[上一次尝试失败");
137
+ (0, vitest_1.expect)(r.attempts[1].intent).toContain("Planning failed: LLM 超时");
138
+ (0, vitest_1.expect)(mockExecute).toHaveBeenCalledTimes(2);
139
+ });
140
+ (0, vitest_1.it)("写盘后验证门失败(编译不过)→ 触发重试而非误报成功", async () => {
141
+ mockExecute.mockResolvedValue(okResult());
142
+ mockVerifyCompiles
143
+ .mockReturnValueOnce({ pass: false, errors: ["out.ts:1 error TS1005"] })
144
+ .mockReturnValue({ pass: true, errors: [] });
145
+ const r = await (0, agent_loop_1.runAgentLoop)({
146
+ projectPath: "/tmp/fake-project",
147
+ intent: "实现工具函数",
148
+ filePath: "util.ts",
149
+ });
150
+ (0, vitest_1.expect)(r.success).toBe(true);
151
+ (0, vitest_1.expect)(r.attempts).toHaveLength(2);
152
+ (0, vitest_1.expect)(r.attempts[0].success).toBe(false); // 编译门失败 → 该次尝试不算成功
153
+ (0, vitest_1.expect)(r.attempts[0].compilePass).toBe(false);
154
+ (0, vitest_1.expect)(r.attempts[1].intent).toContain("编译验证未通过");
155
+ });
156
+ (0, vitest_1.it)("全部重试耗尽 → 失败出口,attempts = iterations × retries,反馈不静默", async () => {
157
+ mockExecute.mockResolvedValue(failResult("总是失败"));
158
+ const r = await (0, agent_loop_1.runAgentLoop)({
159
+ projectPath: "/tmp/fake-project",
160
+ intent: "实现不可能的函数",
161
+ filePath: "x.ts",
162
+ maxIterations: 2,
163
+ maxRetries: 3,
164
+ });
165
+ (0, vitest_1.expect)(r.success).toBe(false);
166
+ (0, vitest_1.expect)(r.attempts).toHaveLength(6);
167
+ (0, vitest_1.expect)(r.iterations).toBe(2);
168
+ (0, vitest_1.expect)(r.retries).toBe(6);
169
+ // 每轮重试都注入反馈,耗尽后最后一轮结束事件在
170
+ const events = r.auditTrail.map((e) => e.event);
171
+ (0, vitest_1.expect)(events).toContain("loop:exhausted");
172
+ (0, vitest_1.expect)(events.filter((e) => e === "retry")).toHaveLength(4); // 2 轮 × 每轮 2 次注入
173
+ (0, vitest_1.expect)(events.filter((e) => e === "iteration:end")).toHaveLength(2);
174
+ });
175
+ (0, vitest_1.it)("execute 抛异常 → 兜底为失败结果并继续重试", async () => {
176
+ mockExecute
177
+ .mockRejectedValueOnce(new Error("crash"))
178
+ .mockResolvedValue(okResult());
179
+ const r = await (0, agent_loop_1.runAgentLoop)({
180
+ projectPath: "/tmp/fake-project",
181
+ intent: "实现函数",
182
+ filePath: "f.ts",
183
+ });
184
+ (0, vitest_1.expect)(r.success).toBe(true);
185
+ (0, vitest_1.expect)(r.attempts[0].error).toContain("execute 抛出异常: crash");
186
+ (0, vitest_1.expect)(r.attempts[1].intent).toContain("execute 抛出异常");
187
+ });
188
+ (0, vitest_1.it)("未指定 filePath 时成功出口 diff 为空占位", async () => {
189
+ mockExecute.mockResolvedValue(okResult());
190
+ const r = await (0, agent_loop_1.runAgentLoop)({
191
+ projectPath: "/tmp/fake-project",
192
+ intent: "生成代码片段",
193
+ });
194
+ (0, vitest_1.expect)(r.success).toBe(true);
195
+ (0, vitest_1.expect)(r.diff).toBe("(未指定输出文件,无 diff)");
196
+ (0, vitest_1.expect)(mockExecSync).not.toHaveBeenCalled();
197
+ });
198
+ (0, vitest_1.it)("P3 测试门:项目测试失败 → 注入测试反馈重试,测试通过后成功", async () => {
199
+ mockExecute.mockResolvedValue(okResult());
200
+ mockRunProjectTests
201
+ .mockReturnValueOnce({
202
+ ran: true, pass: false, failures: ["FAIL login.test.ts", "AssertionError: token 无效"], command: "npm test",
203
+ })
204
+ .mockReturnValue({ ran: true, pass: true, failures: [], command: "npm test" });
205
+ const r = await (0, agent_loop_1.runAgentLoop)({
206
+ projectPath: "/tmp/fake-project",
207
+ intent: "实现登录流程",
208
+ filePath: "login.ts",
209
+ runTests: true,
210
+ });
211
+ (0, vitest_1.expect)(r.success).toBe(true);
212
+ (0, vitest_1.expect)(r.attempts).toHaveLength(2);
213
+ (0, vitest_1.expect)(r.attempts[0].success).toBe(false);
214
+ (0, vitest_1.expect)(r.attempts[0].testRan).toBe(true);
215
+ (0, vitest_1.expect)(r.attempts[0].testPass).toBe(false);
216
+ (0, vitest_1.expect)(r.attempts[1].intent).toContain("项目测试失败");
217
+ (0, vitest_1.expect)(r.attempts[1].intent).toContain("token 无效");
218
+ (0, vitest_1.expect)(r.attempts[1].testPass).toBe(true);
219
+ (0, vitest_1.expect)(mockRunProjectTests).toHaveBeenCalledTimes(2);
220
+ });
221
+ (0, vitest_1.it)("P2 上下文:includeContext 时意图注入 git 上下文,审计含 perception 事件", async () => {
222
+ mockExecute.mockResolvedValue(okResult());
223
+ const r = await (0, agent_loop_1.runAgentLoop)({
224
+ projectPath: "/tmp/fake-project",
225
+ intent: "实现登录流程",
226
+ filePath: "login.ts",
227
+ includeContext: true,
228
+ });
229
+ (0, vitest_1.expect)(r.success).toBe(true);
230
+ (0, vitest_1.expect)(r.attempts[0].intent).toContain("[项目上下文");
231
+ (0, vitest_1.expect)(r.attempts[0].intent).toContain("main");
232
+ const events = r.auditTrail.map((e) => e.event);
233
+ (0, vitest_1.expect)(events).toContain("perception:git");
234
+ (0, vitest_1.expect)(events).toContain("perception:ir");
235
+ (0, vitest_1.expect)(r.gitContext?.branch).toBe("main");
236
+ (0, vitest_1.expect)(r.irDelta).toBeDefined();
237
+ });
238
+ (0, vitest_1.it)("P2 感知:IR 增量写入审计轨迹并返回", async () => {
239
+ mockExecute.mockResolvedValue(okResult());
240
+ mockExtractIRWithDelta
241
+ .mockReturnValueOnce({ ir: [{ name: "verify_password" }], delta: { added: [], removed: [], functionCount: 1 } })
242
+ .mockReturnValueOnce({
243
+ ir: [{ name: "verify_password" }, { name: "main" }],
244
+ delta: { added: ["main"], removed: [], functionCount: 2 },
245
+ });
246
+ const r = await (0, agent_loop_1.runAgentLoop)({
247
+ projectPath: "/tmp/fake-project",
248
+ intent: "实现登录流程",
249
+ filePath: "login.ts",
250
+ });
251
+ (0, vitest_1.expect)(r.success).toBe(true);
252
+ (0, vitest_1.expect)(r.irDelta?.added).toContain("main");
253
+ const events = r.auditTrail.map((e) => e.event);
254
+ (0, vitest_1.expect)(events.filter((e) => e === "perception:ir")).toHaveLength(2);
255
+ });
256
+ });
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 12: Agent 免疫巡逻 (P4) —— 形态 B 第一版
4
+ *
5
+ * 监听/扫描项目 → trust_check(Trust Engine 全量评估)→ 违规报告 + 建议补丁。
6
+ *
7
+ * 修复信任悖论(设计文档铁律):
8
+ * 100% 检测精度 ≠ 修复正确率。巡逻第一形态**只报告 + 建议补丁,
9
+ * 绝不自动合并**(autoApplied 恒为 false)——修错比不修更糟,
10
+ * 第一份错误修复会摧毁"免疫巡逻"信任。
11
+ *
12
+ * 感知职责:每次扫描前强制重提 IR(IR_STALE 的消费方),保证验证的是新世界。
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.runPatrol = runPatrol;
49
+ exports.formatPatrolTerminal = formatPatrolTerminal;
50
+ exports.formatPatrolMarkdown = formatPatrolMarkdown;
51
+ exports.writePatrolReport = writePatrolReport;
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ const engine_1 = require("./trust/engine");
55
+ const agent_perception_1 = require("./agent-perception");
56
+ const extract_ir_1 = require("./extract-ir");
57
+ // ── Main ──
58
+ /**
59
+ * 运行一次免疫巡逻:重提 IR → evaluateTrust → 映射违规(含 fixPath)→ 报告。
60
+ */
61
+ async function runPatrol(projectPath) {
62
+ const abs = path.resolve(projectPath);
63
+ const git = (0, agent_perception_1.collectGitContext)(abs);
64
+ // 感知:扫描前强制重提 IR(IR_STALE 消费方)——验证的是新世界
65
+ try {
66
+ const ir = (0, extract_ir_1.extractIR)(abs);
67
+ fs.writeFileSync(path.join(abs, "ir.json"), JSON.stringify(ir, null, 2));
68
+ }
69
+ catch { /* IR 提取失败不阻塞 —— trust 引擎会降级 */ }
70
+ const decision = await (0, engine_1.evaluateTrust)({
71
+ projectPath: abs,
72
+ projectName: path.basename(abs),
73
+ commit: git.available ? (git.recentCommits[0]?.split(" ")[0] || "unknown") : "unknown",
74
+ branch: git.available ? git.branch : undefined,
75
+ });
76
+ // fixPath 来自引擎的 violationTraces(BFS 修复路径)
77
+ const traces = new Map((decision.violationTraces || []).map((t) => [`${t.rule_id}|${t.file}|${t.function}`, t]));
78
+ const findings = decision.violations.map((v) => {
79
+ const trace = traces.get(`${v.rule_id}|${v.file}|${v.function}`);
80
+ return {
81
+ rule_id: v.rule_id,
82
+ severity: v.severity,
83
+ file: v.file,
84
+ function: v.function,
85
+ message: v.message,
86
+ fixPath: trace?.fixPath || [],
87
+ fix: v.fix,
88
+ evidence: v.evidence,
89
+ reasoningSteps: (trace?.steps || []).map((s) => `[${s.label}] ${s.action} → ${s.explanation}`),
90
+ };
91
+ });
92
+ const auditTrail = [
93
+ {
94
+ timestamp: new Date().toISOString(),
95
+ event: "patrol:scan",
96
+ detail: `decision=${decision.overall.decision} score=${decision.overall.score} ` +
97
+ `violations=${decision.violations.length}`,
98
+ },
99
+ {
100
+ timestamp: decision.timestamp,
101
+ event: "patrol:trust-audit",
102
+ detail: `checkId=${decision.auditTrail.checkId} engine=${decision.engineVersion} ` +
103
+ `reproducible=${decision.auditTrail.reproducible}`,
104
+ },
105
+ ];
106
+ return {
107
+ scannedAt: new Date().toISOString(),
108
+ project: decision.project,
109
+ branch: git.available ? git.branch : "",
110
+ commit: decision.commit,
111
+ decision: decision.overall.decision,
112
+ score: decision.overall.score,
113
+ confidence: decision.overall.confidence,
114
+ findings,
115
+ summary: {
116
+ total: decision.summary.total,
117
+ critical: decision.summary.critical,
118
+ high: decision.summary.high,
119
+ medium: decision.summary.medium,
120
+ low: decision.summary.low,
121
+ },
122
+ auditTrail,
123
+ engineVersion: decision.engineVersion,
124
+ checkId: decision.auditTrail.checkId,
125
+ autoApplied: false,
126
+ changedFiles: git.changedFiles,
127
+ };
128
+ }
129
+ // ── Formatters ──
130
+ /** 终端一行摘要。 */
131
+ function formatPatrolTerminal(r) {
132
+ const icon = r.decision === "APPROVED" ? "✅" : r.decision === "BLOCKED" ? "🛑" : "⚠️";
133
+ const lines = [
134
+ `${icon} 免疫巡逻: ${r.project} — ${r.decision} (score=${r.score}, confidence=${r.confidence})`,
135
+ ` 违规: ${r.summary.total} (critical=${r.summary.critical} high=${r.summary.high} ` +
136
+ `medium=${r.summary.medium} low=${r.summary.low}) | 自动合并: 永不(只报告+建议)`,
137
+ ];
138
+ for (const f of r.findings.slice(0, 10)) {
139
+ lines.push(` [${f.severity}] ${f.rule_id} @ ${f.file}${f.function ? `::${f.function}` : ""}` +
140
+ (f.fixPath.length > 0 ? ` → 建议补丁: ${f.fixPath.join(" → ")}` : ""));
141
+ }
142
+ if (r.findings.length > 10)
143
+ lines.push(` … 其余 ${r.findings.length - 10} 项见报告`);
144
+ return lines.join("\n");
145
+ }
146
+ /** 完整 Markdown 巡逻报告(含证据链回放,可入审计档案)。 */
147
+ function formatPatrolMarkdown(r) {
148
+ const lines = [
149
+ `# 🛡️ Progmune 免疫巡逻报告`,
150
+ ``,
151
+ `- 扫描时间: ${r.scannedAt}`,
152
+ `- 项目: ${r.project}${r.branch ? `(分支 ${r.branch})` : ""}`,
153
+ `- 提交: ${r.commit}`,
154
+ `- 决策: **${r.decision}** | 分数: ${r.score} | 置信度: ${r.confidence}`,
155
+ `- 引擎: ${r.engineVersion} | checkId: ${r.checkId}`,
156
+ `- **自动合并: 永不**(只报告 + 建议补丁,修复需人工审批)`,
157
+ ``,
158
+ `## 违规摘要`,
159
+ ``,
160
+ `| 严重级 | 数量 |`,
161
+ `|---|---|`,
162
+ `| critical | ${r.summary.critical} |`,
163
+ `| high | ${r.summary.high} |`,
164
+ `| medium | ${r.summary.medium} |`,
165
+ `| low | ${r.summary.low} |`,
166
+ `| **合计** | **${r.summary.total}** |`,
167
+ ``,
168
+ ];
169
+ if (r.findings.length === 0) {
170
+ lines.push(`✅ 未发现违规。`, ``);
171
+ }
172
+ else {
173
+ lines.push(`## 违规明细与建议补丁`, ``);
174
+ r.findings.forEach((f, i) => {
175
+ lines.push(`### ${i + 1}. [${f.severity}] ${f.rule_id} — ${f.file}${f.function ? `::${f.function}` : ""}`, ``, `- **问题**: ${f.message}`, `- **修复建议**: ${f.fix}`);
176
+ if (f.fixPath.length > 0) {
177
+ lines.push(`- **建议补丁路径**(不自动应用): \`${f.fixPath.join(" → ")}\``);
178
+ }
179
+ if (f.reasoningSteps.length > 0) {
180
+ lines.push(``, `**推理回放**:`, ``);
181
+ f.reasoningSteps.forEach((s) => lines.push(` - ${s}`));
182
+ }
183
+ lines.push(``);
184
+ });
185
+ }
186
+ lines.push(`## 证据链(可回放)`, ``);
187
+ r.auditTrail.forEach((e) => lines.push(`- \`${e.timestamp}\` **${e.event}**: ${e.detail}`));
188
+ if (r.changedFiles.length > 0) {
189
+ lines.push(``, `## 扫描时变更文件`, ``);
190
+ r.changedFiles.forEach((f) => lines.push(`- ${f}`));
191
+ }
192
+ lines.push(``, `---`, `*由 Progmune 免疫巡逻生成。修复责任: 人工审批后应用。*`, ``);
193
+ return lines.join("\n");
194
+ }
195
+ /** 写入巡逻报告到项目目录,返回文件路径。 */
196
+ function writePatrolReport(r, projectPath) {
197
+ const reportPath = path.join(path.resolve(projectPath), ".progmune_patrol_report.md");
198
+ fs.writeFileSync(reportPath, formatPatrolMarkdown(r), "utf-8");
199
+ return reportPath;
200
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 12: 免疫巡逻测试 (P4)
4
+ *
5
+ * evaluateTrust / extractIR / git 全部 mock —— 验证:
6
+ * - 违规 → 报告映射(fixPath 来自 violationTraces)
7
+ * - autoApplied 恒为 false(修复信任悖论)
8
+ * - Markdown 报告含建议补丁 + 证据链
9
+ * - 报告落盘
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const vitest_1 = require("vitest");
13
+ const engine_1 = require("./trust/engine");
14
+ const extract_ir_1 = require("./extract-ir");
15
+ const child_process_1 = require("child_process");
16
+ const agent_patrol_1 = require("./agent-patrol");
17
+ vitest_1.vi.mock("./trust/engine", () => ({
18
+ evaluateTrust: vitest_1.vi.fn(),
19
+ }));
20
+ vitest_1.vi.mock("./extract-ir", () => ({
21
+ extractIR: vitest_1.vi.fn(),
22
+ }));
23
+ vitest_1.vi.mock("child_process", () => ({
24
+ execSync: vitest_1.vi.fn(),
25
+ }));
26
+ const mockEvaluateTrust = vitest_1.vi.mocked(engine_1.evaluateTrust);
27
+ const mockExtractIR = vitest_1.vi.mocked(extract_ir_1.extractIR);
28
+ const mockExecSync = vitest_1.vi.mocked(child_process_1.execSync);
29
+ /** 构造受控的 TrustDecision */
30
+ function trustDecision(overrides = {}) {
31
+ return {
32
+ project: "demo-patrol",
33
+ commit: "abc123",
34
+ timestamp: "2026-08-21T08:00:00.000Z",
35
+ engineVersion: "trust-runtime-v1.0.0",
36
+ overall: { score: 41, decision: "BLOCKED", confidence: "HIGH" },
37
+ dimensions: {},
38
+ violations: [
39
+ {
40
+ severity: "high",
41
+ rule_id: "SSG_PROTOCOL",
42
+ file: "bad_flow.ts",
43
+ function: "bad_flow",
44
+ message: 'SSG state violation: "generate_jwt" requires states [PASSWORD_VERIFIED]',
45
+ evidence: "调用序列 [generate_jwt] 违反协议",
46
+ why: "缺少密码验证前置",
47
+ fix: "在 generate_jwt 前调用 verify_password",
48
+ policy_ref: "REF-SSG-001",
49
+ },
50
+ ],
51
+ violationTraces: [
52
+ {
53
+ rule_id: "SSG_PROTOCOL",
54
+ file: "bad_flow.ts",
55
+ function: "bad_flow",
56
+ steps: [
57
+ { step: 1, label: "状态", action: "generate_jwt", preState: "UNAUTHENTICATED", explanation: "前置 PASSWORD_VERIFIED 缺失" },
58
+ ],
59
+ fixPath: ["verify_password"],
60
+ estimatedReadingTimeMinutes: 1,
61
+ },
62
+ ],
63
+ summary: { critical: 0, high: 1, medium: 0, low: 0, total: 1 },
64
+ auditTrail: {
65
+ commit: "abc123",
66
+ policy: "default",
67
+ policyVersion: "v1.0.0",
68
+ engineVersion: "trust-runtime-v1.0.0",
69
+ generatedAt: "2026-08-21T08:00:00.000Z",
70
+ reproducible: true,
71
+ checkId: "check_abc",
72
+ },
73
+ ...overrides,
74
+ };
75
+ }
76
+ (0, vitest_1.beforeEach)(() => {
77
+ vitest_1.vi.clearAllMocks();
78
+ mockExtractIR.mockReturnValue([]);
79
+ mockExecSync.mockImplementation((cmd) => {
80
+ const c = String(cmd);
81
+ if (c.includes("rev-parse"))
82
+ return "main";
83
+ if (c.includes("log --oneline"))
84
+ return "abc123 feat: demo";
85
+ if (c.includes("status --porcelain"))
86
+ return " M bad_flow.ts";
87
+ throw new Error("unexpected cmd: " + c);
88
+ });
89
+ });
90
+ (0, vitest_1.describe)("agent-patrol", () => {
91
+ (0, vitest_1.it)("违规映射到报告:fixPath 来自 violationTraces,autoApplied 恒为 false", async () => {
92
+ mockEvaluateTrust.mockResolvedValue(trustDecision());
93
+ const r = await (0, agent_patrol_1.runPatrol)("/tmp/fake-project");
94
+ (0, vitest_1.expect)(r.decision).toBe("BLOCKED");
95
+ (0, vitest_1.expect)(r.score).toBe(41);
96
+ (0, vitest_1.expect)(r.summary).toEqual({ critical: 0, high: 1, medium: 0, low: 0, total: 1 });
97
+ (0, vitest_1.expect)(r.findings).toHaveLength(1);
98
+ (0, vitest_1.expect)(r.findings[0].fixPath).toEqual(["verify_password"]);
99
+ (0, vitest_1.expect)(r.findings[0].reasoningSteps.length).toBe(1);
100
+ (0, vitest_1.expect)(r.autoApplied).toBe(false); // 铁律:永不自动合并
101
+ (0, vitest_1.expect)(r.auditTrail.map((e) => e.event)).toContain("patrol:scan");
102
+ });
103
+ (0, vitest_1.it)("无违规时 APPROVED 报告不含明细", async () => {
104
+ mockEvaluateTrust.mockResolvedValue(trustDecision({
105
+ overall: { score: 95, decision: "APPROVED", confidence: "HIGH" },
106
+ violations: [],
107
+ violationTraces: [],
108
+ summary: { critical: 0, high: 0, medium: 0, low: 0, total: 0 },
109
+ }));
110
+ const r = await (0, agent_patrol_1.runPatrol)("/tmp/fake-project");
111
+ (0, vitest_1.expect)(r.decision).toBe("APPROVED");
112
+ (0, vitest_1.expect)(r.findings).toHaveLength(0);
113
+ const md = (0, agent_patrol_1.formatPatrolMarkdown)(r);
114
+ (0, vitest_1.expect)(md).toContain("未发现违规");
115
+ (0, vitest_1.expect)(md).toContain("自动合并: 永不");
116
+ });
117
+ (0, vitest_1.it)("Markdown 报告含建议补丁路径与证据链回放", async () => {
118
+ mockEvaluateTrust.mockResolvedValue(trustDecision());
119
+ const r = await (0, agent_patrol_1.runPatrol)("/tmp/fake-project");
120
+ const md = (0, agent_patrol_1.formatPatrolMarkdown)(r);
121
+ (0, vitest_1.expect)(md).toContain("免疫巡逻报告");
122
+ (0, vitest_1.expect)(md).toContain("SSG_PROTOCOL");
123
+ (0, vitest_1.expect)(md).toContain("建议补丁路径");
124
+ (0, vitest_1.expect)(md).toContain("verify_password");
125
+ (0, vitest_1.expect)(md).toContain("推理回放");
126
+ (0, vitest_1.expect)(md).toContain("证据链(可回放)");
127
+ (0, vitest_1.expect)(md).toContain("checkId: check_abc");
128
+ });
129
+ (0, vitest_1.it)("writePatrolReport 落盘到项目目录", async () => {
130
+ const fs = require("fs");
131
+ const os = require("os");
132
+ const path = require("path");
133
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-patrol-"));
134
+ mockEvaluateTrust.mockResolvedValue(trustDecision());
135
+ const r = await (0, agent_patrol_1.runPatrol)(dir);
136
+ const reportPath = (0, agent_patrol_1.writePatrolReport)(r, dir);
137
+ (0, vitest_1.expect)(fs.existsSync(reportPath)).toBe(true);
138
+ const content = fs.readFileSync(reportPath, "utf-8");
139
+ (0, vitest_1.expect)(content).toContain("免疫巡逻报告");
140
+ });
141
+ });