progmune-runtime 3.3.7 → 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,324 @@
1
+ "use strict";
2
+ /**
3
+ * Phase 12: Agent Loop Controller (P1)
4
+ *
5
+ * Progmune Agent 最小闭环 —— 免疫门在环内的自主实现循环。
6
+ *
7
+ * Loop:
8
+ * intent → 目标分解(GoalPlanner) → execute()(plan→8门验证→SSG修复→emit→写盘+指纹)
9
+ * → verifyCompiles / verifyFileMarker(写盘后验证门)
10
+ * → 失败反馈注入 → 重试(≤maxRetries) → 迭代(≤maxIterations)
11
+ * → 成功输出带指纹 diff + 完整审计轨迹
12
+ *
13
+ * 铁律(Agent 化设计文档 v1.1):
14
+ * 1. 验证门必须在环内 —— 写盘前已过 plan/emit 内验证,写盘后再过编译+指纹门;
15
+ * 2. 违规优先确定性修复(execute 内 SSG 修复),其次 LLM 重试,最后明确降级;
16
+ * 3. 失败反馈必须注入下一次尝试 —— 不静默重试同一输入。
17
+ *
18
+ * 设计文档里程碑 M1 验收:
19
+ * progmune agent "实现 XX" → 全程过验证门 → 编译通过 → 输出带指纹 diff;
20
+ * 失败注入重试 ≤3;审计轨迹完整。
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.computeDiff = computeDiff;
57
+ exports.runAgentLoop = runAgentLoop;
58
+ const child_process_1 = require("child_process");
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
61
+ const execute_1 = require("./execute");
62
+ const goal_planner_1 = require("./goal-planner");
63
+ const agent_perception_1 = require("./agent-perception");
64
+ const agent_supervision_1 = require("./agent-supervision");
65
+ // ── Helpers ──
66
+ /** 单次执行超时包装。超时后底层 promise 继续运行(P1 已知限制,文档化即可)。 */
67
+ function withTimeout(p, ms, label) {
68
+ return new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => reject(new Error(`${label} 超时(${ms}ms)`)), ms);
70
+ p.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
71
+ });
72
+ }
73
+ /** 构造失败的 ExecuteResult(execute 抛异常时的兜底) */
74
+ function failedExecuteResult(error) {
75
+ return {
76
+ success: false,
77
+ code: "",
78
+ sessionId: "",
79
+ hash: "",
80
+ ruleHash: "",
81
+ irFunctionCount: 0,
82
+ protocolRuleCount: 0,
83
+ violations: 0,
84
+ degraded: false,
85
+ repairApplied: false,
86
+ repairCount: 0,
87
+ repairBranchIds: [],
88
+ error,
89
+ };
90
+ }
91
+ /** 计算目标文件的 git diff;新文件/非 git 仓库时回退为摘要。 */
92
+ function computeDiff(projectPath, filePath) {
93
+ const abs = path.isAbsolute(filePath) ? filePath : path.resolve(projectPath, filePath);
94
+ try {
95
+ const out = (0, child_process_1.execSync)(`git -C "${projectPath}" diff -- "${abs}"`, {
96
+ encoding: "utf-8",
97
+ timeout: 10000,
98
+ stdio: "pipe",
99
+ }).trim();
100
+ if (out)
101
+ return out;
102
+ const status = (0, child_process_1.execSync)(`git -C "${projectPath}" status --porcelain -- "${abs}"`, {
103
+ encoding: "utf-8",
104
+ timeout: 10000,
105
+ stdio: "pipe",
106
+ }).trim();
107
+ if (status)
108
+ return `(新文件,未跟踪)\n${status}`;
109
+ return "(无 git 变更)";
110
+ }
111
+ catch (e) {
112
+ // 非 git 仓库或文件不存在 → 回退为文件内容摘要
113
+ try {
114
+ const content = fs.readFileSync(abs, "utf-8");
115
+ return `(git diff 不可用: ${e.message})\n${content.slice(0, 500)}`;
116
+ }
117
+ catch {
118
+ return `(git diff 不可用: ${e.message})`;
119
+ }
120
+ }
121
+ }
122
+ // ── Main Loop ──
123
+ /**
124
+ * 运行 P1 最小 agent loop。
125
+ *
126
+ * @requires INTENT @produces AGENT_LOOP_RESULT
127
+ */
128
+ async function runAgentLoop(opts) {
129
+ const projectPath = path.resolve(opts.projectPath);
130
+ const maxIterations = opts.maxIterations ?? 5;
131
+ const maxRetries = opts.maxRetries ?? 3;
132
+ const timeoutMs = opts.timeoutMs ?? 120000;
133
+ const includeContext = opts.includeContext ?? false;
134
+ const runTestsGate = opts.runTests ?? false;
135
+ const attempts = [];
136
+ const auditTrail = [];
137
+ const audit = (event, detail) => auditTrail.push({ timestamp: new Date().toISOString(), event, detail });
138
+ audit("loop:start", `intent="${opts.intent}" project=${projectPath} file=${opts.filePath || "(未指定)"} ` +
139
+ `maxIterations=${maxIterations} maxRetries=${maxRetries} timeoutMs=${timeoutMs} ` +
140
+ `context=${includeContext} tests=${runTestsGate}`);
141
+ // ── P2 感知:git 上下文(best-effort) ──
142
+ let gitContext;
143
+ let contextHint = "";
144
+ if (includeContext) {
145
+ gitContext = (0, agent_perception_1.collectGitContext)(projectPath);
146
+ audit("perception:git", gitContext.available
147
+ ? `branch=${gitContext.branch} commits=${gitContext.recentCommits.length} ` +
148
+ `changed=${gitContext.changedFiles.length} files=${gitContext.sourceFiles.length}`
149
+ : `不可用: ${gitContext.error}`);
150
+ if (gitContext.available) {
151
+ contextHint =
152
+ `\n[项目上下文:分支 ${gitContext.branch};` +
153
+ `最近提交: ${gitContext.recentCommits.slice(0, 2).join(" / ") || "(无)"};` +
154
+ `变更文件: ${gitContext.changedFiles.slice(0, 5).join(", ") || "(无)"}]`;
155
+ }
156
+ }
157
+ // ── P2 感知:初始 IR 函数名集合(成功时算增量) ──
158
+ let prevIRNames;
159
+ try {
160
+ const { ir } = (0, agent_perception_1.extractIRWithDelta)(projectPath);
161
+ prevIRNames = new Set(ir.map((f) => String(f.name || "")).filter(Boolean));
162
+ audit("perception:ir", `初始 IR ${prevIRNames.size} 个函数`);
163
+ }
164
+ catch (e) {
165
+ audit("perception:ir", `初始 IR 提取失败(忽略): ${e.message}`);
166
+ }
167
+ // ── 目标分解(best-effort,不阻塞主循环) ──
168
+ let subgoals = [];
169
+ try {
170
+ subgoals = (0, goal_planner_1.expandGoalActions)(opts.intent);
171
+ audit("goal:decompose", subgoals.length > 0 ? `子目标: ${subgoals.join(" → ")}` : "无模板命中,单目标直行");
172
+ }
173
+ catch (e) {
174
+ audit("goal:decompose", `目标分解失败(忽略): ${e.message}`);
175
+ }
176
+ let attemptNo = 0;
177
+ const baseIntent = `${opts.intent}${contextHint}`;
178
+ let currentIntent = baseIntent;
179
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
180
+ audit("iteration:start", `第 ${iteration}/${maxIterations} 轮`);
181
+ for (let retry = 0; retry < maxRetries; retry++) {
182
+ attemptNo++;
183
+ const startedAt = new Date().toISOString();
184
+ audit("attempt:start", `#${attemptNo} (iter ${iteration}, retry ${retry}) intent="${currentIntent.slice(0, 120)}"`);
185
+ // ── 执行(内部含 plan → 8 门验证 → SSG 修复 → emit → 写盘+指纹) ──
186
+ let result;
187
+ try {
188
+ result = await withTimeout((0, execute_1.execute)(currentIntent, projectPath, opts.filePath), timeoutMs, `execute #${attemptNo}`);
189
+ }
190
+ catch (e) {
191
+ result = failedExecuteResult(`execute 抛出异常: ${e.message}`);
192
+ }
193
+ // ── 写盘后验证门(编译 + 指纹标记) ──
194
+ let compilePass = false;
195
+ let markerPass = false;
196
+ let filePath = opts.filePath || result.filePath;
197
+ if (result.success && filePath) {
198
+ const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(projectPath, filePath);
199
+ try {
200
+ const compile = (0, execute_1.verifyCompiles)(resolved);
201
+ compilePass = compile.pass;
202
+ const marker = (0, execute_1.verifyFileMarker)(resolved);
203
+ markerPass = marker.marked;
204
+ }
205
+ catch (e) {
206
+ audit("verify:error", `写盘后验证门异常: ${e.message}`);
207
+ }
208
+ }
209
+ else if (result.success) {
210
+ // 产码模式(未指定输出文件):编译门不适用;指纹门改为检查代码头部标记
211
+ compilePass = true;
212
+ markerPass = result.code.includes("@progmune-generated");
213
+ }
214
+ // ── P3 自监督:项目测试门(可选,编译/指纹通过后才跑) ──
215
+ let testRan = false;
216
+ let testPass = true;
217
+ let testFailureSummary = "";
218
+ if (result.success && compilePass && markerPass && runTestsGate && filePath) {
219
+ try {
220
+ const t = (0, agent_supervision_1.runProjectTests)(projectPath);
221
+ testRan = t.ran;
222
+ testPass = t.pass;
223
+ if (t.ran) {
224
+ audit("verify:test", t.pass ? `测试通过 (${t.command})` : `测试失败: ${t.failures.slice(0, 3).join(" | ")}`);
225
+ if (!t.pass)
226
+ testFailureSummary = `项目测试失败: ${t.failures.slice(0, 3).join(";")}`;
227
+ }
228
+ }
229
+ catch (e) {
230
+ audit("verify:test", `测试门异常(忽略): ${e.message}`);
231
+ }
232
+ }
233
+ const attempt = {
234
+ attempt: attemptNo,
235
+ iteration,
236
+ intent: currentIntent,
237
+ feedback: attemptNo > 1 ? currentIntent.slice(opts.intent.length) || undefined : undefined,
238
+ startedAt,
239
+ success: result.success && compilePass && markerPass && (!testRan || testPass),
240
+ degraded: result.degraded || false,
241
+ sessionId: result.sessionId || "",
242
+ filePath,
243
+ fingerprint: result.hash || "",
244
+ ruleHash: result.ruleHash || "",
245
+ irFunctionCount: result.irFunctionCount,
246
+ violations: result.violations,
247
+ repairApplied: result.repairApplied,
248
+ repairCount: result.repairCount,
249
+ compilePass,
250
+ markerPass,
251
+ testRan,
252
+ testPass,
253
+ error: result.error,
254
+ };
255
+ attempts.push(attempt);
256
+ // ── 成功出口 ──
257
+ if (attempt.success) {
258
+ const diff = filePath ? computeDiff(projectPath, filePath) : "(未指定输出文件,无 diff)";
259
+ audit("attempt:ok", `#${attemptNo} 验证门全通过: sessionId=${result.sessionId} fingerprint=${result.hash} ` +
260
+ `compile=${compilePass} marker=${markerPass} test=${testRan ? testPass : "(未跑)"} ` +
261
+ `repairApplied=${result.repairApplied}`);
262
+ audit("loop:success", `fingerprint=${result.hash} 迭代=${iteration} 重试=${attemptNo - 1}`);
263
+ // ── P2 感知:成功时 IR 增量(agent 写盘后 IR 变化观测) ──
264
+ let irDelta;
265
+ try {
266
+ const { delta } = (0, agent_perception_1.extractIRWithDelta)(projectPath, prevIRNames);
267
+ irDelta = delta;
268
+ audit("perception:ir", `IR 增量: +${delta.added.length} -${delta.removed.length} (共 ${delta.functionCount} 函数)` +
269
+ (delta.added.length > 0 ? ` 新增: ${delta.added.join(", ")}` : ""));
270
+ }
271
+ catch (e) {
272
+ audit("perception:ir", `成功时 IR 增量提取失败(忽略): ${e.message}`);
273
+ }
274
+ return {
275
+ success: true,
276
+ attempts,
277
+ iterations: iteration,
278
+ retries: attemptNo - 1,
279
+ subgoals,
280
+ filePath,
281
+ fingerprint: result.hash,
282
+ diff,
283
+ auditTrail,
284
+ degraded: result.degraded || false,
285
+ irDelta,
286
+ gitContext,
287
+ };
288
+ }
289
+ // ── 失败反馈注入(不静默重试同一输入) ──
290
+ const reasons = [];
291
+ if (!result.success)
292
+ reasons.push(result.error || "执行失败");
293
+ if (result.success && !compilePass)
294
+ reasons.push("编译验证未通过");
295
+ if (result.success && !markerPass)
296
+ reasons.push("指纹标记缺失");
297
+ if (testFailureSummary)
298
+ reasons.push(testFailureSummary);
299
+ const feedback = reasons.join(";");
300
+ audit("attempt:fail", `#${attemptNo} ${feedback || "未知原因"}`);
301
+ if (retry < maxRetries - 1) {
302
+ currentIntent = `${baseIntent}\n[上一次尝试失败:${feedback}。请修复后重新实现。]`;
303
+ audit("retry", `注入反馈后重试 #${retry + 2}/${maxRetries}`);
304
+ }
305
+ else {
306
+ currentIntent = baseIntent; // 进入下一迭代前复位意图
307
+ }
308
+ }
309
+ audit("iteration:end", `第 ${iteration} 轮耗尽 ${maxRetries} 次重试`);
310
+ }
311
+ audit("loop:exhausted", `迭代上限 ${maxIterations} 轮后仍未成功,共 ${attemptNo} 次尝试`);
312
+ return {
313
+ success: false,
314
+ attempts,
315
+ iterations: maxIterations,
316
+ retries: attemptNo,
317
+ subgoals,
318
+ fingerprint: "",
319
+ diff: "",
320
+ auditTrail,
321
+ degraded: attempts.some((a) => a.degraded),
322
+ gitContext,
323
+ };
324
+ }
@@ -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
+ });