@maxiaochao/pi-toolkit 0.9.4 → 0.9.6
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 +12 -0
- package/package.json +1 -1
- package/packages/codex-edit/CHANGELOG.md +15 -0
- package/packages/codex-edit/README.md +10 -3
- package/packages/codex-edit/TESTING.md +40 -9
- package/packages/codex-edit/benchmark/explicit-edit-100.json +2824 -0
- package/packages/codex-edit/benchmark/export-evidence.mjs +77 -0
- package/packages/codex-edit/benchmark/incident-cases.mjs +37 -0
- package/packages/codex-edit/benchmark/prepare.mjs +181 -0
- package/packages/codex-edit/benchmark/results/explicit-edit-100-sol-low-v3.json +4562 -0
- package/packages/codex-edit/benchmark/run-with-budget.mjs +77 -0
- package/packages/codex-edit/benchmark/schedule.mjs +19 -0
- package/packages/codex-edit/benchmark/select-tasks.mjs +167 -0
- package/packages/codex-edit/codex-edit-explainer.html +4 -4
- package/packages/codex-edit/docs/test-report.html +181 -0
- package/packages/codex-edit/docs/test-report.md +242 -0
- package/packages/codex-edit/docs/test-summary.md +9 -0
- package/packages/codex-edit/extensions/codex-edit.ts +4 -2
- package/packages/codex-edit/package.json +8 -2
- package/packages/codex-edit/src/parser.ts +26 -3
- package/packages/codex-edit/tests/benchmark.test.mjs +81 -0
- package/packages/codex-edit/tests/comparison-cases.mjs +396 -0
- package/packages/codex-edit/tests/comparison-harness.mjs +44 -0
- package/packages/codex-edit/tests/generate-test-report.mjs +589 -0
- package/packages/codex-edit/tests/legacy-parser-v0.1.7.mjs +128 -0
- package/packages/codex-edit/tests/native-vs-codex.ts +109 -0
- package/packages/codex-edit/tests/package.test.mjs +8 -0
- package/packages/codex-edit/tests/parser.test.mjs +123 -1
- package/packages/codex-edit/tests/released-vs-local.test.mjs +31 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Frozen semantic baseline from @maxiaochao/pi-codex-edit v0.1.7.
|
|
2
|
+
// Source blob: codex-edit-v0.1.7:packages/codex-edit/src/parser.ts
|
|
3
|
+
// Git blob ID: e2896b59d40b5f6f01b3a4a7918476f4acc28cd8
|
|
4
|
+
// Keep this file unchanged; add cases to comparison-cases.mjs instead.
|
|
5
|
+
|
|
6
|
+
export function parseLegacyPatch(patch) {
|
|
7
|
+
const lines = patch.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim().split("\n");
|
|
8
|
+
if (lines[0]?.trim() !== "*** Begin Patch") throw new Error("The first line of the patch must be '*** Begin Patch'");
|
|
9
|
+
if (lines.at(-1)?.trim() !== "*** End Patch") throw new Error("The last line of the patch must be '*** End Patch'");
|
|
10
|
+
const operations = [];
|
|
11
|
+
let index = 1;
|
|
12
|
+
while (index < lines.length - 1) {
|
|
13
|
+
const line = lines[index];
|
|
14
|
+
if (line.startsWith("*** Add File: ")) {
|
|
15
|
+
const path = line.slice("*** Add File: ".length);
|
|
16
|
+
index += 1;
|
|
17
|
+
const content = [];
|
|
18
|
+
while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
|
|
19
|
+
if (!lines[index].startsWith("+")) throw new Error(`Invalid add line ${index + 1}`);
|
|
20
|
+
content.push(lines[index].slice(1));
|
|
21
|
+
index += 1;
|
|
22
|
+
}
|
|
23
|
+
if (content.length === 0) throw new Error(`Add file hunk for path '${path}' is empty`);
|
|
24
|
+
operations.push({ action: "add", path, content: `${content.join("\n")}\n` });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith("*** Delete File: ")) {
|
|
28
|
+
const path = line.slice("*** Delete File: ".length);
|
|
29
|
+
if (!path) throw new Error(`Delete file hunk on line ${index + 1} has an empty path`);
|
|
30
|
+
operations.push({ action: "delete", path });
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (line.startsWith("*** Update File: ")) {
|
|
35
|
+
const path = line.slice("*** Update File: ".length);
|
|
36
|
+
index += 1;
|
|
37
|
+
let moveTo;
|
|
38
|
+
if (lines[index]?.startsWith("*** Move to: ")) {
|
|
39
|
+
moveTo = lines[index].slice("*** Move to: ".length);
|
|
40
|
+
index += 1;
|
|
41
|
+
}
|
|
42
|
+
const chunks = [];
|
|
43
|
+
while (index < lines.length - 1 && !lines[index].startsWith("*** ")) {
|
|
44
|
+
let context;
|
|
45
|
+
if (lines[index] === "@@" || lines[index].startsWith("@@ ")) {
|
|
46
|
+
context = lines[index].slice(2).trim() || undefined;
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
const oldLines = [];
|
|
50
|
+
const newLines = [];
|
|
51
|
+
const contextLineIndices = [];
|
|
52
|
+
while (index < lines.length - 1 && !lines[index].startsWith("*** ") && !lines[index].startsWith("@@")) {
|
|
53
|
+
const change = lines[index];
|
|
54
|
+
const marker = change[0];
|
|
55
|
+
if (!["+", "-", " "].includes(marker)) throw new Error(`Invalid update line ${index + 1}`);
|
|
56
|
+
const text = change.slice(1);
|
|
57
|
+
if (marker === "+") newLines.push(text);
|
|
58
|
+
else if (marker === "-") oldLines.push(text);
|
|
59
|
+
else {
|
|
60
|
+
contextLineIndices.push([oldLines.length, newLines.length]);
|
|
61
|
+
oldLines.push(text);
|
|
62
|
+
newLines.push(text);
|
|
63
|
+
}
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
const endOfFile = lines[index] === "*** End of File";
|
|
67
|
+
if (endOfFile) index += 1;
|
|
68
|
+
if (oldLines.length === 0 && newLines.length === 0) throw new Error(`Empty update chunk on line ${index + 1}`);
|
|
69
|
+
chunks.push({ context, oldLines, newLines, contextLineIndices, endOfFile });
|
|
70
|
+
}
|
|
71
|
+
if (chunks.length === 0) throw new Error(`Update file hunk for path '${path}' is empty`);
|
|
72
|
+
operations.push({ action: "update", path, moveTo, chunks });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Invalid hunk header on line ${index + 1}: ${line}`);
|
|
76
|
+
}
|
|
77
|
+
if (operations.length === 0) throw new Error("Patch contains no file operations");
|
|
78
|
+
const targets = operations.flatMap((operation) => [operation.path, operation.moveTo].filter(Boolean));
|
|
79
|
+
if (new Set(targets).size !== targets.length) throw new Error("Multiple operations target the same path");
|
|
80
|
+
return operations;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findCandidates(lines, pattern, start, eof, compare) {
|
|
84
|
+
const candidates = [];
|
|
85
|
+
for (let index = start; index + pattern.length <= lines.length; index += 1) {
|
|
86
|
+
if (pattern.every((line, offset) => compare(lines[index + offset], line))) candidates.push(index);
|
|
87
|
+
}
|
|
88
|
+
candidates.sort((left, right) => eof ? right - left : left - right);
|
|
89
|
+
return candidates;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function locate(lines, pattern, start, eof, label) {
|
|
93
|
+
if (pattern.length === 0) return Math.min(start, lines.length);
|
|
94
|
+
const comparisons = [
|
|
95
|
+
(left, right) => left === right,
|
|
96
|
+
(left, right) => left.trimEnd() === right.trimEnd(),
|
|
97
|
+
(left, right) => left.trim() === right.trim(),
|
|
98
|
+
];
|
|
99
|
+
for (const compare of comparisons) {
|
|
100
|
+
const candidates = findCandidates(lines, pattern, start, eof, compare);
|
|
101
|
+
if (candidates.length > 0) return candidates[0];
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`Failed to find expected lines${label ? ` in ${label}` : ""}: ${pattern.join("\\n")}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function applyLegacyUpdate(content, chunks, path) {
|
|
107
|
+
const bom = content.startsWith("\ufeff") ? "\ufeff" : "";
|
|
108
|
+
const withoutBom = bom ? content.slice(1) : content;
|
|
109
|
+
const ending = withoutBom.includes("\r\n") ? "\r\n" : "\n";
|
|
110
|
+
const normalized = withoutBom.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
111
|
+
const hadFinalNewline = normalized.endsWith("\n");
|
|
112
|
+
const lines = normalized === "" ? [] : normalized.split("\n");
|
|
113
|
+
if (hadFinalNewline) lines.pop();
|
|
114
|
+
const replacements = [];
|
|
115
|
+
let cursor = 0;
|
|
116
|
+
for (const chunk of chunks) {
|
|
117
|
+
if (chunk.context !== undefined) cursor = locate(lines, [chunk.context], cursor, false, path) + 1;
|
|
118
|
+
const start = locate(lines, chunk.oldLines, cursor, chunk.endOfFile, path);
|
|
119
|
+
replacements.push({ start, length: chunk.oldLines.length, next: chunk.newLines });
|
|
120
|
+
cursor = start + chunk.oldLines.length;
|
|
121
|
+
}
|
|
122
|
+
const output = [...lines];
|
|
123
|
+
for (const replacement of [...replacements].reverse()) output.splice(replacement.start, replacement.length, ...replacement.next);
|
|
124
|
+
let next = output.join("\n");
|
|
125
|
+
if (hadFinalNewline || next.length > 0) next += "\n";
|
|
126
|
+
if (ending === "\r\n") next = next.replace(/\n/g, "\r\n");
|
|
127
|
+
return bom + next;
|
|
128
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { createEditToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import registerCodexEdit from "../extensions/codex-edit.ts";
|
|
7
|
+
import { comparisonCases } from "./comparison-cases.mjs";
|
|
8
|
+
|
|
9
|
+
async function writeSnapshot(root, snapshot) {
|
|
10
|
+
for (const [path, content] of Object.entries(snapshot)) {
|
|
11
|
+
const absolute = join(root, path);
|
|
12
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
13
|
+
await writeFile(absolute, content, "utf8");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readSnapshot(root, relative = "") {
|
|
18
|
+
const snapshot = {};
|
|
19
|
+
for (const entry of await readdir(join(root, relative), { withFileTypes: true })) {
|
|
20
|
+
const path = relative ? `${relative}/${entry.name}` : entry.name;
|
|
21
|
+
if (entry.isDirectory()) Object.assign(snapshot, await readSnapshot(root, path));
|
|
22
|
+
else snapshot[path] = await readFile(join(root, path), "utf8");
|
|
23
|
+
}
|
|
24
|
+
return snapshot;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function runNativeCase(testCase) {
|
|
28
|
+
if (testCase.outcomes.native === "unsupported") {
|
|
29
|
+
assert.equal(testCase.nativeCalls, null, `${testCase.id}: unsupported native case must not invent edit calls`);
|
|
30
|
+
return "unsupported";
|
|
31
|
+
}
|
|
32
|
+
const workspace = await mkdtemp(join(tmpdir(), "codex-edit-native-"));
|
|
33
|
+
try {
|
|
34
|
+
await writeSnapshot(workspace, testCase.input);
|
|
35
|
+
const edit = createEditToolDefinition(workspace);
|
|
36
|
+
try {
|
|
37
|
+
for (const [index, call] of testCase.nativeCalls.entries()) {
|
|
38
|
+
await edit.execute(`native-${index}`, call, undefined, undefined, { cwd: workspace });
|
|
39
|
+
}
|
|
40
|
+
if (testCase.outcomes.native !== "pass") {
|
|
41
|
+
assert.fail(`${testCase.id}: native edit unexpectedly succeeded`);
|
|
42
|
+
}
|
|
43
|
+
assert.deepEqual(await readSnapshot(workspace), testCase.expected);
|
|
44
|
+
return "pass";
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (testCase.outcomes.native === "pass") throw error;
|
|
47
|
+
return "fail";
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
await rm(workspace, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function getCodexTool() {
|
|
55
|
+
let tool;
|
|
56
|
+
await registerCodexEdit({
|
|
57
|
+
registerTool(definition) { tool = definition; },
|
|
58
|
+
on() {},
|
|
59
|
+
getActiveTools() { return []; },
|
|
60
|
+
});
|
|
61
|
+
assert.ok(tool, "Codex Edit extension did not register apply_patch");
|
|
62
|
+
return tool;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runCodexCase(tool, testCase) {
|
|
66
|
+
const workspace = await mkdtemp(join(tmpdir(), "codex-edit-local-"));
|
|
67
|
+
try {
|
|
68
|
+
await writeSnapshot(workspace, testCase.input);
|
|
69
|
+
try {
|
|
70
|
+
await tool.execute("codex-local", { patch: testCase.patch }, undefined, undefined, {
|
|
71
|
+
cwd: workspace,
|
|
72
|
+
hasUI: false,
|
|
73
|
+
});
|
|
74
|
+
if (testCase.outcomes.local !== "pass") {
|
|
75
|
+
assert.fail(`${testCase.id}: local Codex Edit unexpectedly succeeded`);
|
|
76
|
+
}
|
|
77
|
+
assert.deepEqual(await readSnapshot(workspace), testCase.expected);
|
|
78
|
+
return "pass";
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (testCase.outcomes.local === "pass") throw error;
|
|
81
|
+
assert.deepEqual(
|
|
82
|
+
await readSnapshot(workspace),
|
|
83
|
+
testCase.input,
|
|
84
|
+
`${testCase.id}: failed Codex patch must not partially mutate the workspace`,
|
|
85
|
+
);
|
|
86
|
+
return "fail";
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
await rm(workspace, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default async function () {
|
|
94
|
+
const codexTool = await getCodexTool();
|
|
95
|
+
const results = [];
|
|
96
|
+
for (const testCase of comparisonCases) {
|
|
97
|
+
const native = await runNativeCase(testCase);
|
|
98
|
+
const codex = await runCodexCase(codexTool, testCase);
|
|
99
|
+
results.push({ id: testCase.id, native, codex });
|
|
100
|
+
}
|
|
101
|
+
assert.equal(results.length, comparisonCases.length);
|
|
102
|
+
assert.equal(results.filter(({ native }) => native === "pass").length, 5);
|
|
103
|
+
assert.equal(results.filter(({ native }) => native === "fail").length, 2);
|
|
104
|
+
assert.equal(results.filter(({ native }) => native === "unsupported").length, 5);
|
|
105
|
+
assert.equal(results.filter(({ codex }) => codex === "pass").length, 7);
|
|
106
|
+
assert.equal(results.filter(({ codex }) => codex === "fail").length, 5);
|
|
107
|
+
for (const result of results) console.log(`${result.id}: native=${result.native}, codex=${result.codex}`);
|
|
108
|
+
console.log(`Native edit vs local Codex Edit: ${results.length} comparison cases passed`);
|
|
109
|
+
}
|
|
@@ -12,6 +12,8 @@ test("package points Pi at the extension and config is reachable from it", async
|
|
|
12
12
|
const source = await readFile(join(packageDir, "extensions/codex-edit.ts"), "utf8");
|
|
13
13
|
assert.match(source, /new URL\("\.\.\/config\.json", import\.meta\.url\)/);
|
|
14
14
|
assert.match(source, /model-routing\.ts/);
|
|
15
|
+
assert.match(source, /parameters: Type\.Object\(\{\s*patch: Type\.String/);
|
|
16
|
+
assert.match(source, /applyUpdateGroups\(before, operation\.chunkGroups, operation\.path\)/);
|
|
15
17
|
const config = JSON.parse(await readFile(join(packageDir, "config.json"), "utf8"));
|
|
16
18
|
assert.equal(config.enabled, true);
|
|
17
19
|
assert.deepEqual(config.models, ["gpt-5.6-*"]);
|
|
@@ -41,3 +43,9 @@ test("matches allowed model IDs with globs independently of provider and grammar
|
|
|
41
43
|
assert.equal(modelIsAllowed({ ...baseModel, id: "GPT-5.6-terra" }, settings), false);
|
|
42
44
|
assert.equal(modelIsAllowed(baseModel, { enabled: false, models: ["gpt-5.6-*"] }), false);
|
|
43
45
|
});
|
|
46
|
+
|
|
47
|
+
test("execution keeps a resolved-path conflict guard after parser coalescing", async () => {
|
|
48
|
+
const source = await readFile(join(packageDir, "extensions/codex-edit.ts"), "utf8");
|
|
49
|
+
assert.match(source, /new Set\(targets\)\.size !== targets\.length/);
|
|
50
|
+
assert.match(source, /Multiple patch operations target the same resolved path/);
|
|
51
|
+
});
|
|
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import { applyUpdate, parsePatch, resolveWorkspacePath } from "../src/parser.ts";
|
|
6
|
+
import { applyUpdate, applyUpdateGroups, parsePatch, resolveWorkspacePath } from "../src/parser.ts";
|
|
7
7
|
|
|
8
8
|
test("parses add, delete, update, and move operations", () => {
|
|
9
9
|
const operations = parsePatch(`*** Begin Patch
|
|
@@ -162,6 +162,128 @@ test("parses multiple file operations used by structural refactors", () => {
|
|
|
162
162
|
);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
test("coalesces repeated updates to one path and preserves operation order", () => {
|
|
166
|
+
const operations = parsePatch(`*** Begin Patch
|
|
167
|
+
*** Update File: src/tool.ts
|
|
168
|
+
@@
|
|
169
|
+
-const value = 1;
|
|
170
|
+
+const value = 2;
|
|
171
|
+
*** Update File: src/tool.ts
|
|
172
|
+
@@
|
|
173
|
+
-const value = 2;
|
|
174
|
+
+const value = 3;
|
|
175
|
+
*** End Patch`);
|
|
176
|
+
assert.equal(operations.length, 1);
|
|
177
|
+
assert.equal(operations[0].path, "src/tool.ts");
|
|
178
|
+
assert.equal(operations[0].chunkGroups.length, 2);
|
|
179
|
+
const after = applyUpdateGroups("const value = 1;\n", operations[0].chunkGroups, "src/tool.ts");
|
|
180
|
+
assert.equal(after, "const value = 3;\n");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("still rejects conflicting repeated targets", () => {
|
|
184
|
+
assert.throws(() => parsePatch(`*** Begin Patch
|
|
185
|
+
*** Update File: src/tool.ts
|
|
186
|
+
@@
|
|
187
|
+
-old
|
|
188
|
+
+new
|
|
189
|
+
*** Delete File: src/tool.ts
|
|
190
|
+
*** End Patch`), /same path/);
|
|
191
|
+
assert.throws(() => parsePatch(`*** Begin Patch
|
|
192
|
+
*** Update File: src/old.ts
|
|
193
|
+
*** Move to: src/new.ts
|
|
194
|
+
@@
|
|
195
|
+
-old
|
|
196
|
+
+new
|
|
197
|
+
*** Add File: src/new.ts
|
|
198
|
+
+replacement
|
|
199
|
+
*** End Patch`), /same path/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("rejects empty chunks and stale expected lines instead of guessing", () => {
|
|
203
|
+
assert.throws(() => parsePatch(`*** Begin Patch
|
|
204
|
+
*** Update File: src/tool.ts
|
|
205
|
+
@@
|
|
206
|
+
*** End Patch`), /Empty update chunk/);
|
|
207
|
+
const [operation] = parsePatch(`*** Begin Patch
|
|
208
|
+
*** Update File: src/tool.ts
|
|
209
|
+
@@
|
|
210
|
+
-missing line
|
|
211
|
+
+replacement
|
|
212
|
+
*** End Patch`);
|
|
213
|
+
assert.throws(
|
|
214
|
+
() => applyUpdate("actual line\n", operation.chunks, operation.path),
|
|
215
|
+
/Failed to find expected lines in src\/tool\.ts/,
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("handles the reported project-scale repeated-file and Markdown shapes", () => {
|
|
220
|
+
const operations = parsePatch(`*** Begin Patch
|
|
221
|
+
*** Update File: skills/pi-worktree/SKILL.md
|
|
222
|
+
@@
|
|
223
|
+
-description: old
|
|
224
|
+
+description: updated
|
|
225
|
+
@@
|
|
226
|
+
---
|
|
227
|
+
+Release notes follow.
|
|
228
|
+
*** Update File: packages/tiny-subagent/src/tool.ts
|
|
229
|
+
@@
|
|
230
|
+
-const state = "old";
|
|
231
|
+
+const state = "middle";
|
|
232
|
+
*** Update File: packages/tiny-subagent/src/tool.ts
|
|
233
|
+
@@
|
|
234
|
+
-const state = "middle";
|
|
235
|
+
+const state = "new";
|
|
236
|
+
*** Update File: scripts/release-package.sh
|
|
237
|
+
@@
|
|
238
|
+
-ROOT_LEVEL=patch
|
|
239
|
+
+ROOT_LEVEL=minor
|
|
240
|
+
*** Update File: scripts/release-package.sh
|
|
241
|
+
@@
|
|
242
|
+
-ROOT_LEVEL=minor
|
|
243
|
+
+ROOT_LEVEL=patch
|
|
244
|
+
*** End Patch`);
|
|
245
|
+
assert.deepEqual(operations.map((operation) => operation.path), [
|
|
246
|
+
"skills/pi-worktree/SKILL.md",
|
|
247
|
+
"packages/tiny-subagent/src/tool.ts",
|
|
248
|
+
"scripts/release-package.sh",
|
|
249
|
+
]);
|
|
250
|
+
assert.equal(operations[1].chunkGroups.length, 2);
|
|
251
|
+
assert.equal(operations[2].chunkGroups.length, 2);
|
|
252
|
+
|
|
253
|
+
const skill = `---
|
|
254
|
+
name: pi-worktree
|
|
255
|
+
description: old
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
# pi-worktree
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
Footer
|
|
262
|
+
`;
|
|
263
|
+
assert.equal(
|
|
264
|
+
applyUpdateGroups(skill, operations[0].chunkGroups, operations[0].path),
|
|
265
|
+
`---
|
|
266
|
+
name: pi-worktree
|
|
267
|
+
description: updated
|
|
268
|
+
---
|
|
269
|
+
Release notes follow.
|
|
270
|
+
|
|
271
|
+
# pi-worktree
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
Footer
|
|
275
|
+
`,
|
|
276
|
+
);
|
|
277
|
+
assert.equal(
|
|
278
|
+
applyUpdateGroups('const state = "old";\n', operations[1].chunkGroups, operations[1].path),
|
|
279
|
+
'const state = "new";\n',
|
|
280
|
+
);
|
|
281
|
+
assert.equal(
|
|
282
|
+
applyUpdateGroups("ROOT_LEVEL=patch\n", operations[2].chunkGroups, operations[2].path),
|
|
283
|
+
"ROOT_LEVEL=patch\n",
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
165
287
|
test("rejects workspace escapes", () => {
|
|
166
288
|
assert.equal(resolveWorkspacePath("/tmp/project", "/tmp/project/src/a.js"), "/tmp/project/src/a.js");
|
|
167
289
|
assert.throws(() => resolveWorkspacePath("/tmp/project", "/tmp/other/a.js"), /escapes/);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { applyUpdate, parsePatch } from "../src/parser.ts";
|
|
4
|
+
import { comparisonCases } from "./comparison-cases.mjs";
|
|
5
|
+
import { runPatchCase } from "./comparison-harness.mjs";
|
|
6
|
+
import { applyLegacyUpdate, parseLegacyPatch } from "./legacy-parser-v0.1.7.mjs";
|
|
7
|
+
|
|
8
|
+
test("released v0.1.7 versus local parser over the growing comparison corpus", () => {
|
|
9
|
+
const results = [];
|
|
10
|
+
for (const testCase of comparisonCases) {
|
|
11
|
+
const released = runPatchCase(
|
|
12
|
+
testCase,
|
|
13
|
+
parseLegacyPatch,
|
|
14
|
+
applyLegacyUpdate,
|
|
15
|
+
testCase.outcomes.released,
|
|
16
|
+
);
|
|
17
|
+
const local = runPatchCase(testCase, parsePatch, applyUpdate, testCase.outcomes.local);
|
|
18
|
+
results.push({ id: testCase.id, released, local });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
assert.equal(results.length, comparisonCases.length);
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
results.find(({ id }) => id === "repeated-update-same-path"),
|
|
24
|
+
{ id: "repeated-update-same-path", released: "fail", local: "pass" },
|
|
25
|
+
);
|
|
26
|
+
assert.equal(results.filter(({ released }) => released === "pass").length, 6);
|
|
27
|
+
assert.equal(results.filter(({ released }) => released === "fail").length, 6);
|
|
28
|
+
assert.equal(results.filter(({ local }) => local === "pass").length, 7);
|
|
29
|
+
assert.equal(results.filter(({ local }) => local === "fail").length, 5);
|
|
30
|
+
for (const result of results) console.log(`${result.id}: released=${result.released}, local=${result.local}`);
|
|
31
|
+
});
|