Files
fusion/packages/engine/src/__tests__/derive-subject-summary.test.ts
gsxdsm 9a83117294 fix(merger): address code-review findings on autostash + observer
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>
2026-05-06 18:04:19 -07:00

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)");
});
});