edsger 0.42.1 → 0.44.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.
- package/.claude/settings.local.json +23 -3
- package/.env.local +12 -0
- package/dist/api/release-test-cases.d.ts +7 -0
- package/dist/api/release-test-cases.js +21 -0
- package/dist/api/releases.d.ts +41 -0
- package/dist/api/releases.js +31 -0
- package/dist/api/web-deploy.d.ts +8 -1
- package/dist/api/web-deploy.js +2 -1
- package/dist/commands/release-sync/index.d.ts +5 -0
- package/dist/commands/release-sync/index.js +38 -0
- package/dist/commands/smoke-test/index.d.ts +5 -0
- package/dist/commands/smoke-test/index.js +40 -0
- package/dist/commands/workflow/phase-orchestrator.js +3 -1
- package/dist/index.js +40 -0
- package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +1 -0
- package/dist/phases/app-store-generation/index.js +3 -1
- package/dist/phases/app-store-generation/screenshot-composer.js +34 -10
- package/dist/phases/branch-planning/index.js +3 -1
- package/dist/phases/bug-fixing/analyzer.js +3 -1
- package/dist/phases/code-implementation/index.js +3 -1
- package/dist/phases/code-refine/index.js +3 -1
- package/dist/phases/code-review/__tests__/diff-utils.test.js +11 -11
- package/dist/phases/code-review/index.js +3 -1
- package/dist/phases/code-testing/analyzer.js +3 -1
- package/dist/phases/feature-analysis/index.js +3 -1
- package/dist/phases/functional-testing/analyzer.js +3 -1
- package/dist/phases/growth-analysis/index.js +3 -1
- package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +12 -12
- package/dist/phases/intelligence-analysis/agent.js +2 -0
- package/dist/phases/intelligence-analysis/index.js +1 -0
- package/dist/phases/intelligence-analysis/prompts.js +11 -1
- package/dist/phases/output-contracts.js +1 -0
- package/dist/phases/pr-execution/__tests__/file-assigner.test.js +22 -13
- package/dist/phases/pr-execution/context.js +4 -2
- package/dist/phases/pr-execution/file-assigner.js +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +11 -11
- package/dist/phases/pr-resolve/__tests__/prompts.test.js +12 -12
- package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +6 -6
- package/dist/phases/pr-resolve/__tests__/types.test.js +11 -11
- package/dist/phases/pr-resolve/__tests__/workspace.test.js +13 -13
- package/dist/phases/pr-resolve/checklist-learner.js +34 -9
- package/dist/phases/pr-resolve/index.js +29 -13
- package/dist/phases/pr-resolve/prompts.js +2 -1
- package/dist/phases/pr-resolve/workspace.d.ts +12 -2
- package/dist/phases/pr-resolve/workspace.js +6 -4
- package/dist/phases/pr-review/__tests__/prompts.test.js +9 -9
- package/dist/phases/pr-review/__tests__/review-comments.test.js +6 -6
- package/dist/phases/pr-review/index.js +1 -0
- package/dist/phases/pr-shared/__tests__/agent-utils.test.js +17 -17
- package/dist/phases/pr-shared/__tests__/context.test.js +12 -12
- package/dist/phases/pr-splitting/import-dep-validator.js +14 -6
- package/dist/phases/pr-splitting/index.js +3 -1
- package/dist/phases/release-sync/__tests__/github.test.d.ts +9 -0
- package/dist/phases/release-sync/__tests__/github.test.js +123 -0
- package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +8 -0
- package/dist/phases/release-sync/__tests__/snapshot.test.js +93 -0
- package/dist/phases/release-sync/github.d.ts +54 -0
- package/dist/phases/release-sync/github.js +101 -0
- package/dist/phases/release-sync/index.d.ts +24 -0
- package/dist/phases/release-sync/index.js +147 -0
- package/dist/phases/release-sync/snapshot.d.ts +27 -0
- package/dist/phases/release-sync/snapshot.js +159 -0
- package/dist/phases/smoke-test/__tests__/agent.test.d.ts +4 -0
- package/dist/phases/smoke-test/__tests__/agent.test.js +85 -0
- package/dist/phases/smoke-test/agent.d.ts +12 -0
- package/dist/phases/smoke-test/agent.js +94 -0
- package/dist/phases/smoke-test/index.d.ts +22 -0
- package/dist/phases/smoke-test/index.js +233 -0
- package/dist/phases/smoke-test/prompts.d.ts +15 -0
- package/dist/phases/smoke-test/prompts.js +35 -0
- package/dist/phases/technical-design/index.js +3 -1
- package/dist/phases/test-cases-analysis/index.js +3 -1
- package/dist/phases/user-stories-analysis/index.js +3 -1
- package/dist/services/phase-hooks/__tests__/hook-executor.test.js +7 -4
- package/dist/services/phase-hooks/__tests__/hook-runner.test.js +22 -21
- package/dist/services/phase-hooks/hook-executor.js +1 -0
- package/dist/services/phase-hooks/plugin-loader.js +3 -0
- package/dist/services/video/screenshot-generator.js +8 -2
- package/dist/skills/phase/smoke-test/SKILL.md +80 -0
- package/dist/utils/json-extract.d.ts +6 -0
- package/dist/utils/json-extract.js +44 -0
- package/dist/workspace/__tests__/workspace-manager.test.d.ts +7 -0
- package/dist/workspace/__tests__/workspace-manager.test.js +52 -0
- package/dist/workspace/workspace-manager.d.ts +31 -0
- package/dist/workspace/workspace-manager.js +96 -10
- package/package.json +1 -1
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +0 -133
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +0 -4
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +0 -336
- package/dist/services/lifecycle-agent/index.d.ts +0 -24
- package/dist/services/lifecycle-agent/index.js +0 -25
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +0 -57
- package/dist/services/lifecycle-agent/phase-criteria.js +0 -335
- package/dist/services/lifecycle-agent/transition-rules.d.ts +0 -60
- package/dist/services/lifecycle-agent/transition-rules.js +0 -184
- package/dist/services/lifecycle-agent/types.d.ts +0 -190
- package/dist/services/lifecycle-agent/types.js +0 -12
|
@@ -110,26 +110,26 @@ function makeDeps(aiResult, contextOverride) {
|
|
|
110
110
|
saveSnapCalls,
|
|
111
111
|
saveRptCalls,
|
|
112
112
|
updateRptCalls,
|
|
113
|
-
prepareContext:
|
|
113
|
+
prepareContext: () => Promise.resolve({
|
|
114
114
|
context: contextOverride || makeContext(),
|
|
115
115
|
analysisPrompt: 'fake prompt',
|
|
116
116
|
}),
|
|
117
|
-
executeQuery:
|
|
118
|
-
batchCreate:
|
|
117
|
+
executeQuery: () => Promise.resolve(aiResult),
|
|
118
|
+
batchCreate: (_productId, competitors) => {
|
|
119
119
|
batchCreateCalls.push([_productId, competitors]);
|
|
120
|
-
return competitors.map((c) => makeCompetitor(c.name, `discovered-${batchCreateCounter++}`, 'suggested'));
|
|
120
|
+
return Promise.resolve(competitors.map((c) => makeCompetitor(c.name, `discovered-${batchCreateCounter++}`, 'suggested')));
|
|
121
121
|
},
|
|
122
|
-
saveSnap:
|
|
122
|
+
saveSnap: (snap) => {
|
|
123
123
|
saveSnapCalls.push([snap]);
|
|
124
|
-
return makeSnapshot(snap.competitor_id, `snap-${saveSnapCalls.length}`);
|
|
124
|
+
return Promise.resolve(makeSnapshot(snap.competitor_id, `snap-${saveSnapCalls.length}`));
|
|
125
125
|
},
|
|
126
|
-
saveRpt:
|
|
126
|
+
saveRpt: (report) => {
|
|
127
127
|
saveRptCalls.push([report]);
|
|
128
|
-
return makeReport(`rpt-${saveRptCalls.length}`);
|
|
128
|
+
return Promise.resolve(makeReport(`rpt-${saveRptCalls.length}`));
|
|
129
129
|
},
|
|
130
|
-
updateRpt:
|
|
130
|
+
updateRpt: (_id, updates) => {
|
|
131
131
|
updateRptCalls.push([_id, updates]);
|
|
132
|
-
return makeReport(_id);
|
|
132
|
+
return Promise.resolve(makeReport(_id));
|
|
133
133
|
},
|
|
134
134
|
};
|
|
135
135
|
}
|
|
@@ -368,8 +368,8 @@ void describe('analyseIntelligence — guidance', () => {
|
|
|
368
368
|
void describe('analyseIntelligence — exception in deps', () => {
|
|
369
369
|
void it('should return error when context fetch throws', async () => {
|
|
370
370
|
const deps = makeDeps(null);
|
|
371
|
-
deps.prepareContext =
|
|
372
|
-
|
|
371
|
+
deps.prepareContext = () => {
|
|
372
|
+
return Promise.reject(new Error('Network error'));
|
|
373
373
|
};
|
|
374
374
|
const result = await analyseIntelligence({ productId: 'prod-001' }, fakeConfig, deps);
|
|
375
375
|
assert.strictEqual(result.status, 'error');
|
|
@@ -261,12 +261,14 @@ function parseIntelligenceResult(responseText) {
|
|
|
261
261
|
error: 'JSON parsing failed: could not extract structured data from response',
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
264
265
|
async function* prompt(analysisPrompt) {
|
|
265
266
|
yield {
|
|
266
267
|
type: 'user',
|
|
267
268
|
message: { role: 'user', content: analysisPrompt },
|
|
268
269
|
};
|
|
269
270
|
}
|
|
271
|
+
// eslint-disable-next-line complexity
|
|
270
272
|
export async function executeIntelligenceQuery(currentPrompt, systemPrompt, _config, verbose, cwd) {
|
|
271
273
|
let lastAssistantResponse = '';
|
|
272
274
|
let structuredResult = null;
|
|
@@ -74,6 +74,7 @@ const defaultDeps = {
|
|
|
74
74
|
* 2. AI agent researches competitors and market via web search
|
|
75
75
|
* 3. Save discovered competitors, snapshots, and report
|
|
76
76
|
*/
|
|
77
|
+
// eslint-disable-next-line complexity
|
|
77
78
|
export async function analyseIntelligence(options, config, deps = defaultDeps) {
|
|
78
79
|
const { productId, reportType = 'competitive', verbose, guidance, reportId, } = options;
|
|
79
80
|
try {
|
|
@@ -29,9 +29,19 @@ export const createIntelligencePromptWithContext = (productId, contextInfo, repo
|
|
|
29
29
|
**Competitor Discovery**: While analyzing confirmed competitors, also look for new entrants or products that should be tracked. Include any new finds in the "discovered_competitors" array.`;
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
32
|
+
let focusLabel;
|
|
33
|
+
if (reportType === 'research') {
|
|
34
|
+
focusLabel = 'papers and reports';
|
|
35
|
+
}
|
|
36
|
+
else if (reportType === 'market') {
|
|
37
|
+
focusLabel = 'market signals and trends';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
focusLabel = 'product-market fit metrics';
|
|
41
|
+
}
|
|
32
42
|
discoveryInstructions = `
|
|
33
43
|
|
|
34
|
-
**Note**: This is a ${reportType} analysis — focus on ${
|
|
44
|
+
**Note**: This is a ${reportType} analysis — focus on ${focusLabel}. Do NOT discover new competitors. Set "discovered_competitors" to an empty array.`;
|
|
35
45
|
}
|
|
36
46
|
const researchInstructions = reportType === 'research'
|
|
37
47
|
? `
|
|
@@ -20,8 +20,8 @@ function makePR(overrides) {
|
|
|
20
20
|
...overrides,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
function cf(path,
|
|
24
|
-
return { path, change_type };
|
|
23
|
+
function cf(path, changeType = 'modified') {
|
|
24
|
+
return { path, change_type: changeType };
|
|
25
25
|
}
|
|
26
26
|
// ============================================================
|
|
27
27
|
// findUnassignedFiles
|
|
@@ -133,8 +133,9 @@ void describe('applyAssignments', () => {
|
|
|
133
133
|
];
|
|
134
134
|
const unassigned = [cf('b.ts', 'added')];
|
|
135
135
|
const updates = [];
|
|
136
|
-
const mockUpdater =
|
|
136
|
+
const mockUpdater = (prId, u) => {
|
|
137
137
|
updates.push({ prId, files: u.files });
|
|
138
|
+
return Promise.resolve();
|
|
138
139
|
};
|
|
139
140
|
const count = await applyAssignments([{ file: 'b.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
140
141
|
assert.strictEqual(count, 1);
|
|
@@ -155,8 +156,9 @@ void describe('applyAssignments', () => {
|
|
|
155
156
|
];
|
|
156
157
|
const unassigned = [cf('a.ts')];
|
|
157
158
|
const updates = [];
|
|
158
|
-
const mockUpdater =
|
|
159
|
+
const mockUpdater = (_prId, u) => {
|
|
159
160
|
updates.push({ files: u.files });
|
|
161
|
+
return Promise.resolve();
|
|
160
162
|
};
|
|
161
163
|
await applyAssignments([{ file: 'a.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
162
164
|
// File list should not have duplicates
|
|
@@ -168,8 +170,9 @@ void describe('applyAssignments', () => {
|
|
|
168
170
|
const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
|
|
169
171
|
const unassigned = [cf('x.ts', 'added')];
|
|
170
172
|
let called = false;
|
|
171
|
-
const mockUpdater =
|
|
173
|
+
const mockUpdater = () => {
|
|
172
174
|
called = true;
|
|
175
|
+
return Promise.resolve();
|
|
173
176
|
};
|
|
174
177
|
const count = await applyAssignments([{ file: 'x.ts', pr_id: 'unknown-id' }], unassigned, prs, mockUpdater);
|
|
175
178
|
assert.strictEqual(count, 0);
|
|
@@ -179,8 +182,9 @@ void describe('applyAssignments', () => {
|
|
|
179
182
|
const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
|
|
180
183
|
const unassigned = [cf('new.ts', 'added')];
|
|
181
184
|
const updates = [];
|
|
182
|
-
const mockUpdater =
|
|
185
|
+
const mockUpdater = (_prId, u) => {
|
|
183
186
|
updates.push({ files: u.files });
|
|
187
|
+
return Promise.resolve();
|
|
184
188
|
};
|
|
185
189
|
await applyAssignments([{ file: 'new.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
186
190
|
assert.strictEqual(updates[0].files[0].change_type, 'added');
|
|
@@ -195,8 +199,9 @@ void describe('applyAssignments', () => {
|
|
|
195
199
|
cf('b.ts', 'modified'),
|
|
196
200
|
];
|
|
197
201
|
const updates = [];
|
|
198
|
-
const mockUpdater =
|
|
202
|
+
const mockUpdater = (prId) => {
|
|
199
203
|
updates.push({ prId });
|
|
204
|
+
return Promise.resolve();
|
|
200
205
|
};
|
|
201
206
|
const count = await applyAssignments([
|
|
202
207
|
{ file: 'a.ts', pr_id: 'pr-1' },
|
|
@@ -208,8 +213,8 @@ void describe('applyAssignments', () => {
|
|
|
208
213
|
void it('handles updater failure gracefully', async () => {
|
|
209
214
|
const prs = [makePR({ id: 'pr-1', sequence: 1, files: [] })];
|
|
210
215
|
const unassigned = [cf('a.ts')];
|
|
211
|
-
const mockUpdater =
|
|
212
|
-
|
|
216
|
+
const mockUpdater = () => {
|
|
217
|
+
return Promise.reject(new Error('DB error'));
|
|
213
218
|
};
|
|
214
219
|
const count = await applyAssignments([{ file: 'a.ts', pr_id: 'pr-1' }], unassigned, prs, mockUpdater);
|
|
215
220
|
assert.strictEqual(count, 0);
|
|
@@ -235,8 +240,9 @@ void describe('removeDeletedFilesFromPRs', () => {
|
|
|
235
240
|
cf('gone.ts', 'deleted'),
|
|
236
241
|
];
|
|
237
242
|
const updates = [];
|
|
238
|
-
const mockUpdater =
|
|
243
|
+
const mockUpdater = (prId, u) => {
|
|
239
244
|
updates.push({ prId, files: u.files });
|
|
245
|
+
return Promise.resolve();
|
|
240
246
|
};
|
|
241
247
|
const count = await removeDeletedFilesFromPRs(changedFiles, prs, mockUpdater);
|
|
242
248
|
assert.strictEqual(count, 1);
|
|
@@ -253,8 +259,9 @@ void describe('removeDeletedFilesFromPRs', () => {
|
|
|
253
259
|
}),
|
|
254
260
|
];
|
|
255
261
|
let called = false;
|
|
256
|
-
const mockUpdater =
|
|
262
|
+
const mockUpdater = () => {
|
|
257
263
|
called = true;
|
|
264
|
+
return Promise.resolve();
|
|
258
265
|
};
|
|
259
266
|
const count = await removeDeletedFilesFromPRs([cf('a.ts')], prs, mockUpdater);
|
|
260
267
|
assert.strictEqual(count, 0);
|
|
@@ -274,8 +281,9 @@ void describe('removeDeletedFilesFromPRs', () => {
|
|
|
274
281
|
}),
|
|
275
282
|
];
|
|
276
283
|
const updates = [];
|
|
277
|
-
const mockUpdater =
|
|
284
|
+
const mockUpdater = (prId) => {
|
|
278
285
|
updates.push(prId);
|
|
286
|
+
return Promise.resolve();
|
|
279
287
|
};
|
|
280
288
|
await removeDeletedFilesFromPRs([cf('b.ts', 'deleted')], prs, mockUpdater);
|
|
281
289
|
// Only pr-2 should be updated
|
|
@@ -284,8 +292,9 @@ void describe('removeDeletedFilesFromPRs', () => {
|
|
|
284
292
|
void it('handles PRs with null files', async () => {
|
|
285
293
|
const prs = [makePR({ id: 'pr-1', sequence: 1, files: null })];
|
|
286
294
|
let called = false;
|
|
287
|
-
const mockUpdater =
|
|
295
|
+
const mockUpdater = () => {
|
|
288
296
|
called = true;
|
|
297
|
+
return Promise.resolve();
|
|
289
298
|
};
|
|
290
299
|
const count = await removeDeletedFilesFromPRs([cf('a.ts', 'deleted')], prs, mockUpdater);
|
|
291
300
|
assert.strictEqual(count, 0);
|
|
@@ -193,7 +193,9 @@ export async function fetchPRExecutionContext(featureId, verbose) {
|
|
|
193
193
|
// If the branch was rebased, lastSyncedCommit is no longer in the history.
|
|
194
194
|
// Treat this as a full re-execution so downstream logic uses the correct
|
|
195
195
|
// prompt and skips incremental file-assignment against an invalid base.
|
|
196
|
-
if (isIncrementalSync &&
|
|
196
|
+
if (isIncrementalSync &&
|
|
197
|
+
lastSyncedCommit &&
|
|
198
|
+
!isAncestor(lastSyncedCommit, devRef)) {
|
|
197
199
|
if (verbose) {
|
|
198
200
|
logInfo(`⚠️ Last synced commit ${lastSyncedCommit} is no longer in branch history (likely rebased). Treating as full re-execution.`);
|
|
199
201
|
}
|
|
@@ -201,7 +203,7 @@ export async function fetchPRExecutionContext(featureId, verbose) {
|
|
|
201
203
|
isIncrementalSync = false;
|
|
202
204
|
}
|
|
203
205
|
// Get diff info: for incremental, diff from last sync; for first run, diff from main
|
|
204
|
-
const diffBase = isIncrementalSync ? lastSyncedCommit : 'main';
|
|
206
|
+
const diffBase = isIncrementalSync && lastSyncedCommit ? lastSyncedCommit : 'main';
|
|
205
207
|
const diffStat = getDiffStat(diffBase, devRef);
|
|
206
208
|
const changedFiles = getChangedFiles(diffBase, devRef);
|
|
207
209
|
if (verbose) {
|
|
@@ -97,6 +97,7 @@ async function callLLMForAssignment(prompt, verbose) {
|
|
|
97
97
|
})) {
|
|
98
98
|
if (message.type === 'assistant' && message.message?.content) {
|
|
99
99
|
for (const item of message.message.content) {
|
|
100
|
+
// eslint-disable-next-line max-depth
|
|
100
101
|
if (item.type === 'text') {
|
|
101
102
|
responseText += `${item.text}\n`;
|
|
102
103
|
}
|
|
@@ -26,8 +26,8 @@ function makeMap(entries) {
|
|
|
26
26
|
return new Map(entries);
|
|
27
27
|
}
|
|
28
28
|
// ── buildLearnerPrompt ──────────────────────────────────────
|
|
29
|
-
describe('buildLearnerPrompt', () => {
|
|
30
|
-
it('includes addressed comment count in header', () => {
|
|
29
|
+
void describe('buildLearnerPrompt', () => {
|
|
30
|
+
void it('includes addressed comment count in header', () => {
|
|
31
31
|
const comments = [
|
|
32
32
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
33
33
|
{ comment_id: 'comment_2', action: 'changed', reply: 'Done' },
|
|
@@ -40,7 +40,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
40
40
|
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
41
41
|
assert.ok(prompt.includes('2 review comment(s)'));
|
|
42
42
|
});
|
|
43
|
-
it('includes reviewer, file path, line, and comment body', () => {
|
|
43
|
+
void it('includes reviewer, file path, line, and comment body', () => {
|
|
44
44
|
const comments = [
|
|
45
45
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Added null check' },
|
|
46
46
|
];
|
|
@@ -60,7 +60,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
60
60
|
assert.ok(prompt.includes('Missing null check here'));
|
|
61
61
|
assert.ok(prompt.includes('Added null check'));
|
|
62
62
|
});
|
|
63
|
-
it('includes resolution text for each comment', () => {
|
|
63
|
+
void it('includes resolution text for each comment', () => {
|
|
64
64
|
const comments = [
|
|
65
65
|
{
|
|
66
66
|
comment_id: 'comment_1',
|
|
@@ -73,7 +73,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
73
73
|
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
74
74
|
assert.ok(prompt.includes('**Resolution**: Refactored to use try/catch'));
|
|
75
75
|
});
|
|
76
|
-
it('includes summary when provided', () => {
|
|
76
|
+
void it('includes summary when provided', () => {
|
|
77
77
|
const comments = [
|
|
78
78
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
79
79
|
];
|
|
@@ -83,7 +83,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
83
83
|
assert.ok(prompt.includes('## Overall Summary'));
|
|
84
84
|
assert.ok(prompt.includes('Improved error handling across 3 files'));
|
|
85
85
|
});
|
|
86
|
-
it('omits summary section when not provided', () => {
|
|
86
|
+
void it('omits summary section when not provided', () => {
|
|
87
87
|
const comments = [
|
|
88
88
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
89
89
|
];
|
|
@@ -92,7 +92,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
92
92
|
const prompt = buildLearnerPrompt(comments, threads, map);
|
|
93
93
|
assert.ok(!prompt.includes('## Overall Summary'));
|
|
94
94
|
});
|
|
95
|
-
it('handles comment with no matching thread gracefully', () => {
|
|
95
|
+
void it('handles comment with no matching thread gracefully', () => {
|
|
96
96
|
const comments = [
|
|
97
97
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
98
98
|
];
|
|
@@ -105,7 +105,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
105
105
|
// Should NOT include reviewer info since thread was not found
|
|
106
106
|
assert.ok(!prompt.includes('**Reviewer**'));
|
|
107
107
|
});
|
|
108
|
-
it('handles comment_id not in map gracefully', () => {
|
|
108
|
+
void it('handles comment_id not in map gracefully', () => {
|
|
109
109
|
const comments = [
|
|
110
110
|
{ comment_id: 'comment_99', action: 'changed', reply: 'Fixed' },
|
|
111
111
|
];
|
|
@@ -113,7 +113,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
113
113
|
assert.ok(prompt.includes('## comment_99'));
|
|
114
114
|
assert.ok(prompt.includes('**Resolution**: Fixed'));
|
|
115
115
|
});
|
|
116
|
-
it('omits line when null', () => {
|
|
116
|
+
void it('omits line when null', () => {
|
|
117
117
|
const comments = [
|
|
118
118
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
119
119
|
];
|
|
@@ -123,7 +123,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
123
123
|
assert.ok(prompt.includes('**File**: src/index.ts'));
|
|
124
124
|
assert.ok(!prompt.includes('**Line**'));
|
|
125
125
|
});
|
|
126
|
-
it('uses threadById map correctly for multiple comments', () => {
|
|
126
|
+
void it('uses threadById map correctly for multiple comments', () => {
|
|
127
127
|
const comments = [
|
|
128
128
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fix A' },
|
|
129
129
|
{ comment_id: 'comment_3', action: 'changed', reply: 'Fix C' },
|
|
@@ -146,7 +146,7 @@ describe('buildLearnerPrompt', () => {
|
|
|
146
146
|
// comment_2 was not in addressedComments, should not appear
|
|
147
147
|
assert.ok(!prompt.includes('Issue B'));
|
|
148
148
|
});
|
|
149
|
-
it('includes instructions section', () => {
|
|
149
|
+
void it('includes instructions section', () => {
|
|
150
150
|
const comments = [
|
|
151
151
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
152
152
|
];
|
|
@@ -34,26 +34,26 @@ function makeThread(id, overrides) {
|
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
describe('createResolveSystemPrompt', () => {
|
|
38
|
-
it('includes decision criteria', () => {
|
|
37
|
+
void describe('createResolveSystemPrompt', () => {
|
|
38
|
+
void it('includes decision criteria', () => {
|
|
39
39
|
const prompt = createResolveSystemPrompt();
|
|
40
40
|
assert.ok(prompt.includes('Make the change when'));
|
|
41
41
|
assert.ok(prompt.includes('Skip the change when'));
|
|
42
42
|
});
|
|
43
|
-
it('specifies comment_id format', () => {
|
|
43
|
+
void it('specifies comment_id format', () => {
|
|
44
44
|
const prompt = createResolveSystemPrompt();
|
|
45
45
|
assert.ok(prompt.includes('comment_id'));
|
|
46
46
|
assert.ok(prompt.includes('comment_1'));
|
|
47
47
|
});
|
|
48
|
-
it('includes result format', () => {
|
|
48
|
+
void it('includes result format', () => {
|
|
49
49
|
const prompt = createResolveSystemPrompt();
|
|
50
50
|
assert.ok(prompt.includes('resolve_result'));
|
|
51
51
|
assert.ok(prompt.includes('"action"'));
|
|
52
52
|
assert.ok(prompt.includes('"reply"'));
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
-
describe('createResolveUserPrompt', () => {
|
|
56
|
-
it('uses sequential comment IDs not thread IDs', () => {
|
|
55
|
+
void describe('createResolveUserPrompt', () => {
|
|
56
|
+
void it('uses sequential comment IDs not thread IDs', () => {
|
|
57
57
|
const threads = [makeThread('PRRT_kwDOxx_1'), makeThread('PRRT_kwDOxx_2')];
|
|
58
58
|
const { prompt, commentIdToThreadId } = createResolveUserPrompt(threads);
|
|
59
59
|
// Should use comment_1, comment_2 in the prompt
|
|
@@ -67,20 +67,20 @@ describe('createResolveUserPrompt', () => {
|
|
|
67
67
|
assert.strictEqual(commentIdToThreadId.get('comment_2'), 'PRRT_kwDOxx_2');
|
|
68
68
|
assert.strictEqual(commentIdToThreadId.size, 2);
|
|
69
69
|
});
|
|
70
|
-
it('includes file path and line number', () => {
|
|
70
|
+
void it('includes file path and line number', () => {
|
|
71
71
|
const threads = [makeThread('t1', { path: 'src/auth.ts', line: 42 })];
|
|
72
72
|
const { prompt } = createResolveUserPrompt(threads);
|
|
73
73
|
assert.ok(prompt.includes('src/auth.ts'));
|
|
74
74
|
assert.ok(prompt.includes('42'));
|
|
75
75
|
});
|
|
76
|
-
it('includes comment body', () => {
|
|
76
|
+
void it('includes comment body', () => {
|
|
77
77
|
const threads = [
|
|
78
78
|
makeThread('t1', { body: 'This should use a const instead of let' }),
|
|
79
79
|
];
|
|
80
80
|
const { prompt } = createResolveUserPrompt(threads);
|
|
81
81
|
assert.ok(prompt.includes('This should use a const instead of let'));
|
|
82
82
|
});
|
|
83
|
-
it('includes follow-up comments', () => {
|
|
83
|
+
void it('includes follow-up comments', () => {
|
|
84
84
|
const threads = [
|
|
85
85
|
makeThread('t1', {
|
|
86
86
|
body: 'Main comment',
|
|
@@ -92,12 +92,12 @@ describe('createResolveUserPrompt', () => {
|
|
|
92
92
|
assert.ok(prompt.includes('I disagree because...'));
|
|
93
93
|
assert.ok(prompt.includes('@dev'));
|
|
94
94
|
});
|
|
95
|
-
it('includes instruction to use exact comment IDs', () => {
|
|
95
|
+
void it('includes instruction to use exact comment IDs', () => {
|
|
96
96
|
const threads = [makeThread('t1'), makeThread('t2'), makeThread('t3')];
|
|
97
97
|
const { prompt } = createResolveUserPrompt(threads);
|
|
98
98
|
assert.ok(prompt.includes('comment_1, comment_2, comment_3'));
|
|
99
99
|
});
|
|
100
|
-
it('handles threads with no comments gracefully', () => {
|
|
100
|
+
void it('handles threads with no comments gracefully', () => {
|
|
101
101
|
const emptyThread = {
|
|
102
102
|
id: 'empty',
|
|
103
103
|
isResolved: false,
|
|
@@ -108,7 +108,7 @@ describe('createResolveUserPrompt', () => {
|
|
|
108
108
|
// Empty thread should be skipped (no comment nodes to index)
|
|
109
109
|
assert.strictEqual(commentIdToThreadId.size, 0);
|
|
110
110
|
});
|
|
111
|
-
it('returns correct count in header', () => {
|
|
111
|
+
void it('returns correct count in header', () => {
|
|
112
112
|
const threads = [makeThread('t1'), makeThread('t2')];
|
|
113
113
|
const { prompt } = createResolveUserPrompt(threads);
|
|
114
114
|
assert.ok(prompt.includes('2 unresolved review comment(s)'));
|
|
@@ -45,8 +45,8 @@ function makeThread(id, body) {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
describe('resolve comment→thread mapping (integration)', () => {
|
|
49
|
-
it('correctly maps comment_ids to thread IDs', () => {
|
|
48
|
+
void describe('resolve comment→thread mapping (integration)', () => {
|
|
49
|
+
void it('correctly maps comment_ids to thread IDs', () => {
|
|
50
50
|
const threads = [
|
|
51
51
|
makeThread('PRRT_aaa', 'Use const'),
|
|
52
52
|
makeThread('PRRT_bbb', 'Add error handling'),
|
|
@@ -75,7 +75,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
75
75
|
assert.strictEqual(result.addressed[1].threadId, 'PRRT_bbb');
|
|
76
76
|
assert.strictEqual(result.skipped[0].threadId, 'PRRT_ccc');
|
|
77
77
|
});
|
|
78
|
-
it('reports errors for unknown comment_ids', () => {
|
|
78
|
+
void it('reports errors for unknown comment_ids', () => {
|
|
79
79
|
const threads = [makeThread('PRRT_aaa', 'Fix bug')];
|
|
80
80
|
const { commentIdToThreadId } = createResolveUserPrompt(threads);
|
|
81
81
|
const agentResult = [
|
|
@@ -87,7 +87,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
87
87
|
assert.strictEqual(result.errors.length, 1);
|
|
88
88
|
assert.ok(result.errors[0].includes('comment_99'));
|
|
89
89
|
});
|
|
90
|
-
it('handles agent returning partial results', () => {
|
|
90
|
+
void it('handles agent returning partial results', () => {
|
|
91
91
|
const threads = [
|
|
92
92
|
makeThread('PRRT_aaa', 'Fix A'),
|
|
93
93
|
makeThread('PRRT_bbb', 'Fix B'),
|
|
@@ -105,7 +105,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
105
105
|
assert.strictEqual(result.errors.length, 0);
|
|
106
106
|
// comment_2 was not mentioned - no error, just not processed
|
|
107
107
|
});
|
|
108
|
-
it('handles empty agent result', () => {
|
|
108
|
+
void it('handles empty agent result', () => {
|
|
109
109
|
const threads = [makeThread('PRRT_aaa', 'Fix')];
|
|
110
110
|
const { commentIdToThreadId } = createResolveUserPrompt(threads);
|
|
111
111
|
const result = processResolveResult([], commentIdToThreadId);
|
|
@@ -113,7 +113,7 @@ describe('resolve comment→thread mapping (integration)', () => {
|
|
|
113
113
|
assert.strictEqual(result.skipped.length, 0);
|
|
114
114
|
assert.strictEqual(result.errors.length, 0);
|
|
115
115
|
});
|
|
116
|
-
it('preserves reply text in all cases', () => {
|
|
116
|
+
void it('preserves reply text in all cases', () => {
|
|
117
117
|
const threads = [
|
|
118
118
|
makeThread('PRRT_aaa', 'Fix this'),
|
|
119
119
|
makeThread('PRRT_bbb', 'Change that'),
|
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { isResolveResult } from '../types.js';
|
|
4
|
-
describe('isResolveResult', () => {
|
|
5
|
-
it('returns true for valid ResolveResult', () => {
|
|
4
|
+
void describe('isResolveResult', () => {
|
|
5
|
+
void it('returns true for valid ResolveResult', () => {
|
|
6
6
|
assert.ok(isResolveResult({
|
|
7
7
|
comments: [
|
|
8
8
|
{ comment_id: 'comment_1', action: 'changed', reply: 'Fixed' },
|
|
9
9
|
],
|
|
10
10
|
}));
|
|
11
11
|
});
|
|
12
|
-
it('returns true when optional fields are present', () => {
|
|
12
|
+
void it('returns true when optional fields are present', () => {
|
|
13
13
|
assert.ok(isResolveResult({
|
|
14
14
|
comments: [],
|
|
15
15
|
files_modified: ['a.ts'],
|
|
16
16
|
summary: 'Done',
|
|
17
17
|
}));
|
|
18
18
|
});
|
|
19
|
-
it('returns true for empty comments array', () => {
|
|
19
|
+
void it('returns true for empty comments array', () => {
|
|
20
20
|
assert.ok(isResolveResult({ comments: [] }));
|
|
21
21
|
});
|
|
22
|
-
it('returns false for null', () => {
|
|
22
|
+
void it('returns false for null', () => {
|
|
23
23
|
assert.ok(!isResolveResult(null));
|
|
24
24
|
});
|
|
25
|
-
it('returns false for undefined', () => {
|
|
25
|
+
void it('returns false for undefined', () => {
|
|
26
26
|
assert.ok(!isResolveResult(undefined));
|
|
27
27
|
});
|
|
28
|
-
it('returns false for string', () => {
|
|
28
|
+
void it('returns false for string', () => {
|
|
29
29
|
assert.ok(!isResolveResult('not an object'));
|
|
30
30
|
});
|
|
31
|
-
it('returns false for number', () => {
|
|
31
|
+
void it('returns false for number', () => {
|
|
32
32
|
assert.ok(!isResolveResult(42));
|
|
33
33
|
});
|
|
34
|
-
it('returns false when comments is missing', () => {
|
|
34
|
+
void it('returns false when comments is missing', () => {
|
|
35
35
|
assert.ok(!isResolveResult({ files_modified: ['a.ts'] }));
|
|
36
36
|
});
|
|
37
|
-
it('returns false when comments is not an array', () => {
|
|
37
|
+
void it('returns false when comments is not an array', () => {
|
|
38
38
|
assert.ok(!isResolveResult({ comments: 'not-array' }));
|
|
39
39
|
});
|
|
40
|
-
it('returns false for empty object', () => {
|
|
40
|
+
void it('returns false for empty object', () => {
|
|
41
41
|
assert.ok(!isResolveResult({}));
|
|
42
42
|
});
|
|
43
43
|
});
|
|
@@ -16,8 +16,8 @@ function createTempRepo() {
|
|
|
16
16
|
execSync('git commit -m "init"', { cwd: dir, stdio: 'pipe' });
|
|
17
17
|
return dir;
|
|
18
18
|
}
|
|
19
|
-
describe('buildCredentialArgs', () => {
|
|
20
|
-
it('returns 4 args with credential helper config', () => {
|
|
19
|
+
void describe('buildCredentialArgs', () => {
|
|
20
|
+
void it('returns 4 args with credential helper config', () => {
|
|
21
21
|
const args = buildCredentialArgs('my-token');
|
|
22
22
|
assert.strictEqual(args.length, 4);
|
|
23
23
|
assert.strictEqual(args[0], '-c');
|
|
@@ -26,12 +26,12 @@ describe('buildCredentialArgs', () => {
|
|
|
26
26
|
assert.ok(args[3].includes('my-token'));
|
|
27
27
|
assert.ok(args[3].includes('x-access-token'));
|
|
28
28
|
});
|
|
29
|
-
it('escapes token in credential helper', () => {
|
|
29
|
+
void it('escapes token in credential helper', () => {
|
|
30
30
|
const args = buildCredentialArgs('token-with-special-chars!@#');
|
|
31
31
|
assert.ok(args[3].includes('token-with-special-chars!@#'));
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
-
describe('hasUncommittedChanges', () => {
|
|
34
|
+
void describe('hasUncommittedChanges', () => {
|
|
35
35
|
let repoPath;
|
|
36
36
|
beforeEach(() => {
|
|
37
37
|
repoPath = createTempRepo();
|
|
@@ -39,18 +39,18 @@ describe('hasUncommittedChanges', () => {
|
|
|
39
39
|
afterEach(() => {
|
|
40
40
|
rmSync(repoPath, { recursive: true, force: true });
|
|
41
41
|
});
|
|
42
|
-
it('returns false for clean repo', () => {
|
|
42
|
+
void it('returns false for clean repo', () => {
|
|
43
43
|
assert.strictEqual(hasUncommittedChanges(repoPath), false);
|
|
44
44
|
});
|
|
45
|
-
it('returns true after modifying a file', () => {
|
|
45
|
+
void it('returns true after modifying a file', () => {
|
|
46
46
|
writeFileSync(join(repoPath, 'README.md'), '# Modified');
|
|
47
47
|
assert.strictEqual(hasUncommittedChanges(repoPath), true);
|
|
48
48
|
});
|
|
49
|
-
it('returns true for new untracked file', () => {
|
|
49
|
+
void it('returns true for new untracked file', () => {
|
|
50
50
|
writeFileSync(join(repoPath, 'new-file.txt'), 'content');
|
|
51
51
|
assert.strictEqual(hasUncommittedChanges(repoPath), true);
|
|
52
52
|
});
|
|
53
|
-
it('returns false after staging and committing', () => {
|
|
53
|
+
void it('returns false after staging and committing', () => {
|
|
54
54
|
writeFileSync(join(repoPath, 'new.txt'), 'x');
|
|
55
55
|
execSync('git add . && git commit -m "add"', {
|
|
56
56
|
cwd: repoPath,
|
|
@@ -58,11 +58,11 @@ describe('hasUncommittedChanges', () => {
|
|
|
58
58
|
});
|
|
59
59
|
assert.strictEqual(hasUncommittedChanges(repoPath), false);
|
|
60
60
|
});
|
|
61
|
-
it('returns false for non-existent path', () => {
|
|
61
|
+
void it('returns false for non-existent path', () => {
|
|
62
62
|
assert.strictEqual(hasUncommittedChanges('/tmp/nonexistent-repo-xyz'), false);
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
-
describe('hasNewCommits', () => {
|
|
65
|
+
void describe('hasNewCommits', () => {
|
|
66
66
|
let repoPath;
|
|
67
67
|
let bareRemote;
|
|
68
68
|
beforeEach(() => {
|
|
@@ -89,10 +89,10 @@ describe('hasNewCommits', () => {
|
|
|
89
89
|
rmSync(repoPath, { recursive: true, force: true });
|
|
90
90
|
rmSync(bareRemote, { recursive: true, force: true });
|
|
91
91
|
});
|
|
92
|
-
it('returns false when HEAD matches remote', () => {
|
|
92
|
+
void it('returns false when HEAD matches remote', () => {
|
|
93
93
|
assert.strictEqual(hasNewCommits(repoPath, 'main'), false);
|
|
94
94
|
});
|
|
95
|
-
it('returns true after a local commit not pushed', () => {
|
|
95
|
+
void it('returns true after a local commit not pushed', () => {
|
|
96
96
|
writeFileSync(join(repoPath, 'new.txt'), 'hello');
|
|
97
97
|
execSync('git add . && git commit -m "local"', {
|
|
98
98
|
cwd: repoPath,
|
|
@@ -100,7 +100,7 @@ describe('hasNewCommits', () => {
|
|
|
100
100
|
});
|
|
101
101
|
assert.strictEqual(hasNewCommits(repoPath, 'main'), true);
|
|
102
102
|
});
|
|
103
|
-
it('returns false after pushing local commit', () => {
|
|
103
|
+
void it('returns false after pushing local commit', () => {
|
|
104
104
|
writeFileSync(join(repoPath, 'new.txt'), 'hello');
|
|
105
105
|
execSync('git add . && git commit -m "local" && git push origin main', {
|
|
106
106
|
cwd: repoPath,
|