P0 — parsePorcelainZ rename/copy handling Git's -z porcelain emits `R <new>\0<old>\0` for renames (and C for copies). The naive split-and-slice treated <old> as an independent dirty path, which made runObservedDestructiveSyncOp warn about phantom "cleared paths" whenever a rename was in flight. Now we detect R/C status and skip the trailing entry. P1 — race-rescue loop unstages between attempts `git stash create` snapshots the index without clearing it, so iteration 2's `git add -A` would re-stage atop iteration 1's leftovers. Tree differences inside the loop then reflected stale staging rather than genuine new writes. Added a `git reset` at the top of each iteration so every attempt starts from a clean index baseline. P1 — writeActiveMergerStatus is now atomic Switched from in-place writeFileSync to temp-file + renameSync. POSIX guarantees rename atomicity on the same filesystem, so a reader can no longer catch the file mid-flush and return a false-negative "no merger active" advisory. P2 — Step regex em-dash clarity `[—\-:]` is functionally fine but obscures intent; switched to `(?:—|-|:)` so the em-dash branch is obvious. Added a test case for the em-dash separator. New tests: - parse-porcelain-z.test.ts (8 cases including renames + copies) - em-dash case added to derive-subject-summary.test.ts 247/247 merger-suite tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { deriveDeterministicSubjectSummary } from "../merger.js";
|
|
|
|
describe("deriveDeterministicSubjectSummary", () => {
|
|
it("returns null on empty input", () => {
|
|
expect(deriveDeterministicSubjectSummary("")).toBeNull();
|
|
expect(deriveDeterministicSubjectSummary(" \n ")).toBeNull();
|
|
});
|
|
|
|
it("uses the only line as the summary", () => {
|
|
expect(deriveDeterministicSubjectSummary("- feat(X): add widget")).toBe("add widget");
|
|
});
|
|
|
|
it("prefers the lowest-numbered Step commit over the most recent revision", () => {
|
|
// Order is most-recent-first, mimicking what the merger feeds in.
|
|
const log = [
|
|
"- fix(FN-3617): align mailbox modal css with design tokens",
|
|
"- feat(FN-3617): complete Step 4 — document Claude manual OAuth flow",
|
|
"- test(FN-3617): complete Step 2 — cover Anthropic manual-code UX",
|
|
"- feat(FN-3617): complete Step 1 — switch Anthropic OAuth to manual code",
|
|
].join("\n");
|
|
expect(deriveDeterministicSubjectSummary(log)).toBe(
|
|
"switch Anthropic OAuth to manual code (+3 more)",
|
|
);
|
|
});
|
|
|
|
it("falls back to the oldest commit when no Step headlines are present", () => {
|
|
const log = [
|
|
"- chore: lint",
|
|
"- fix: handle null",
|
|
"- feat: add foo",
|
|
].join("\n");
|
|
expect(deriveDeterministicSubjectSummary(log)).toBe("add foo (+2 more)");
|
|
});
|
|
|
|
it("handles plain hyphen separator in Step lines", () => {
|
|
const log = [
|
|
"- feat: complete Step 2 - second thing",
|
|
"- feat: complete Step 1 - first thing",
|
|
].join("\n");
|
|
expect(deriveDeterministicSubjectSummary(log)).toBe("first thing (+1 more)");
|
|
});
|
|
|
|
it("handles em-dash separator in Step lines", () => {
|
|
const log = [
|
|
"- feat: complete Step 2 — second thing",
|
|
"- feat: complete Step 1 — first thing",
|
|
].join("\n");
|
|
expect(deriveDeterministicSubjectSummary(log)).toBe("first thing (+1 more)");
|
|
});
|
|
});
|