pi-pr-review 1.10.4 → 1.10.5
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/CHANGELOG.md +7 -0
- package/package.json +24 -2
- package/.github/workflows/pull-request.yml +0 -55
- package/.github/workflows/release-please.yml +0 -105
- package/.release-please-manifest.json +0 -3
- package/RELEASING.md +0 -29
- package/release-please-config.json +0 -24
- package/scripts/verify-release-version.mjs +0 -66
- package/tests/pr-review-concurrency.test.ts +0 -45
- package/tests/pr-review-context.test.ts +0 -66
- package/tests/pr-review-extension-lifecycle.test.ts +0 -696
- package/tests/pr-review-focus-view.test.ts +0 -235
- package/tests/pr-review-focus.test.ts +0 -250
- package/tests/pr-review-loop.test.ts +0 -167
- package/tests/pr-review-policy.test.ts +0 -95
- package/tests/pr-review-prompt.test.ts +0 -207
- package/tests/pr-review-publish.test.ts +0 -804
- package/tests/pr-review-registration.test.ts +0 -157
- package/tests/pr-review-subprocess.test.ts +0 -440
- package/tests/pr-review-telemetry.test.ts +0 -179
- package/tests/pr-review-thinking.test.ts +0 -83
- package/tests/pr-review-tool-gate.test.ts +0 -125
- package/tests/pr-review-verify.test.ts +0 -826
- package/tests/pr-self-review.test.ts +0 -342
- package/tests/release-version.test.ts +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.5](https://github.com/10ego/pi-pr-review/compare/v1.10.4...v1.10.5) (2026-07-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **release:** harden Node 24 automation ([#32](https://github.com/10ego/pi-pr-review/issues/32)) ([6b670b3](https://github.com/10ego/pi-pr-review/commit/6b670b3a1e33f3f2392027ae79c159afd9738690))
|
|
9
|
+
|
|
3
10
|
## [1.10.4](https://github.com/10ego/pi-pr-review/compare/v1.10.3...v1.10.4) (2026-07-15)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
4
4
|
"description": "Parallel AI code review for GitHub pull requests in the Pi coding agent, with model-agnostic tiered subagents, structured findings, optional verification, and safe COMMENT-only publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,9 +30,31 @@
|
|
|
30
30
|
"type": "git",
|
|
31
31
|
"url": "git+https://github.com/10ego/pi-pr-review.git"
|
|
32
32
|
},
|
|
33
|
+
"homepage": "https://github.com/10ego/pi-pr-review#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/10ego/pi-pr-review/issues"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"README.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"extensions/",
|
|
44
|
+
"lib/",
|
|
45
|
+
"prompts/"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org/",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
33
52
|
"scripts": {
|
|
34
53
|
"test": "bun test",
|
|
35
|
-
"
|
|
54
|
+
"test:tooling": "node --test tests/tooling/compare-semver.node.mjs tests/tooling/package-contents.node.mjs tests/tooling/workflows.node.mjs",
|
|
55
|
+
"verify:release-version": "node scripts/verify-release-version.mjs",
|
|
56
|
+
"verify:package": "node scripts/verify-package-contents.mjs",
|
|
57
|
+
"verify:workflows": "node scripts/verify-workflows.mjs"
|
|
36
58
|
},
|
|
37
59
|
"peerDependencies": {
|
|
38
60
|
"@earendil-works/pi-ai": "*",
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
name: Pull Request
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
types:
|
|
6
|
-
- opened
|
|
7
|
-
- edited
|
|
8
|
-
- synchronize
|
|
9
|
-
- reopened
|
|
10
|
-
- ready_for_review
|
|
11
|
-
|
|
12
|
-
permissions:
|
|
13
|
-
contents: read
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
title:
|
|
17
|
-
name: Validate PR title
|
|
18
|
-
runs-on: ubuntu-latest
|
|
19
|
-
timeout-minutes: 5
|
|
20
|
-
steps:
|
|
21
|
-
- name: Check Conventional Commit format
|
|
22
|
-
env:
|
|
23
|
-
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
24
|
-
run: |
|
|
25
|
-
node <<'NODE'
|
|
26
|
-
const title = process.env.PR_TITLE ?? "";
|
|
27
|
-
const conventionalTitle = /^(feat|fix|perf|revert|docs|style|refactor|test|chore|release)(\([a-z0-9][a-z0-9._/-]*\))?!?: \S.*$/;
|
|
28
|
-
|
|
29
|
-
if (!conventionalTitle.test(title)) {
|
|
30
|
-
console.error(`Invalid PR title: ${title}`);
|
|
31
|
-
console.error("Use: <type>(optional-scope): description");
|
|
32
|
-
console.error("Allowed types: feat, fix, perf, revert, docs, style, refactor, test, chore, release");
|
|
33
|
-
console.error("Add ! before : for a breaking change, for example feat!: description");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
NODE
|
|
37
|
-
|
|
38
|
-
test:
|
|
39
|
-
name: Test
|
|
40
|
-
runs-on: ubuntu-latest
|
|
41
|
-
timeout-minutes: 20
|
|
42
|
-
steps:
|
|
43
|
-
- name: Check out repository
|
|
44
|
-
uses: actions/checkout@v4
|
|
45
|
-
|
|
46
|
-
- name: Verify release version metadata
|
|
47
|
-
run: npm run verify:release-version
|
|
48
|
-
|
|
49
|
-
- name: Set up Bun
|
|
50
|
-
uses: oven-sh/setup-bun@v2
|
|
51
|
-
with:
|
|
52
|
-
bun-version: 1.2.15
|
|
53
|
-
|
|
54
|
-
- name: Run tests
|
|
55
|
-
run: bun test
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
name: Release Please
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
concurrency:
|
|
10
|
-
group: release-please-${{ github.ref }}
|
|
11
|
-
cancel-in-progress: false
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
release:
|
|
15
|
-
name: Create release PR or GitHub release
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
timeout-minutes: 10
|
|
18
|
-
permissions:
|
|
19
|
-
contents: read
|
|
20
|
-
outputs:
|
|
21
|
-
release_created: ${{ steps.release.outputs.release_created }}
|
|
22
|
-
release_version: ${{ steps.release.outputs.version }}
|
|
23
|
-
steps:
|
|
24
|
-
- name: Create nerv-ops installation token
|
|
25
|
-
id: app-token
|
|
26
|
-
uses: actions/create-github-app-token@v2
|
|
27
|
-
with:
|
|
28
|
-
app-id: ${{ vars.NERV_OPS_APP_ID }}
|
|
29
|
-
private-key: ${{ secrets.NERV_OPS_PRIVATE_KEY }}
|
|
30
|
-
|
|
31
|
-
- name: Run Release Please
|
|
32
|
-
id: release
|
|
33
|
-
uses: googleapis/release-please-action@v4
|
|
34
|
-
with:
|
|
35
|
-
config-file: release-please-config.json
|
|
36
|
-
manifest-file: .release-please-manifest.json
|
|
37
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
38
|
-
|
|
39
|
-
- name: Enable squash auto-merge for release PRs
|
|
40
|
-
if: ${{ steps.release.outputs.prs_created == 'true' }}
|
|
41
|
-
env:
|
|
42
|
-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
43
|
-
RELEASE_PRS: ${{ steps.release.outputs.prs }}
|
|
44
|
-
run: |
|
|
45
|
-
jq -r '.[].number' <<< "$RELEASE_PRS" | while read -r number; do
|
|
46
|
-
merge_method="$(gh pr view "$number" --repo "$GITHUB_REPOSITORY" --json autoMergeRequest --jq '.autoMergeRequest.mergeMethod // "NONE"')"
|
|
47
|
-
echo "Release PR #$number auto-merge method: $merge_method"
|
|
48
|
-
|
|
49
|
-
case "$merge_method" in
|
|
50
|
-
SQUASH)
|
|
51
|
-
echo "Squash auto-merge is already enabled for release PR #$number"
|
|
52
|
-
continue
|
|
53
|
-
;;
|
|
54
|
-
MERGE|REBASE)
|
|
55
|
-
echo "Replacing $merge_method auto-merge for release PR #$number"
|
|
56
|
-
gh pr merge "$number" --repo "$GITHUB_REPOSITORY" --disable-auto
|
|
57
|
-
;;
|
|
58
|
-
NONE)
|
|
59
|
-
;;
|
|
60
|
-
*)
|
|
61
|
-
echo "Unexpected auto-merge method for release PR #$number: $merge_method" >&2
|
|
62
|
-
exit 1
|
|
63
|
-
;;
|
|
64
|
-
esac
|
|
65
|
-
|
|
66
|
-
gh pr merge "$number" --repo "$GITHUB_REPOSITORY" --auto --squash
|
|
67
|
-
done
|
|
68
|
-
|
|
69
|
-
publish:
|
|
70
|
-
name: Test and publish to npm
|
|
71
|
-
needs: release
|
|
72
|
-
if: ${{ needs.release.outputs.release_created == 'true' }}
|
|
73
|
-
runs-on: ubuntu-latest
|
|
74
|
-
timeout-minutes: 20
|
|
75
|
-
permissions:
|
|
76
|
-
contents: read
|
|
77
|
-
id-token: write
|
|
78
|
-
steps:
|
|
79
|
-
- name: Check out release commit
|
|
80
|
-
uses: actions/checkout@v4
|
|
81
|
-
|
|
82
|
-
- name: Set up Node.js
|
|
83
|
-
uses: actions/setup-node@v4
|
|
84
|
-
with:
|
|
85
|
-
node-version: 22
|
|
86
|
-
registry-url: https://registry.npmjs.org
|
|
87
|
-
|
|
88
|
-
- name: Set up Bun
|
|
89
|
-
uses: oven-sh/setup-bun@v2
|
|
90
|
-
with:
|
|
91
|
-
bun-version: 1.2.15
|
|
92
|
-
|
|
93
|
-
- name: Install npm with trusted publishing support
|
|
94
|
-
run: npm install --global npm@11.5.1
|
|
95
|
-
|
|
96
|
-
- name: Test
|
|
97
|
-
run: bun test
|
|
98
|
-
|
|
99
|
-
- name: Verify release version metadata
|
|
100
|
-
env:
|
|
101
|
-
EXPECTED_VERSION: ${{ needs.release.outputs.release_version }}
|
|
102
|
-
run: npm run verify:release-version -- --expected "$EXPECTED_VERSION"
|
|
103
|
-
|
|
104
|
-
- name: Publish to npm
|
|
105
|
-
run: npm publish --access public --provenance
|
package/RELEASING.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# Releasing
|
|
2
|
-
|
|
3
|
-
[Release Please](https://github.com/googleapis/release-please) watches conventional commits merged into `main`. It opens or updates a release PR containing the calculated version change, `CHANGELOG.md`, and release manifest, then enables squash auto-merge for that PR. Required checks must pass before GitHub merges it. Its root Node strategy is the sole version writer: the generated PR keeps `package.json` and `.release-please-manifest.json` in sync. The generated PR title—and therefore its squash commit—uses `release(main): release <version>`. The merge creates a GitHub release and, after the test suite passes, publishes `pi-pr-review` to npm with signed provenance.
|
|
4
|
-
|
|
5
|
-
Pull-request CI verifies both root version fields remain equal. The publish job repeats that read-only check against the version emitted by Release Please, so an inconsistent release commit cannot publish; no feature PR should manually set a release version.
|
|
6
|
-
|
|
7
|
-
## Semver
|
|
8
|
-
|
|
9
|
-
The squash-merged PR title becomes the commit Release Please evaluates:
|
|
10
|
-
|
|
11
|
-
- `feat: ...` creates a minor release.
|
|
12
|
-
- `feat!: ...`, `fix!: ...`, or a `BREAKING CHANGE:` footer creates a major release.
|
|
13
|
-
- Every other allowed type—`fix:`, `perf:`, `revert:`, `chore:`, `docs:`, `refactor:`, `style:`, and `test:`—creates a patch release.
|
|
14
|
-
|
|
15
|
-
All changes to `main` must go through a pull request and use squash merging. The required `Validate PR title` check enforces the conventional title, and the required `Test` check runs the Bun test suite. Direct pushes, force pushes, branch deletion, and administrator bypass are disabled.
|
|
16
|
-
|
|
17
|
-
## One-time setup
|
|
18
|
-
|
|
19
|
-
1. Install the private [`nerv-ops`](https://github.com/settings/apps/nerv-ops) GitHub App on this repository with **Contents: read and write** and **Pull requests: read and write** permissions.
|
|
20
|
-
2. Under the repository's **Settings → General → Pull Requests**, enable **Allow auto-merge** and keep squash merging enabled.
|
|
21
|
-
3. Add the App ID as the repository Actions variable `NERV_OPS_APP_ID`, and add a generated PEM private key as the repository Actions secret `NERV_OPS_PRIVATE_KEY`.
|
|
22
|
-
4. In the npm settings for [`pi-pr-review`](https://www.npmjs.com/package/pi-pr-review), add a GitHub Actions trusted publisher with:
|
|
23
|
-
- organization/user: `10ego`
|
|
24
|
-
- repository: `pi-pr-review`
|
|
25
|
-
- workflow filename: `release-please.yml`
|
|
26
|
-
- environment: leave blank
|
|
27
|
-
5. Use conventional titles for squash-merged PRs.
|
|
28
|
-
|
|
29
|
-
No npm token is stored in GitHub. The workflow exchanges the App credentials for a short-lived repository installation token and uses npm trusted publishing through GitHub OIDC.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
-
"bootstrap-sha": "b9b62811aab72269b5f2bbb58d08d37b3c20ecff",
|
|
4
|
-
"packages": {
|
|
5
|
-
".": {
|
|
6
|
-
"release-type": "node",
|
|
7
|
-
"package-name": "pi-pr-review",
|
|
8
|
-
"changelog-path": "CHANGELOG.md",
|
|
9
|
-
"pull-request-title-pattern": "release${scope}: release${component} ${version}",
|
|
10
|
-
"include-component-in-tag": false,
|
|
11
|
-
"changelog-sections": [
|
|
12
|
-
{ "type": "feat", "section": "Features", "hidden": false },
|
|
13
|
-
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
|
|
14
|
-
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
|
|
15
|
-
{ "type": "revert", "section": "Reverts", "hidden": false },
|
|
16
|
-
{ "type": "docs", "section": "Documentation", "hidden": false },
|
|
17
|
-
{ "type": "style", "section": "Styles", "hidden": false },
|
|
18
|
-
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
|
|
19
|
-
{ "type": "test", "section": "Tests", "hidden": false },
|
|
20
|
-
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false }
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
// SemVer numeric prerelease identifiers cannot contain leading zeroes.
|
|
6
|
-
const NUMERIC_IDENTIFIER = "(?:0|[1-9]\\d*)";
|
|
7
|
-
const PRERELEASE_IDENTIFIER = `(?:${NUMERIC_IDENTIFIER}|\\d*[A-Za-z-][0-9A-Za-z-]*)`;
|
|
8
|
-
const SEMVER = new RegExp(`^${NUMERIC_IDENTIFIER}\\.${NUMERIC_IDENTIFIER}\\.${NUMERIC_IDENTIFIER}(?:-${PRERELEASE_IDENTIFIER}(?:\\.${PRERELEASE_IDENTIFIER})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
|
|
9
|
-
|
|
10
|
-
function readJson(filePath) {
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
13
|
-
} catch (error) {
|
|
14
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
-
throw new Error(`Could not read ${filePath}: ${message}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function assertVersion(location, value) {
|
|
20
|
-
if (typeof value !== "string" || !SEMVER.test(value)) {
|
|
21
|
-
throw new Error(`${location} must contain a valid semantic version; received ${JSON.stringify(value)}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Verify the root package metadata that Release Please updates in one release PR.
|
|
27
|
-
* This deliberately reads only: Release Please remains the sole version writer.
|
|
28
|
-
*/
|
|
29
|
-
export function verifyRootReleaseVersion(rootDir = process.cwd(), expectedVersion) {
|
|
30
|
-
const manifest = readJson(path.join(rootDir, ".release-please-manifest.json"));
|
|
31
|
-
const packageJson = readJson(path.join(rootDir, "package.json"));
|
|
32
|
-
const versions = {
|
|
33
|
-
'.release-please-manifest.json["."]': manifest["."],
|
|
34
|
-
"package.json.version": packageJson.version,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
for (const [location, version] of Object.entries(versions)) assertVersion(location, version);
|
|
38
|
-
if (expectedVersion !== undefined) assertVersion("expected release version", expectedVersion);
|
|
39
|
-
|
|
40
|
-
const [firstLocation, firstVersion] = Object.entries(versions)[0];
|
|
41
|
-
for (const [location, version] of Object.entries(versions).slice(1)) {
|
|
42
|
-
if (version !== firstVersion) {
|
|
43
|
-
throw new Error(`Root release version mismatch: ${firstLocation} is ${firstVersion}, but ${location} is ${version}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (expectedVersion !== undefined && firstVersion !== expectedVersion) {
|
|
47
|
-
throw new Error(`Root release version mismatch: ${firstLocation} is ${firstVersion}, but expected release version is ${expectedVersion}`);
|
|
48
|
-
}
|
|
49
|
-
return firstVersion;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function main(args) {
|
|
53
|
-
if (args.length === 0) return verifyRootReleaseVersion();
|
|
54
|
-
if (args.length === 2 && args[0] === "--expected") return verifyRootReleaseVersion(process.cwd(), args[1]);
|
|
55
|
-
throw new Error("Usage: node scripts/verify-release-version.mjs [--expected <version>]");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
59
|
-
try {
|
|
60
|
-
const version = main(process.argv.slice(2));
|
|
61
|
-
console.log(`Verified root release version ${version}.`);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { runWithConcurrency } from "../lib/pr-review-concurrency.ts";
|
|
3
|
-
|
|
4
|
-
async function waitFor(condition: () => boolean): Promise<void> {
|
|
5
|
-
for (let attempts = 0; attempts < 50; attempts++) {
|
|
6
|
-
if (condition()) return;
|
|
7
|
-
await Promise.resolve();
|
|
8
|
-
}
|
|
9
|
-
throw new Error("condition was not reached");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe("bounded review-pass scheduling", () => {
|
|
13
|
-
test("starts independent workers in parallel without exceeding the concurrency cap", async () => {
|
|
14
|
-
const started: number[] = [];
|
|
15
|
-
const releases = new Map<number, () => void>();
|
|
16
|
-
let active = 0;
|
|
17
|
-
let peakActive = 0;
|
|
18
|
-
|
|
19
|
-
const batch = runWithConcurrency([0, 1, 2, 3], 2, async (item) => {
|
|
20
|
-
started.push(item);
|
|
21
|
-
active++;
|
|
22
|
-
peakActive = Math.max(peakActive, active);
|
|
23
|
-
await new Promise<void>((resolve) => releases.set(item, resolve));
|
|
24
|
-
active--;
|
|
25
|
-
return item * 10;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
expect(started).toEqual([0, 1]);
|
|
29
|
-
expect(peakActive).toBe(2);
|
|
30
|
-
|
|
31
|
-
releases.get(0)!();
|
|
32
|
-
await waitFor(() => started.length === 3);
|
|
33
|
-
expect(started).toEqual([0, 1, 2]);
|
|
34
|
-
expect(active).toBe(2);
|
|
35
|
-
|
|
36
|
-
releases.get(1)!();
|
|
37
|
-
await waitFor(() => started.length === 4);
|
|
38
|
-
expect(started).toEqual([0, 1, 2, 3]);
|
|
39
|
-
expect(peakActive).toBe(2);
|
|
40
|
-
|
|
41
|
-
releases.get(2)!();
|
|
42
|
-
releases.get(3)!();
|
|
43
|
-
expect(await batch).toEqual([0, 10, 20, 30]);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
|
-
import { loadReviewContext, shardUnifiedDiff } from "../lib/pr-review-context.ts";
|
|
6
|
-
|
|
7
|
-
const roots: string[] = [];
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
function fixture(): string {
|
|
13
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pr-review-context-test-"));
|
|
14
|
-
roots.push(root);
|
|
15
|
-
return root;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("unified diff sharding", () => {
|
|
19
|
-
test("keeps file blocks whole and covers each block exactly once", () => {
|
|
20
|
-
const diff = [
|
|
21
|
-
"diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-old\n+new",
|
|
22
|
-
"diff --git a/b.ts b/b.ts\n--- a/b.ts\n+++ b/b.ts\n@@ -1 +1,3 @@\n-old\n+one\n+two\n+three",
|
|
23
|
-
"diff --git a/c.ts b/c.ts\n--- a/c.ts\n+++ b/c.ts\n@@ -1 +1 @@\n-old\n+new",
|
|
24
|
-
].join("\n");
|
|
25
|
-
const shards = shardUnifiedDiff(diff, 3);
|
|
26
|
-
expect(shards).toHaveLength(3);
|
|
27
|
-
for (const path of ["a.ts", "b.ts", "c.ts"]) {
|
|
28
|
-
expect(shards.filter((shard) => shard.includes(`a/${path} b/${path}`))).toHaveLength(1);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("does not invent empty shards for a single changed file", () => {
|
|
33
|
-
const diff = "diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-old\n+new";
|
|
34
|
-
expect(shardUnifiedDiff(diff, 2)).toEqual([diff]);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("review context files", () => {
|
|
39
|
-
test("appends a relative complete diff to compact inline metadata", async () => {
|
|
40
|
-
const root = fixture();
|
|
41
|
-
fs.writeFileSync(path.join(root, "pr.diff"), "diff --git a/a.ts b/a.ts\n+added\n");
|
|
42
|
-
const loaded = await loadReviewContext(root, "PR #7 metadata", "pr.diff");
|
|
43
|
-
expect(loaded.context).toContain("PR #7 metadata");
|
|
44
|
-
expect(loaded.context).toContain("--- Complete PR diff from context_file ---");
|
|
45
|
-
expect(loaded.context).toContain("diff --git a/a.ts b/a.ts");
|
|
46
|
-
expect(loaded.contextFile).toBe(path.join(root, "pr.diff"));
|
|
47
|
-
expect(loaded.contextFileText).toContain("diff --git a/a.ts b/a.ts");
|
|
48
|
-
expect(loaded.contextFileBytes).toBeGreaterThan(0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("preserves inline-only compatibility", async () => {
|
|
52
|
-
expect(await loadReviewContext("/tmp", " inline diff ", undefined)).toEqual({
|
|
53
|
-
context: "inline diff",
|
|
54
|
-
contextFileBytes: 0,
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("rejects empty, non-file, and oversized inputs before dispatch", async () => {
|
|
59
|
-
const root = fixture();
|
|
60
|
-
fs.writeFileSync(path.join(root, "empty.diff"), "");
|
|
61
|
-
fs.writeFileSync(path.join(root, "large.diff"), "12345");
|
|
62
|
-
await expect(loadReviewContext(root, undefined, "empty.diff")).rejects.toThrow("is empty");
|
|
63
|
-
await expect(loadReviewContext(root, undefined, ".")).rejects.toThrow("not a regular file");
|
|
64
|
-
await expect(loadReviewContext(root, undefined, "large.diff", 4)).rejects.toThrow("exceeds 4 bytes");
|
|
65
|
-
});
|
|
66
|
-
});
|