Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/loopover-engine/src/miner/worktree-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, "");
}

/**
Expand Down
18 changes: 18 additions & 0 deletions test/unit/worktree-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down