test(#2916): parameterize fixtures so both main and trunk are exercised

Two follow-ups on commit 80f14cac (which hardened quick-branching with a
trunk fixture):

1. quick-branching.test.cjs: add a `defaultBranch` parameter to
   setupFixture and run the "branches off origin/HEAD" assertion against
   both `main` and `trunk`. The wholesale switch to trunk in 80f14cac
   removed coverage of the conventional `main` path; parameterizing
   restores it without giving up the symbolic-ref guarantee.

2. bug-2916-handle-branching-default-base.test.cjs: apply the same
   parameterization here. handle_branching has the same default-branch
   detection logic as Step 2.5, so it deserves the same trunk regression
   guard. Previously this file only exercised `main`.

A regression that silently defaults to `main` instead of consulting
`git symbolic-ref refs/remotes/origin/HEAD` now fails the `trunk`
variant in both files.

Tests: 10/10 in the touched suites.
This commit is contained in:
Tom Boucher
2026-04-30 21:57:27 -04:00
parent 80f14cac1f
commit ace61869d0
2 changed files with 103 additions and 85 deletions

View File

@@ -108,39 +108,44 @@ function extractHandleBranchingBash() {
}
/**
* Build a fixture: a bare "origin" repo with `main` (one commit), a clone with
* `origin/HEAD` pointed at `main`, and a checked-out previous-phase branch
* carrying its own unmerged commit. Returns the clone path.
* Build a fixture: a bare "origin" repo with the named default branch (one
* commit), a clone with `origin/HEAD` pointed at it, and a checked-out
* previous-phase branch carrying its own unmerged commit.
*
* `defaultBranch` is parameterized so callers can lock in that the workflow
* honors `git symbolic-ref refs/remotes/origin/HEAD` rather than silently
* defaulting to `main` (#2921 CR feedback — quick-branching.test.cjs got the
* same treatment in 80f14cac; this test deserves the same coverage).
*/
function setupFixture() {
function setupFixture(defaultBranch = 'main') {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2916-'));
const seedPath = path.join(root, 'seed');
const originPath = path.join(root, 'origin.git');
const clonePath = path.join(root, 'clone');
fs.mkdirSync(seedPath);
git(seedPath, 'init', '-b', 'main');
git(seedPath, 'init', '-b', defaultBranch);
git(seedPath, 'config', 'commit.gpgsign', 'false');
fs.writeFileSync(path.join(seedPath, 'README.md'), '# seed\n');
git(seedPath, 'add', 'README.md');
git(seedPath, 'commit', '-m', 'initial');
git(root, 'clone', '--bare', seedPath, originPath);
git(originPath, 'symbolic-ref', 'HEAD', 'refs/heads/main');
git(originPath, 'symbolic-ref', 'HEAD', `refs/heads/${defaultBranch}`);
git(root, 'clone', originPath, clonePath);
git(clonePath, 'config', 'commit.gpgsign', 'false');
git(clonePath, 'config', 'user.email', 'test@test.com');
git(clonePath, 'config', 'user.name', 'Test');
// Simulate finishing a previous phase: branch off main, add a commit, and
// *stay* on it (this is the failure scenario described in the bug).
// Simulate finishing a previous phase: branch off the default branch, add
// a commit, and *stay* on it (the failure scenario described in the bug).
git(clonePath, 'checkout', '-b', 'feature/phase-01-foundation');
fs.writeFileSync(path.join(clonePath, 'phase01.txt'), 'phase 1 work\n');
git(clonePath, 'add', 'phase01.txt');
git(clonePath, 'commit', '-m', 'phase 01 work');
return { root, clonePath };
return { root, clonePath, defaultBranch };
}
function runHandleBranchingStep(bash, cwd, branchName) {
@@ -163,45 +168,51 @@ function runHandleBranchingStep(bash, cwd, branchName) {
}
describe('handle_branching branches off origin/HEAD, not current HEAD (#2916)', () => {
test('new phase branch contains 0 commits inherited from previous-phase HEAD', () => {
const bash = extractHandleBranchingBash();
const { root, clonePath } = setupFixture();
// Run against `main` (conventional default) and `trunk` (non-main default
// exercising the symbolic-ref code path) so a regression that hard-codes
// `main` instead of consulting origin/HEAD will fail the trunk variant.
for (const defaultBranch of ['main', 'trunk']) {
test(`new phase branch branches off origin/${defaultBranch} with 0 inherited commits`, () => {
const bash = extractHandleBranchingBash();
const { root, clonePath } = setupFixture(defaultBranch);
try {
// Sanity: we begin sitting on the previous phase branch, 1 ahead of origin/main.
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'feature/phase-01-foundation'
);
assert.equal(
git(clonePath, 'rev-list', '--count', 'origin/main..HEAD'),
'1',
'fixture should be 1 commit ahead of origin/main'
);
try {
const upstream = `origin/${defaultBranch}`;
runHandleBranchingStep(bash, clonePath, 'feature/phase-02-content-sync');
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'feature/phase-01-foundation'
);
assert.equal(
git(clonePath, 'rev-list', '--count', `${upstream}..HEAD`),
'1',
`fixture should be 1 commit ahead of ${upstream}`
);
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'feature/phase-02-content-sync',
'handle_branching should switch to the new phase branch'
);
runHandleBranchingStep(bash, clonePath, 'feature/phase-02-content-sync');
const inherited = git(clonePath, 'rev-list', '--count', 'origin/main..HEAD');
assert.equal(
inherited,
'0',
`new phase branch must branch off origin/main, but inherited ${inherited} commit(s) from previous-phase HEAD`
);
assert.equal(
git(clonePath, 'rev-parse', 'HEAD'),
git(clonePath, 'rev-parse', 'origin/main'),
'new phase branch tip must equal origin/main tip'
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'feature/phase-02-content-sync',
'handle_branching should switch to the new phase branch'
);
const inherited = git(clonePath, 'rev-list', '--count', `${upstream}..HEAD`);
assert.equal(
inherited,
'0',
`new phase branch must branch off ${upstream}, but inherited ${inherited} commit(s) from previous-phase HEAD`
);
assert.equal(
git(clonePath, 'rev-parse', 'HEAD'),
git(clonePath, 'rev-parse', upstream),
`new phase branch tip must equal ${upstream} tip`
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
}
test('handle_branching reuses an existing branch instead of forking again', () => {
const bash = extractHandleBranchingBash();

View File

@@ -103,35 +103,35 @@ function extractStep25Bash() {
* implementation skips `git symbolic-ref refs/remotes/origin/HEAD` and just
* defaults to `main`, every assertion below collapses (#2921 CR nitpick).
*/
function setupFixture() {
function setupFixture(defaultBranch = 'trunk') {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-quick-branching-'));
const seedPath = path.join(root, 'seed');
const originPath = path.join(root, 'origin.git');
const clonePath = path.join(root, 'clone');
fs.mkdirSync(seedPath);
git(seedPath, 'init', '-b', 'trunk');
git(seedPath, 'init', '-b', defaultBranch);
git(seedPath, 'config', 'commit.gpgsign', 'false');
fs.writeFileSync(path.join(seedPath, 'README.md'), '# seed\n');
git(seedPath, 'add', 'README.md');
git(seedPath, 'commit', '-m', 'initial');
git(root, 'clone', '--bare', seedPath, originPath);
git(originPath, 'symbolic-ref', 'HEAD', 'refs/heads/trunk');
git(originPath, 'symbolic-ref', 'HEAD', `refs/heads/${defaultBranch}`);
git(root, 'clone', originPath, clonePath);
git(clonePath, 'config', 'commit.gpgsign', 'false');
git(clonePath, 'config', 'user.email', 'test@test.com');
git(clonePath, 'config', 'user.name', 'Test');
// Simulate finishing a previous quick task: branch off trunk, add a commit,
// and stay on it (this is the failure scenario from #2916).
// Simulate finishing a previous quick task: branch off the default branch,
// add a commit, and stay on it (this is the failure scenario from #2916).
git(clonePath, 'checkout', '-b', 'quick/01-prev-task');
fs.writeFileSync(path.join(clonePath, 'prev.txt'), 'prev work\n');
git(clonePath, 'add', 'prev.txt');
git(clonePath, 'commit', '-m', 'prev quick task work');
return { root, clonePath };
return { root, clonePath, defaultBranch };
}
function runStep(bash, cwd, branchName) {
@@ -214,45 +214,52 @@ describe('quick workflow: branching support', () => {
);
});
test('new quick-task branch contains 0 commits inherited from previous-task HEAD (#2916)', () => {
const bash = extractStep25Bash();
const { root, clonePath } = setupFixture();
// Run against both `main` (the conventional default) and `trunk` (a non-
// main default that exercises the symbolic-ref code path). Keeping both
// restores main coverage that was removed when the fixture switched
// wholesale to trunk in 80f14cac.
for (const defaultBranch of ['main', 'trunk']) {
test(`new quick-task branch branches off origin/${defaultBranch} (#2916)`, () => {
const bash = extractStep25Bash();
const { root, clonePath } = setupFixture(defaultBranch);
try {
// Sanity: we begin sitting on the previous quick-task branch, 1 ahead of origin/trunk.
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'quick/01-prev-task'
);
assert.equal(
git(clonePath, 'rev-list', '--count', 'origin/trunk..HEAD'),
'1',
'fixture should be 1 commit ahead of origin/trunk'
);
try {
const upstream = `origin/${defaultBranch}`;
runStep(bash, clonePath, 'quick/02-new-task');
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'quick/01-prev-task'
);
assert.equal(
git(clonePath, 'rev-list', '--count', `${upstream}..HEAD`),
'1',
`fixture should be 1 commit ahead of ${upstream}`
);
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'quick/02-new-task',
'Step 2.5 should switch to the new quick-task branch'
);
runStep(bash, clonePath, 'quick/02-new-task');
const inherited = git(clonePath, 'rev-list', '--count', 'origin/trunk..HEAD');
assert.equal(
inherited,
'0',
`new quick-task branch must branch off origin/trunk, but inherited ${inherited} commit(s) from previous-task HEAD`
);
assert.equal(
git(clonePath, 'rev-parse', 'HEAD'),
git(clonePath, 'rev-parse', 'origin/trunk'),
'new quick-task branch tip must equal origin/trunk tip'
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
assert.equal(
git(clonePath, 'rev-parse', '--abbrev-ref', 'HEAD'),
'quick/02-new-task',
'Step 2.5 should switch to the new quick-task branch'
);
const inherited = git(clonePath, 'rev-list', '--count', `${upstream}..HEAD`);
assert.equal(
inherited,
'0',
`new quick-task branch must branch off ${upstream}, but inherited ${inherited} commit(s) from previous-task HEAD`
);
assert.equal(
git(clonePath, 'rev-parse', 'HEAD'),
git(clonePath, 'rev-parse', upstream),
`new quick-task branch tip must equal ${upstream} tip`
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
}
test('Step 2.5 reuses an existing quick-task branch instead of forking again', () => {
const bash = extractStep25Bash();