diff --git a/packages/loopover-engine/src/miner/worktree-plan.ts b/packages/loopover-engine/src/miner/worktree-plan.ts index e052ad074c..b8ff54a9b5 100644 --- a/packages/loopover-engine/src/miner/worktree-plan.ts +++ b/packages/loopover-engine/src/miner/worktree-plan.ts @@ -42,7 +42,12 @@ function slugifyAttemptId(attemptId: string): string { .replace(/[^a-z0-9._-]+/g, "-") .replace(/^[-.]+|[-.]+$/g, ""); if (!slug) throw new Error("invalid_attempt_id"); - return slug.slice(0, MAX_SLUG_LENGTH); + // Re-trim the TRAILING separators AFTER truncation: `.` survives the character-class replace and is a + // valid interior char, so slicing to MAX_SLUG_LENGTH can leave a trailing `.`/`-` that the pre-truncation + // trim never saw -- git rejects a ref ending in `.` (#7528). Only the trailing edge needs re-trimming: the + // pre-truncation trim already removed any leading separators, and slice(0, n) never introduces a new leading + // one, so the truncated slug always keeps its non-separator first char (never empties). + return slug.slice(0, MAX_SLUG_LENGTH).replace(/[-.]+$/g, ""); } /** diff --git a/test/unit/worktree-plan.test.ts b/test/unit/worktree-plan.test.ts index 0cd45fc484..08575ecdd6 100644 --- a/test/unit/worktree-plan.test.ts +++ b/test/unit/worktree-plan.test.ts @@ -41,6 +41,24 @@ describe("planWorktree (#4269)", () => { it("rejects an attempt id that sanitizes to nothing", () => { expect(() => planWorktree({ repoPath: "/repo", attemptId: " --- " })).toThrow(/invalid_attempt_id/); }); + + it("REGRESSION (#7528): re-trims after truncation so a `.` landing on the 64-char boundary is not left trailing", () => { + // Slug is 63 'a's + '.' + 10 'b's (all survive the character class). Slicing to 64 keeps 63 'a's + '.', + // which would end in '.' -- a ref git rejects (`fatal: invalid reference`) -- unless we re-trim after slicing. + const attemptId = `${"a".repeat(63)}.${"b".repeat(10)}`; + const plan = planWorktree({ repoPath: "/repo", attemptId }); + expect(plan.branchName).toBe(`${WORKTREE_BRANCH_PREFIX}${"a".repeat(63)}`); + expect(plan.branchName.endsWith(".")).toBe(false); + expect(plan.branchName.endsWith("-")).toBe(false); + }); + + it("REGRESSION (#7528): a trailing `-` landing exactly on the boundary is likewise re-trimmed", () => { + // Same boundary case with '-' rather than '.', confirming both separators are handled post-truncation. + const attemptId = `${"a".repeat(63)}-${"b".repeat(10)}`; + const plan = planWorktree({ repoPath: "/repo", attemptId }); + expect(plan.branchName).toBe(`${WORKTREE_BRANCH_PREFIX}${"a".repeat(63)}`); + expect(plan.branchName.endsWith("-")).toBe(false); + }); }); describe("addWorktree", () => {