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
|
@@ -9,24 +9,48 @@
|
|
|
9
9
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
10
10
|
import { createChecklistsMcpServer } from '../../commands/checklists/tools.js';
|
|
11
11
|
import { logInfo, logWarning } from '../../utils/logger.js';
|
|
12
|
-
const LEARNER_SYSTEM_PROMPT = `You are a software quality engineer. Your task is to analyse review comments that were addressed during a PR resolve and distil them into actionable checklist items
|
|
12
|
+
const LEARNER_SYSTEM_PROMPT = `You are a software quality engineer. Your task is to analyse review comments that were addressed during a PR resolve and distil them into actionable checklist items — assigned to the most appropriate phase(s) so issues are caught as early as possible.
|
|
13
|
+
|
|
14
|
+
## Available Phases (earliest → latest)
|
|
15
|
+
|
|
16
|
+
| Phase | When it runs | Good for |
|
|
17
|
+
|-------|-------------|----------|
|
|
18
|
+
| \`feature_analysis\` | Requirements gathering | Requirement gaps, scope issues, edge cases |
|
|
19
|
+
| \`user_stories_analysis\` | User story refinement | Acceptance criteria gaps, missing scenarios |
|
|
20
|
+
| \`test_cases_analysis\` | Test planning | Missing test coverage, untested edge cases |
|
|
21
|
+
| \`technical_design\` | Architecture & design | Design flaws, API contract issues, data modelling |
|
|
22
|
+
| \`branch_planning\` | Work breakdown | Dependency issues, ordering problems |
|
|
23
|
+
| \`code_implementation\` | Writing code | Coding patterns, error handling, security, performance |
|
|
24
|
+
| \`code_testing\` | Unit/integration tests | Test quality, assertion coverage, mocking issues |
|
|
25
|
+
| \`code_refine\` | Code polish | Readability, naming, documentation |
|
|
26
|
+
| \`code_review\` | Final review | Cross-cutting concerns not caught earlier |
|
|
13
27
|
|
|
14
28
|
## Workflow
|
|
15
29
|
|
|
16
30
|
1. **Read** the addressed review comments provided below.
|
|
17
|
-
2. **Query existing checklists** using \`list_checklists\`
|
|
31
|
+
2. **Query existing checklists** using \`list_checklists\` (without a phase filter) to see what already exists across all phases.
|
|
18
32
|
3. **Identify patterns** — group related comments into categories (e.g., error handling, naming, security, performance, testing).
|
|
19
|
-
4. **
|
|
20
|
-
|
|
21
|
-
- If
|
|
22
|
-
|
|
33
|
+
4. **Decide the best phase(s)** for each pattern — prefer earlier phases where the issue could have been prevented. A checklist item can belong to multiple phases if appropriate.
|
|
34
|
+
5. **Update or create checklist items**:
|
|
35
|
+
- If an existing checklist covers the category and phase, add new items to it (skip if a similar item already exists).
|
|
36
|
+
- If no suitable checklist exists, create a new one with the appropriate phases.
|
|
37
|
+
6. **Summarise** what you added or updated, and explain why you chose each phase.
|
|
38
|
+
|
|
39
|
+
## Phase Selection Guidelines
|
|
40
|
+
|
|
41
|
+
- **Shift left**: If an issue could have been prevented in an earlier phase, assign it there. For example, a missing null check found in code review should become a \`code_implementation\` checklist item.
|
|
42
|
+
- **Security & error handling** → typically \`code_implementation\`
|
|
43
|
+
- **Missing test cases or edge cases** → \`test_cases_analysis\` or \`code_testing\`
|
|
44
|
+
- **Design or architecture issues** → \`technical_design\`
|
|
45
|
+
- **Missing requirements or acceptance criteria** → \`feature_analysis\` or \`user_stories_analysis\`
|
|
46
|
+
- **Code style, readability, naming** → \`code_refine\`
|
|
47
|
+
- **Only use \`code_review\`** for cross-cutting concerns that genuinely can't be checked earlier.
|
|
23
48
|
|
|
24
49
|
## Rules
|
|
25
50
|
|
|
26
|
-
- Only create items for **genuine quality patterns** — things that should be checked
|
|
51
|
+
- Only create items for **genuine quality patterns** — things that should be checked repeatedly.
|
|
27
52
|
- Skip one-off nits, purely stylistic preferences, or context-specific fixes that won't generalise.
|
|
28
53
|
- Role: \`developer\`
|
|
29
|
-
- Phases: \`["code_review"]\`
|
|
30
54
|
- Item type: \`boolean\` (yes/no checkable)
|
|
31
55
|
- Keep item titles concise (< 80 chars). Use the description for details.
|
|
32
56
|
- Do NOT duplicate items that already exist in the checklists.
|
|
@@ -72,7 +96,7 @@ export function buildLearnerPrompt(addressedComments, unresolvedThreads, comment
|
|
|
72
96
|
sections.push('');
|
|
73
97
|
}
|
|
74
98
|
sections.push('## Instructions');
|
|
75
|
-
sections.push('Based on the patterns above, query existing
|
|
99
|
+
sections.push('Based on the patterns above, query existing checklists across all phases and create or update items in the most appropriate phase(s) to prevent these issues from recurring. Prefer earlier phases where the issue could have been caught sooner.');
|
|
76
100
|
return sections.join('\n');
|
|
77
101
|
}
|
|
78
102
|
/**
|
|
@@ -112,6 +136,7 @@ export async function learnFromReviewFeedback(input) {
|
|
|
112
136
|
if (message.type === 'result') {
|
|
113
137
|
if (message.subtype === 'success') {
|
|
114
138
|
logInfo('Checklist learning completed.');
|
|
139
|
+
// eslint-disable-next-line max-depth
|
|
115
140
|
if (verbose && message.result) {
|
|
116
141
|
logInfo(message.result);
|
|
117
142
|
}
|
|
@@ -22,6 +22,7 @@ export { isResolveResult } from './types.js';
|
|
|
22
22
|
/**
|
|
23
23
|
* Resolve PR change requests: evaluate each comment, fix or explain.
|
|
24
24
|
*/
|
|
25
|
+
// eslint-disable-next-line complexity
|
|
25
26
|
export async function resolveStandalonePR(options) {
|
|
26
27
|
const { pullRequestUrl, githubToken, owner, repo, verbose, prId } = options;
|
|
27
28
|
logInfo(`Starting PR resolve: ${pullRequestUrl}`);
|
|
@@ -69,9 +70,17 @@ export async function resolveStandalonePR(options) {
|
|
|
69
70
|
// For fork PRs, pass fallback info so prepareWorkspace can fetch the PR ref
|
|
70
71
|
// from upstream if the fork branch is unavailable.
|
|
71
72
|
// For deleted forks, clone upstream directly and always use PR ref fallback.
|
|
72
|
-
const repoPath = prepareWorkspace(
|
|
73
|
-
|
|
74
|
-
:
|
|
73
|
+
const repoPath = prepareWorkspace({
|
|
74
|
+
owner: cloneOwner,
|
|
75
|
+
repo: cloneRepo,
|
|
76
|
+
headRef,
|
|
77
|
+
prNumber: prInfo.prNumber,
|
|
78
|
+
token: githubToken,
|
|
79
|
+
verbose,
|
|
80
|
+
forkFallback: isFork
|
|
81
|
+
? { upstreamOwner: owner, upstreamRepo: repo }
|
|
82
|
+
: undefined,
|
|
83
|
+
});
|
|
75
84
|
try {
|
|
76
85
|
// Run Claude Agent SDK to evaluate and fix comments
|
|
77
86
|
const systemPrompt = createResolveSystemPrompt();
|
|
@@ -104,6 +113,7 @@ export async function resolveStandalonePR(options) {
|
|
|
104
113
|
logInfo('Agent completed, parsing results...');
|
|
105
114
|
const responseText = message.result || lastAssistantResponse;
|
|
106
115
|
const parsed = tryExtractResult(responseText, 'resolve_result');
|
|
116
|
+
// eslint-disable-next-line max-depth
|
|
107
117
|
if (isResolveResult(parsed)) {
|
|
108
118
|
resolveResult = parsed;
|
|
109
119
|
}
|
|
@@ -114,17 +124,19 @@ export async function resolveStandalonePR(options) {
|
|
|
114
124
|
else {
|
|
115
125
|
logError(`Agent incomplete: ${message.subtype}`);
|
|
116
126
|
// Try to salvage partial results from last response
|
|
127
|
+
// eslint-disable-next-line max-depth
|
|
117
128
|
if (lastAssistantResponse) {
|
|
118
129
|
const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
|
|
130
|
+
// eslint-disable-next-line max-depth
|
|
119
131
|
if (isResolveResult(salvaged)) {
|
|
120
132
|
resolveResult = salvaged;
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
|
-
//
|
|
137
|
+
// Fallback: commit any leftover uncommitted changes the agent didn't commit itself
|
|
126
138
|
if (hasUncommittedChanges(repoPath)) {
|
|
127
|
-
logInfo('Committing changes...');
|
|
139
|
+
logInfo('Committing remaining uncommitted changes...');
|
|
128
140
|
execSync('git add -A', { cwd: repoPath, stdio: 'pipe' });
|
|
129
141
|
execSync('git commit -m "Resolve PR review comments\n\nAutomated resolution by Edsger AI"', { cwd: repoPath, stdio: 'pipe' });
|
|
130
142
|
}
|
|
@@ -142,13 +154,16 @@ export async function resolveStandalonePR(options) {
|
|
|
142
154
|
for (const comment of comments) {
|
|
143
155
|
// Map comment_id back to real GraphQL thread ID
|
|
144
156
|
const threadId = commentIdToThreadId.get(comment.comment_id);
|
|
157
|
+
// eslint-disable-next-line max-depth
|
|
145
158
|
if (!threadId) {
|
|
146
159
|
logError(`Unknown comment_id "${comment.comment_id}", skipping reply`);
|
|
147
160
|
threadsErrored++;
|
|
148
161
|
continue;
|
|
149
162
|
}
|
|
163
|
+
// eslint-disable-next-line max-depth
|
|
150
164
|
try {
|
|
151
165
|
const replied = await replyToReviewThread(octokit, threadId, comment.reply, verbose);
|
|
166
|
+
// eslint-disable-next-line max-depth
|
|
152
167
|
if (replied && comment.action === 'changed') {
|
|
153
168
|
// Resolve the thread since the change was made
|
|
154
169
|
await resolveReviewThread(octokit, threadId, verbose);
|
|
@@ -178,6 +193,7 @@ export async function resolveStandalonePR(options) {
|
|
|
178
193
|
? 'Changes were made to address review feedback. Please re-review.'
|
|
179
194
|
: 'Reviewed this comment. No changes were made at this time.';
|
|
180
195
|
const replied = await replyToReviewThread(octokit, thread.id, genericReply, verbose);
|
|
196
|
+
// eslint-disable-next-line max-depth
|
|
181
197
|
if (replied) {
|
|
182
198
|
threadsSkipped++;
|
|
183
199
|
}
|
|
@@ -212,6 +228,14 @@ export async function resolveStandalonePR(options) {
|
|
|
212
228
|
// Non-critical
|
|
213
229
|
}
|
|
214
230
|
}
|
|
231
|
+
// Clean up workspace on success
|
|
232
|
+
try {
|
|
233
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
234
|
+
logInfo(`Cleaned up workspace: ${repoPath}`);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// ignore cleanup errors
|
|
238
|
+
}
|
|
215
239
|
return {
|
|
216
240
|
status: 'success',
|
|
217
241
|
message: `Resolved ${threadsAddressed} threads, skipped ${threadsSkipped}, ${threadsErrored} errors`,
|
|
@@ -228,14 +252,6 @@ export async function resolveStandalonePR(options) {
|
|
|
228
252
|
logInfo(`Workspace preserved for inspection: ${repoPath}`);
|
|
229
253
|
throw innerError;
|
|
230
254
|
}
|
|
231
|
-
// Only clean up on success
|
|
232
|
-
try {
|
|
233
|
-
rmSync(repoPath, { recursive: true, force: true });
|
|
234
|
-
logInfo(`Cleaned up workspace: ${repoPath}`);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// ignore cleanup errors
|
|
238
|
-
}
|
|
239
255
|
}
|
|
240
256
|
catch (error) {
|
|
241
257
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -27,7 +27,8 @@ export function createResolveSystemPrompt() {
|
|
|
27
27
|
2. For each comment, examine the relevant code
|
|
28
28
|
3. If you agree: make the change in the file
|
|
29
29
|
4. If you disagree: skip it (do NOT modify the file for that comment)
|
|
30
|
-
5. After
|
|
30
|
+
5. After making all changes, commit them with a descriptive message summarizing what was resolved (do NOT push)
|
|
31
|
+
6. After committing, output a JSON summary
|
|
31
32
|
|
|
32
33
|
**CRITICAL - Result Format**:
|
|
33
34
|
After making all changes, you MUST output a JSON result. Use the exact comment_id from each comment (comment_1, comment_2, etc.):
|
|
@@ -8,19 +8,29 @@
|
|
|
8
8
|
export declare function buildCredentialArgs(token: string): string[];
|
|
9
9
|
/**
|
|
10
10
|
* Get the workspace path for a PR resolve operation.
|
|
11
|
+
* Includes owner/repo to avoid collisions when resolving PRs from different repos.
|
|
11
12
|
*/
|
|
12
|
-
export declare function getResolveWorkspacePath(prNumber: number): string;
|
|
13
|
+
export declare function getResolveWorkspacePath(owner: string, repo: string, prNumber: number): string;
|
|
13
14
|
export interface ForkFallbackInfo {
|
|
14
15
|
upstreamOwner: string;
|
|
15
16
|
upstreamRepo: string;
|
|
16
17
|
}
|
|
18
|
+
export interface PrepareWorkspaceOptions {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
headRef: string;
|
|
22
|
+
prNumber: number;
|
|
23
|
+
token: string;
|
|
24
|
+
verbose?: boolean;
|
|
25
|
+
forkFallback?: ForkFallbackInfo;
|
|
26
|
+
}
|
|
17
27
|
/**
|
|
18
28
|
* Clone or reuse a repo for PR resolve.
|
|
19
29
|
* For fork PRs, clones from the fork repo. If the fork branch is unavailable,
|
|
20
30
|
* falls back to fetching the PR ref from the upstream repo.
|
|
21
31
|
* Returns the workspace path.
|
|
22
32
|
*/
|
|
23
|
-
export declare function prepareWorkspace(
|
|
33
|
+
export declare function prepareWorkspace(options: PrepareWorkspaceOptions): string;
|
|
24
34
|
/**
|
|
25
35
|
* Push changes from workspace back to remote.
|
|
26
36
|
*/
|
|
@@ -21,9 +21,10 @@ export function buildCredentialArgs(token) {
|
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Get the workspace path for a PR resolve operation.
|
|
24
|
+
* Includes owner/repo to avoid collisions when resolving PRs from different repos.
|
|
24
25
|
*/
|
|
25
|
-
export function getResolveWorkspacePath(prNumber) {
|
|
26
|
-
return join(homedir(), 'edsger', `pr-resolve-${prNumber}`);
|
|
26
|
+
export function getResolveWorkspacePath(owner, repo, prNumber) {
|
|
27
|
+
return join(homedir(), 'edsger', `pr-resolve-${owner}-${repo}-${prNumber}`);
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Clone or reuse a repo for PR resolve.
|
|
@@ -31,8 +32,9 @@ export function getResolveWorkspacePath(prNumber) {
|
|
|
31
32
|
* falls back to fetching the PR ref from the upstream repo.
|
|
32
33
|
* Returns the workspace path.
|
|
33
34
|
*/
|
|
34
|
-
export function prepareWorkspace(
|
|
35
|
-
const
|
|
35
|
+
export function prepareWorkspace(options) {
|
|
36
|
+
const { owner, repo, headRef, prNumber, token, verbose, forkFallback } = options;
|
|
37
|
+
const repoPath = getResolveWorkspacePath(owner, repo, prNumber);
|
|
36
38
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
37
39
|
const gitCredentialArgs = buildCredentialArgs(token);
|
|
38
40
|
if (existsSync(join(repoPath, '.git'))) {
|
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, } from '../prompts.js';
|
|
4
|
-
describe('createStandaloneReviewSystemPrompt', () => {
|
|
5
|
-
it('includes review focus areas', () => {
|
|
4
|
+
void describe('createStandaloneReviewSystemPrompt', () => {
|
|
5
|
+
void it('includes review focus areas', () => {
|
|
6
6
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
7
7
|
assert.ok(prompt.includes('Code Quality'));
|
|
8
8
|
assert.ok(prompt.includes('Security'));
|
|
9
9
|
assert.ok(prompt.includes('Performance'));
|
|
10
10
|
});
|
|
11
|
-
it('specifies JSON result format', () => {
|
|
11
|
+
void it('specifies JSON result format', () => {
|
|
12
12
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
13
13
|
assert.ok(prompt.includes('review_result'));
|
|
14
14
|
assert.ok(prompt.includes('"file"'));
|
|
15
15
|
assert.ok(prompt.includes('"line"'));
|
|
16
16
|
assert.ok(prompt.includes('"comment"'));
|
|
17
17
|
});
|
|
18
|
-
it('includes assessment options', () => {
|
|
18
|
+
void it('includes assessment options', () => {
|
|
19
19
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
20
20
|
assert.ok(prompt.includes('APPROVE'));
|
|
21
21
|
assert.ok(prompt.includes('REQUEST_CHANGES'));
|
|
22
22
|
assert.ok(prompt.includes('COMMENT'));
|
|
23
23
|
});
|
|
24
|
-
it('does not include feature/checklist instructions', () => {
|
|
24
|
+
void it('does not include feature/checklist instructions', () => {
|
|
25
25
|
const prompt = createStandaloneReviewSystemPrompt();
|
|
26
26
|
assert.ok(!prompt.includes('checklist'));
|
|
27
27
|
assert.ok(!prompt.includes('feature_id'));
|
|
28
28
|
assert.ok(!prompt.includes('user stories'));
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
|
-
describe('createStandaloneReviewUserPrompt', () => {
|
|
32
|
-
it('includes context info in the prompt', () => {
|
|
31
|
+
void describe('createStandaloneReviewUserPrompt', () => {
|
|
32
|
+
void it('includes context info in the prompt', () => {
|
|
33
33
|
const contextInfo = '# Pull Request\n**Title**: Fix auth bug\n';
|
|
34
34
|
const prompt = createStandaloneReviewUserPrompt(contextInfo);
|
|
35
35
|
assert.ok(prompt.includes('Fix auth bug'));
|
|
36
36
|
});
|
|
37
|
-
it('includes review instructions', () => {
|
|
37
|
+
void it('includes review instructions', () => {
|
|
38
38
|
const prompt = createStandaloneReviewUserPrompt('some context');
|
|
39
39
|
assert.ok(prompt.includes('Analyze Each File'));
|
|
40
40
|
assert.ok(prompt.includes('Identify Issues'));
|
|
41
41
|
assert.ok(prompt.includes('Actionable Feedback'));
|
|
42
42
|
});
|
|
43
|
-
it('mentions severity categories', () => {
|
|
43
|
+
void it('mentions severity categories', () => {
|
|
44
44
|
const prompt = createStandaloneReviewUserPrompt('context');
|
|
45
45
|
assert.ok(prompt.includes('Critical'));
|
|
46
46
|
assert.ok(prompt.includes('Major'));
|
|
@@ -36,7 +36,7 @@ function mapCommentsToReviewPayload(agentComments, files) {
|
|
|
36
36
|
}
|
|
37
37
|
return result;
|
|
38
38
|
}
|
|
39
|
-
describe('review comment mapping (integration)', () => {
|
|
39
|
+
void describe('review comment mapping (integration)', () => {
|
|
40
40
|
const samplePatch = `@@ -1,5 +1,7 @@
|
|
41
41
|
import { useState } from 'react'
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
47
47
|
+ const [count, setCount] = useState<number>(0)
|
|
48
48
|
return <div>{count}</div>`;
|
|
49
49
|
const files = [{ filename: 'src/App.tsx', patch: samplePatch }];
|
|
50
|
-
it('maps exact line comments to correct diff positions', () => {
|
|
50
|
+
void it('maps exact line comments to correct diff positions', () => {
|
|
51
51
|
const comments = [
|
|
52
52
|
{
|
|
53
53
|
file: 'src/App.tsx',
|
|
@@ -63,7 +63,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
63
63
|
// No "Note" prefix since line was exact
|
|
64
64
|
assert.ok(!result[0].body.includes('**Note**'));
|
|
65
65
|
});
|
|
66
|
-
it('adjusts line numbers when exact line not in diff', () => {
|
|
66
|
+
void it('adjusts line numbers when exact line not in diff', () => {
|
|
67
67
|
// Line 2 is blank (in diff) but line 100 is way outside
|
|
68
68
|
const comments = [{ file: 'src/App.tsx', line: 8, comment: 'Fix this' }];
|
|
69
69
|
const result = mapCommentsToReviewPayload(comments, files);
|
|
@@ -73,7 +73,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
73
73
|
}
|
|
74
74
|
// If no match within range, it's filtered out - also valid
|
|
75
75
|
});
|
|
76
|
-
it('filters out comments for files not in diff', () => {
|
|
76
|
+
void it('filters out comments for files not in diff', () => {
|
|
77
77
|
const comments = [
|
|
78
78
|
{
|
|
79
79
|
file: 'src/other.ts',
|
|
@@ -84,7 +84,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
84
84
|
const result = mapCommentsToReviewPayload(comments, files);
|
|
85
85
|
assert.strictEqual(result.length, 0);
|
|
86
86
|
});
|
|
87
|
-
it('handles multiple comments on same file', () => {
|
|
87
|
+
void it('handles multiple comments on same file', () => {
|
|
88
88
|
const comments = [
|
|
89
89
|
{ file: 'src/App.tsx', line: 3, comment: 'Comment A' },
|
|
90
90
|
{ file: 'src/App.tsx', line: 6, comment: 'Comment B' },
|
|
@@ -94,7 +94,7 @@ describe('review comment mapping (integration)', () => {
|
|
|
94
94
|
assert.ok(result[0].body.includes('Comment A'));
|
|
95
95
|
assert.ok(result[1].body.includes('Comment B'));
|
|
96
96
|
});
|
|
97
|
-
it('handles files with no patch (binary files)', () => {
|
|
97
|
+
void it('handles files with no patch (binary files)', () => {
|
|
98
98
|
const binaryFiles = [
|
|
99
99
|
{ filename: 'image.png' }, // no patch
|
|
100
100
|
{ filename: 'src/App.tsx', patch: samplePatch },
|
|
@@ -14,6 +14,7 @@ import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, }
|
|
|
14
14
|
/**
|
|
15
15
|
* Review a standalone PR and post comments to GitHub.
|
|
16
16
|
*/
|
|
17
|
+
// eslint-disable-next-line complexity
|
|
17
18
|
export async function reviewStandalonePR(options) {
|
|
18
19
|
const { pullRequestUrl, githubToken, verbose, prId } = options;
|
|
19
20
|
logInfo(`Starting standalone PR review: ${pullRequestUrl}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { extractTextFromContent, tryExtractResult, tryParseJsonFromResponse, userMessage, } from '../agent-utils.js';
|
|
4
|
-
describe('userMessage', () => {
|
|
5
|
-
it('creates a user message object', () => {
|
|
4
|
+
void describe('userMessage', () => {
|
|
5
|
+
void it('creates a user message object', () => {
|
|
6
6
|
const msg = userMessage('hello');
|
|
7
7
|
assert.deepStrictEqual(msg, {
|
|
8
8
|
type: 'user',
|
|
@@ -10,8 +10,8 @@ describe('userMessage', () => {
|
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
|
-
describe('extractTextFromContent', () => {
|
|
14
|
-
it('extracts text items from content array', () => {
|
|
13
|
+
void describe('extractTextFromContent', () => {
|
|
14
|
+
void it('extracts text items from content array', () => {
|
|
15
15
|
const content = [
|
|
16
16
|
{ type: 'text', text: 'hello' },
|
|
17
17
|
{ type: 'tool_use', id: '123', name: 'read', input: {} },
|
|
@@ -20,35 +20,35 @@ describe('extractTextFromContent', () => {
|
|
|
20
20
|
const result = extractTextFromContent(content);
|
|
21
21
|
assert.strictEqual(result, 'hello\nworld\n');
|
|
22
22
|
});
|
|
23
|
-
it('returns empty string for no text items', () => {
|
|
23
|
+
void it('returns empty string for no text items', () => {
|
|
24
24
|
const content = [{ type: 'tool_use', id: '123', name: 'read', input: {} }];
|
|
25
25
|
const result = extractTextFromContent(content);
|
|
26
26
|
assert.strictEqual(result, '');
|
|
27
27
|
});
|
|
28
|
-
it('returns empty string for empty array', () => {
|
|
28
|
+
void it('returns empty string for empty array', () => {
|
|
29
29
|
assert.strictEqual(extractTextFromContent([]), '');
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
-
describe('tryParseJsonFromResponse', () => {
|
|
33
|
-
it('parses JSON from code block', () => {
|
|
32
|
+
void describe('tryParseJsonFromResponse', () => {
|
|
33
|
+
void it('parses JSON from code block', () => {
|
|
34
34
|
const text = 'Here is the result:\n```json\n{"key": "value"}\n```\nDone.';
|
|
35
35
|
const result = tryParseJsonFromResponse(text);
|
|
36
36
|
assert.deepStrictEqual(result, { key: 'value' });
|
|
37
37
|
});
|
|
38
|
-
it('parses raw JSON', () => {
|
|
38
|
+
void it('parses raw JSON', () => {
|
|
39
39
|
const text = '{"key": "value"}';
|
|
40
40
|
const result = tryParseJsonFromResponse(text);
|
|
41
41
|
assert.deepStrictEqual(result, { key: 'value' });
|
|
42
42
|
});
|
|
43
|
-
it('returns null for invalid JSON', () => {
|
|
43
|
+
void it('returns null for invalid JSON', () => {
|
|
44
44
|
const result = tryParseJsonFromResponse('not json at all');
|
|
45
45
|
assert.strictEqual(result, null);
|
|
46
46
|
});
|
|
47
|
-
it('returns null for empty string', () => {
|
|
47
|
+
void it('returns null for empty string', () => {
|
|
48
48
|
const result = tryParseJsonFromResponse('');
|
|
49
49
|
assert.strictEqual(result, null);
|
|
50
50
|
});
|
|
51
|
-
it('parses JSON block with surrounding text', () => {
|
|
51
|
+
void it('parses JSON block with surrounding text', () => {
|
|
52
52
|
const text = `I analyzed the code and here are my findings:
|
|
53
53
|
|
|
54
54
|
\`\`\`json
|
|
@@ -66,22 +66,22 @@ That's my review.`;
|
|
|
66
66
|
assert.ok(result.review_result);
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
|
-
describe('tryExtractResult', () => {
|
|
70
|
-
it('extracts keyed result from JSON code block', () => {
|
|
69
|
+
void describe('tryExtractResult', () => {
|
|
70
|
+
void it('extracts keyed result from JSON code block', () => {
|
|
71
71
|
const text = '```json\n{"review_result": {"summary": "good"}}\n```';
|
|
72
72
|
const result = tryExtractResult(text, 'review_result');
|
|
73
73
|
assert.deepStrictEqual(result, { summary: 'good' });
|
|
74
74
|
});
|
|
75
|
-
it('returns whole object if key not found but JSON valid', () => {
|
|
75
|
+
void it('returns whole object if key not found but JSON valid', () => {
|
|
76
76
|
const text = '{"summary": "good", "comments": []}';
|
|
77
77
|
const result = tryExtractResult(text, 'review_result');
|
|
78
78
|
assert.deepStrictEqual(result, { summary: 'good', comments: [] });
|
|
79
79
|
});
|
|
80
|
-
it('returns null for unparseable text', () => {
|
|
80
|
+
void it('returns null for unparseable text', () => {
|
|
81
81
|
const result = tryExtractResult('no json here', 'review_result');
|
|
82
82
|
assert.strictEqual(result, null);
|
|
83
83
|
});
|
|
84
|
-
it('extracts resolve_result key', () => {
|
|
84
|
+
void it('extracts resolve_result key', () => {
|
|
85
85
|
const text = '```json\n{"resolve_result": {"comments": [{"comment_id": "comment_1", "action": "changed", "reply": "fixed"}]}}\n```';
|
|
86
86
|
const result = tryExtractResult(text, 'resolve_result');
|
|
87
87
|
assert.ok(result);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import { formatStandalonePRContextForPrompt, parsePullRequestUrl, } from '../context.js';
|
|
4
|
-
describe('parsePullRequestUrl (re-exported)', () => {
|
|
5
|
-
it('parses a standard GitHub PR URL', () => {
|
|
4
|
+
void describe('parsePullRequestUrl (re-exported)', () => {
|
|
5
|
+
void it('parses a standard GitHub PR URL', () => {
|
|
6
6
|
const result = parsePullRequestUrl('https://github.com/owner/repo/pull/123');
|
|
7
7
|
assert.deepStrictEqual(result, {
|
|
8
8
|
owner: 'owner',
|
|
@@ -10,7 +10,7 @@ describe('parsePullRequestUrl (re-exported)', () => {
|
|
|
10
10
|
prNumber: 123,
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
|
-
it('parses URL with trailing path segments', () => {
|
|
13
|
+
void it('parses URL with trailing path segments', () => {
|
|
14
14
|
const result = parsePullRequestUrl('https://github.com/my-org/my-repo/pull/456/files');
|
|
15
15
|
assert.deepStrictEqual(result, {
|
|
16
16
|
owner: 'my-org',
|
|
@@ -18,14 +18,14 @@ describe('parsePullRequestUrl (re-exported)', () => {
|
|
|
18
18
|
prNumber: 456,
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
|
-
it('returns null for non-PR URL', () => {
|
|
21
|
+
void it('returns null for non-PR URL', () => {
|
|
22
22
|
assert.strictEqual(parsePullRequestUrl('https://github.com/owner/repo/issues/1'), null);
|
|
23
23
|
});
|
|
24
|
-
it('returns null for non-GitHub URL', () => {
|
|
24
|
+
void it('returns null for non-GitHub URL', () => {
|
|
25
25
|
assert.strictEqual(parsePullRequestUrl('https://example.com/pull/1'), null);
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
-
describe('formatStandalonePRContextForPrompt', () => {
|
|
28
|
+
void describe('formatStandalonePRContextForPrompt', () => {
|
|
29
29
|
const context = {
|
|
30
30
|
pullRequestUrl: 'https://github.com/owner/repo/pull/42',
|
|
31
31
|
pullRequestNumber: 42,
|
|
@@ -61,33 +61,33 @@ describe('formatStandalonePRContextForPrompt', () => {
|
|
|
61
61
|
},
|
|
62
62
|
],
|
|
63
63
|
};
|
|
64
|
-
it('includes PR URL and number', () => {
|
|
64
|
+
void it('includes PR URL and number', () => {
|
|
65
65
|
const output = formatStandalonePRContextForPrompt(context);
|
|
66
66
|
assert.ok(output.includes('#42'));
|
|
67
67
|
assert.ok(output.includes('github.com/owner/repo/pull/42'));
|
|
68
68
|
});
|
|
69
|
-
it('includes PR title and author', () => {
|
|
69
|
+
void it('includes PR title and author', () => {
|
|
70
70
|
const output = formatStandalonePRContextForPrompt(context);
|
|
71
71
|
assert.ok(output.includes('Fix bug in auth'));
|
|
72
72
|
assert.ok(output.includes('@testuser'));
|
|
73
73
|
});
|
|
74
|
-
it('includes branch info', () => {
|
|
74
|
+
void it('includes branch info', () => {
|
|
75
75
|
const output = formatStandalonePRContextForPrompt(context);
|
|
76
76
|
assert.ok(output.includes('fix/auth'));
|
|
77
77
|
assert.ok(output.includes('main'));
|
|
78
78
|
});
|
|
79
|
-
it('includes file diff', () => {
|
|
79
|
+
void it('includes file diff', () => {
|
|
80
80
|
const output = formatStandalonePRContextForPrompt(context);
|
|
81
81
|
assert.ok(output.includes('src/auth.ts'));
|
|
82
82
|
assert.ok(output.includes('+added'));
|
|
83
83
|
assert.ok(output.includes('+5 -2'));
|
|
84
84
|
});
|
|
85
|
-
it('includes commit info', () => {
|
|
85
|
+
void it('includes commit info', () => {
|
|
86
86
|
const output = formatStandalonePRContextForPrompt(context);
|
|
87
87
|
assert.ok(output.includes('abc1234'));
|
|
88
88
|
assert.ok(output.includes('fix: auth bug'));
|
|
89
89
|
});
|
|
90
|
-
it('includes PR body/description', () => {
|
|
90
|
+
void it('includes PR body/description', () => {
|
|
91
91
|
const output = formatStandalonePRContextForPrompt(context);
|
|
92
92
|
assert.ok(output.includes('This fixes the login issue'));
|
|
93
93
|
});
|
|
@@ -155,6 +155,9 @@ export function getTransitiveDependencies(file, graph) {
|
|
|
155
155
|
const stack = [file];
|
|
156
156
|
while (stack.length > 0) {
|
|
157
157
|
const current = stack.pop();
|
|
158
|
+
if (current === undefined) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
158
161
|
const deps = graph.get(current);
|
|
159
162
|
if (!deps) {
|
|
160
163
|
continue;
|
|
@@ -169,11 +172,8 @@ export function getTransitiveDependencies(file, graph) {
|
|
|
169
172
|
return result;
|
|
170
173
|
}
|
|
171
174
|
const MAX_FIX_ITERATIONS = 100;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* Returns true if a file was moved.
|
|
175
|
-
*/
|
|
176
|
-
function moveDepToEarlierPR(dep, prIdx, sorted, fileToPRIndex, movedFiles, reason) {
|
|
175
|
+
function moveDepToEarlierPR(options) {
|
|
176
|
+
const { dep, prIdx, sorted, fileToPRIndex, movedFiles, reason } = options;
|
|
177
177
|
const depPRIdx = fileToPRIndex.get(dep);
|
|
178
178
|
if (depPRIdx === undefined || depPRIdx <= prIdx) {
|
|
179
179
|
return false;
|
|
@@ -229,7 +229,15 @@ export function autoFixPROrdering(pullRequests, dependencyGraph) {
|
|
|
229
229
|
for (const file of sorted[prIdx].files ?? []) {
|
|
230
230
|
const transitiveDeps = getTransitiveDependencies(file.path, dependencyGraph);
|
|
231
231
|
for (const dep of transitiveDeps) {
|
|
232
|
-
const moved = moveDepToEarlierPR(
|
|
232
|
+
const moved = moveDepToEarlierPR({
|
|
233
|
+
dep,
|
|
234
|
+
prIdx,
|
|
235
|
+
sorted,
|
|
236
|
+
fileToPRIndex,
|
|
237
|
+
movedFiles,
|
|
238
|
+
reason: `imported by ${file.path}`,
|
|
239
|
+
});
|
|
240
|
+
// eslint-disable-next-line max-depth
|
|
233
241
|
if (moved) {
|
|
234
242
|
changed = true;
|
|
235
243
|
}
|
|
@@ -27,7 +27,9 @@ async function* prompt(analysisPrompt) {
|
|
|
27
27
|
* then uses AI to produce a PR split plan saved to the database.
|
|
28
28
|
* Human review is expected before running the pr-execution phase.
|
|
29
29
|
*/
|
|
30
|
-
export const splitFeatureIntoPRs = async (options, config
|
|
30
|
+
export const splitFeatureIntoPRs = async (options, config
|
|
31
|
+
// eslint-disable-next-line complexity
|
|
32
|
+
) => {
|
|
31
33
|
const { featureId, verbose, replaceExisting } = options;
|
|
32
34
|
if (verbose) {
|
|
33
35
|
logInfo(`Starting PR splitting for feature ID: ${featureId}`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for smoke-test github helpers.
|
|
3
|
+
*
|
|
4
|
+
* Covers the pure functions that shape GitHub compare data into a
|
|
5
|
+
* prompt-ready digest. Network-facing functions (fetchLatestTwoReleases,
|
|
6
|
+
* fetchCompare) are exercised only indirectly — their output shape is
|
|
7
|
+
* fed through buildDiffDigest / summariseStats here.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|