Skip to content

Commit d808c2a

Browse files
test+scripts: bare-clone everywhere
Test helpers + scripts updated to match the bare-repo invariant from openPublicStore. createTestRepo / createFullDataRepo now build the bare gitdir + use a transient working-tree clone to seed initial commits via push, then discard the working tree. seedFixtures opens the bare directly via openRepo({ gitDir }). New seedRawToml helper (apps/api/tests/helpers/seed-fixtures.ts) wraps the transient-clone-push dance for ad-hoc TOML seeds — replaces the identical-shape git-add-commit blocks that lived in account-claim, auth, github-oauth, and saml tests. internal-reload + import-laddr test rigs: local clone is --bare; imp-laddr's ensureBranchCheckedOut uses update-ref + symbolic-ref instead of git checkout. internal-reload's post-reload sanity check reads the file via `git show HEAD:<path>` instead of filesystem. scrub-data: source repo opens bare; .gitsheets configs copied to the target via `git ls-tree` + `git show` instead of filesystem readdir. All 241 API tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 98a76b8 commit d808c2a

13 files changed

Lines changed: 189 additions & 129 deletions

apps/api/scripts/import-laddr/importer.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -557,34 +557,44 @@ async function ensureGitRepo(repo: string): Promise<void> {
557557
}
558558

559559
/**
560-
* Check out the target branch in the working tree. On first run, create it
561-
* from `origin/<branch>` if available, falling back to `initialParent`
562-
* (typically `origin/empty`).
560+
* Point HEAD at the target branch in the bare data repo. On first run,
561+
* the branch ref is created from `origin/<branch>` if available, falling
562+
* back to `initialParent` (typically `origin/empty`).
563+
*
564+
* Bare-friendly — no working tree to check out into. The branch ref is
565+
* created/updated via `git update-ref`, and HEAD becomes a symbolic-ref
566+
* pointing at it so subsequent transacts commit onto the right branch.
563567
*/
564568
async function ensureBranchCheckedOut(
565569
repo: string,
566570
branch: string,
567571
initialParent: string,
568572
): Promise<void> {
569-
// Existing local branch: just switch.
570-
try {
571-
await exec('git', ['rev-parse', '--verify', `refs/heads/${branch}`], { cwd: repo });
572-
await exec('git', ['checkout', branch], { cwd: repo });
573-
return;
574-
} catch {
575-
// No local branch — fall through.
576-
}
577-
// No local branch yet. Try origin/<branch>, fall back to initialParent.
578-
let parent: string;
573+
// Resolve the parent commit hash: either the existing local branch (use
574+
// it as-is), origin/<branch> if it exists, or the initialParent fallback.
575+
let parentCommit: string;
579576
try {
580-
await exec('git', ['rev-parse', '--verify', `refs/remotes/origin/${branch}`], {
581-
cwd: repo,
582-
});
583-
parent = `origin/${branch}`;
577+
const result = await exec('git', ['rev-parse', '--verify', `refs/heads/${branch}`], { cwd: repo });
578+
parentCommit = result.stdout.trim();
584579
} catch {
585-
parent = initialParent;
580+
// No local branch — try origin/<branch>, fall back to initialParent.
581+
let parentRef: string;
582+
try {
583+
await exec('git', ['rev-parse', '--verify', `refs/remotes/origin/${branch}`], {
584+
cwd: repo,
585+
});
586+
parentRef = `refs/remotes/origin/${branch}`;
587+
} catch {
588+
parentRef = initialParent;
589+
}
590+
const result = await exec('git', ['rev-parse', '--verify', parentRef], { cwd: repo });
591+
parentCommit = result.stdout.trim();
592+
await exec('git', ['update-ref', `refs/heads/${branch}`, parentCommit], { cwd: repo });
586593
}
587-
await exec('git', ['checkout', '-b', branch, parent], { cwd: repo });
594+
595+
// Point HEAD at the (now-existing) branch so subsequent gitsheets
596+
// transacts commit onto it.
597+
await exec('git', ['symbolic-ref', 'HEAD', `refs/heads/${branch}`], { cwd: repo });
588598
}
589599

590600
// ---------------------------------------------------------------------------

apps/api/scripts/scrub-data.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ export async function scrubRepo(opts: ScrubOptions): Promise<ScrubResult> {
476476
// -------------------------------------------------------------------------
477477
// 1. Open source repo
478478
// -------------------------------------------------------------------------
479-
const sourceRepo = await openRepo({ workTree: source, gitDir: join(source, '.git') });
479+
const sourceRepo = await openRepo({ gitDir: source });
480480
const sourceHeadHash = await sourceRepo.resolveRef('HEAD');
481481

482482
// -------------------------------------------------------------------------
@@ -585,16 +585,24 @@ export async function scrubRepo(opts: ScrubOptions): Promise<ScrubResult> {
585585
await writeFile(fullPath, content, 'utf-8');
586586
}
587587

588-
// Copy .gitsheets config from source
589-
const sourceGitsheetsDir = join(source, '.gitsheets');
588+
// Copy .gitsheets configs from source's HEAD tree (source is a bare clone,
589+
// per specs/behaviors/storage.md → "The data clone is bare", so the configs
590+
// live as git blobs rather than working-tree files).
590591
const targetGitsheetsDir = join(target, '.gitsheets');
591592
await mkdir(targetGitsheetsDir, { recursive: true });
592-
const configFiles = await readdir(sourceGitsheetsDir, { withFileTypes: true }).catch(() => []);
593-
for (const entry of configFiles) {
594-
if (entry.isFile() && entry.name.endsWith('.toml')) {
595-
const configContent = await readFile(join(sourceGitsheetsDir, entry.name), 'utf-8');
596-
await writeFile(join(targetGitsheetsDir, entry.name), configContent, 'utf-8');
597-
}
593+
const lsTreeResult = await exec('git', [
594+
'--git-dir', source,
595+
'ls-tree', '--name-only', 'HEAD', '.gitsheets/',
596+
]);
597+
const configFiles = lsTreeResult.stdout
598+
.split('\n')
599+
.filter((name) => name.endsWith('.toml'));
600+
for (const path of configFiles) {
601+
const blob = await exec('git', [
602+
'--git-dir', source,
603+
'show', `HEAD:${path}`,
604+
]);
605+
await writeFile(join(target, path), blob.stdout, 'utf-8');
598606
}
599607

600608
// -------------------------------------------------------------------------

apps/api/tests/account-claim.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2121
import { type FastifyInstance } from 'fastify';
2222
import { execFile } from 'node:child_process';
2323
import { promisify } from 'node:util';
24-
import { writeFile, mkdir, readFile } from 'node:fs/promises';
24+
import { writeFile, readFile } from 'node:fs/promises';
2525
import { join } from 'node:path';
2626
import bcrypt from 'bcryptjs';
2727

2828
import { buildApp } from '../src/app.js';
2929
import { issueClaimPending, issueSession } from '../src/auth/jwt.js';
3030
import { createFullDataRepo, createPrivateStorageDir } from './helpers/test-full-repo.js';
31+
import { seedRawToml } from './helpers/seed-fixtures.js';
3132

3233
const exec = promisify(execFile);
3334
const JWT_KEY = 'test-jwt-signing-key-at-least-32-chars!!';
@@ -52,7 +53,6 @@ async function seedPerson(
5253
id: string,
5354
opts: SeedPersonOpts = {},
5455
): Promise<void> {
55-
const git = (...args: string[]) => exec('git', args, { cwd: repoDir });
5656
const lines = [
5757
`id = "${id}"`,
5858
`slug = "${slug}"`,
@@ -68,14 +68,7 @@ async function seedPerson(
6868
lines.push(`slackSamlNameId = "${opts.slackSamlNameId}"`);
6969
}
7070

71-
await mkdir(join(repoDir, 'people'), { recursive: true });
72-
await writeFile(join(repoDir, 'people', `${slug}.toml`), lines.join('\n'));
73-
await git('add', `people/${slug}.toml`);
74-
await git(
75-
'-c', 'user.email=test@cfp.test',
76-
'-c', 'user.name=test',
77-
'commit', '-m', `seed person ${slug}`,
78-
);
71+
await seedRawToml(repoDir, `people/${slug}.toml`, lines.join('\n'), `seed person ${slug}`);
7972
}
8073

8174
async function seedPrivateProfile(

apps/api/tests/auth.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,13 @@
2020
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2121
import { type FastifyInstance } from 'fastify';
2222
import { SignJWT } from 'jose';
23-
import { execFile } from 'node:child_process';
24-
import { promisify } from 'node:util';
25-
import { writeFile, mkdir } from 'node:fs/promises';
26-
import { join } from 'node:path';
2723

2824
import { buildApp } from '../src/app.js';
2925
import { mintSessionFor } from '../src/auth/issue.js';
3026
import { verifyAccess, verifyRefresh } from '../src/auth/jwt.js';
3127
import { createFullDataRepo, createPrivateStorageDir } from './helpers/test-full-repo.js';
28+
import { seedRawToml } from './helpers/seed-fixtures.js';
3229

33-
const exec = promisify(execFile);
3430
const JWT_KEY = 'test-jwt-signing-key-at-least-32-chars!!';
3531

3632
async function buildTestApp(
@@ -61,7 +57,6 @@ async function seedPerson(
6157
id: string,
6258
accountLevel = 'user',
6359
): Promise<void> {
64-
const git = (...args: string[]) => exec('git', args, { cwd: repoDir });
6560
const personToml = [
6661
`id = "${id}"`,
6762
`slug = "${slug}"`,
@@ -71,14 +66,7 @@ async function seedPerson(
7166
`updatedAt = "2026-05-01T00:00:00Z"`,
7267
].join('\n');
7368

74-
await mkdir(join(repoDir, 'people'), { recursive: true });
75-
await writeFile(join(repoDir, 'people', `${slug}.toml`), personToml);
76-
await git('add', `people/${slug}.toml`);
77-
await git(
78-
'-c', 'user.email=test@cfp.test',
79-
'-c', 'user.name=test',
80-
'commit', '-m', `seed person ${slug}`,
81-
);
69+
await seedRawToml(repoDir, `people/${slug}.toml`, personToml, `seed person ${slug}`);
8270
}
8371

8472
// ---------------------------------------------------------------------------

apps/api/tests/cutover-mailout.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function seedPerson(
2222
repoPath: string,
2323
fields: { id: string; slug: string; fullName?: string; githubUserId?: number; deletedAt?: string },
2424
): Promise<void> {
25-
const repo = await openRepo({ gitDir: `${repoPath}/.git`, workTree: repoPath });
25+
const repo = await openRepo({ gitDir: repoPath });
2626
await repo.transact(
2727
{ message: `seed person ${fields.slug}`, author: { name: 'test', email: 'test@cfp.test' } },
2828
async (tx) => {

apps/api/tests/github-oauth.test.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,17 @@
1717
*/
1818
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
1919
import { type FastifyInstance } from 'fastify';
20-
import { execFile } from 'node:child_process';
21-
import { promisify } from 'node:util';
22-
import { writeFile, mkdir, readFile } from 'node:fs/promises';
20+
import { writeFile, readFile } from 'node:fs/promises';
2321
import { join } from 'node:path';
2422
import { SignJWT } from 'jose';
2523

2624
import { buildApp } from '../src/app.js';
2725
import { verifyAccess, verifyRefresh, verifyClaimPending } from '../src/auth/jwt.js';
2826
import { verifyOAuthSession } from '../src/auth/oauth-session-cookie.js';
2927
import { createFullDataRepo, createPrivateStorageDir } from './helpers/test-full-repo.js';
28+
import { seedRawToml } from './helpers/seed-fixtures.js';
3029
import { createGitHubMock } from './helpers/mocks.js';
3130

32-
const exec = promisify(execFile);
3331
const JWT_KEY = 'test-jwt-signing-key-at-least-32-chars!!';
3432
const GH_CLIENT_ID = 'test-client-id';
3533
const GH_CLIENT_SECRET = 'test-client-secret';
@@ -47,7 +45,6 @@ async function seedPerson(
4745
id: string,
4846
opts: SeedPersonOpts = {},
4947
): Promise<void> {
50-
const git = (...args: string[]) => exec('git', args, { cwd: repoDir });
5148
const lines = [
5249
`id = "${id}"`,
5350
`slug = "${slug}"`,
@@ -66,14 +63,7 @@ async function seedPerson(
6663
lines.push(`githubLinkedAt = "${opts.githubLinkedAt}"`);
6764
}
6865

69-
await mkdir(join(repoDir, 'people'), { recursive: true });
70-
await writeFile(join(repoDir, 'people', `${slug}.toml`), lines.join('\n'));
71-
await git('add', `people/${slug}.toml`);
72-
await git(
73-
'-c', 'user.email=test@cfp.test',
74-
'-c', 'user.name=test',
75-
'commit', '-m', `seed person ${slug}`,
76-
);
66+
await seedRawToml(repoDir, `people/${slug}.toml`, lines.join('\n'), `seed person ${slug}`);
7767
}
7868

7969
/** Seed a PrivateProfile directly into the filesystem private store. */

apps/api/tests/helpers/seed-fixtures.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,54 @@
88
* In production the write-api will construct these path fields from the
99
* in-memory index at write time. Tests must do the same.
1010
*/
11+
import { execFile } from 'node:child_process';
12+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
13+
import { tmpdir } from 'node:os';
14+
import { dirname, join } from 'node:path';
15+
import { promisify } from 'node:util';
16+
1117
import { openRepo } from 'gitsheets';
1218

19+
const execAsync = promisify(execFile);
20+
21+
/**
22+
* Write a raw TOML file into a bare gitsheets repo via a transient
23+
* working-tree clone. The bare-repo invariant
24+
* (specs/behaviors/storage.md → "The data clone is bare") rules out the
25+
* traditional `git add` / `git commit` flow against the data path — this
26+
* helper does the transient-clone-push dance once per call so test
27+
* fixtures can land arbitrary file shapes.
28+
*
29+
* `relPath` is the path within the working tree (e.g. `people/jane.toml`).
30+
* `commitMessage` is used verbatim; author/committer default to the
31+
* test identity.
32+
*/
33+
export async function seedRawToml(
34+
bareRepoPath: string,
35+
relPath: string,
36+
toml: string,
37+
commitMessage: string,
38+
): Promise<void> {
39+
const wt = await mkdtemp(join(tmpdir(), 'cfp-seed-wt-'));
40+
try {
41+
await execAsync('git', ['clone', bareRepoPath, wt]);
42+
await execAsync('git', ['config', 'user.email', 'test@cfp.test'], { cwd: wt });
43+
await execAsync('git', ['config', 'user.name', 'cfp test'], { cwd: wt });
44+
await execAsync('git', ['config', 'commit.gpgsign', 'false'], { cwd: wt });
45+
await execAsync('git', ['config', 'core.hooksPath', '/dev/null'], { cwd: wt });
46+
47+
const absPath = join(wt, relPath);
48+
await mkdir(dirname(absPath), { recursive: true });
49+
await writeFile(absPath, toml);
50+
51+
await execAsync('git', ['add', relPath], { cwd: wt });
52+
await execAsync('git', ['commit', '-m', commitMessage], { cwd: wt });
53+
await execAsync('git', ['push', 'origin', 'main'], { cwd: wt });
54+
} finally {
55+
await rm(wt, { recursive: true, force: true });
56+
}
57+
}
58+
1359
const NOW = '2026-05-01T00:00:00Z';
1460
const NOW2 = '2026-05-10T00:00:00Z';
1561

@@ -34,7 +80,7 @@ export interface SeededFixtures {
3480
* Returns the IDs/slugs for use in assertions.
3581
*/
3682
export async function seedFixtures(repoPath: string): Promise<SeededFixtures> {
37-
const repo = await openRepo({ gitDir: `${repoPath}/.git`, workTree: repoPath });
83+
const repo = await openRepo({ gitDir: repoPath });
3884

3985
const projectId = uuid(1);
4086
const personId = uuid(2);

apps/api/tests/helpers/test-full-repo.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,42 @@ export interface FullTestRepo {
4343
* for full-app tests where the store plugin boots the real openPublicStore().
4444
*/
4545
export async function createFullDataRepo(): Promise<FullTestRepo> {
46-
const dir = await mkdtemp(join(tmpdir(), 'cfp-full-data-'));
47-
const git = (...args: string[]) => execFileAsync('git', args, { cwd: dir });
46+
const root = await mkdtemp(join(tmpdir(), 'cfp-full-data-'));
47+
const bareDir = join(root, 'data.git');
48+
const seedDir = join(root, 'seed');
4849

49-
await git('init', '-b', 'main');
50-
await git('config', 'user.email', 'test@cfp.test');
51-
await git('config', 'user.name', 'cfp test');
52-
await git('config', 'commit.gpgsign', 'false');
53-
await git('config', 'core.hooksPath', '/dev/null');
54-
await git('commit', '--allow-empty', '-m', 'initial');
50+
// Bare gitdir — the path tests pass to openPublicStore. Matches the
51+
// production bare-repo invariant (specs/behaviors/storage.md).
52+
await execFileAsync('git', ['init', '--bare', '-b', 'main', bareDir]);
53+
await execFileAsync('git', ['config', 'receive.denyCurrentBranch', 'ignore'], { cwd: bareDir });
5554

56-
// Write all sheet configs
57-
await mkdir(join(dir, '.gitsheets'), { recursive: true });
55+
// Transient working-tree clone for seeding the sheet configs.
56+
await execFileAsync('git', ['init', '-b', 'main', seedDir]);
57+
const seedGit = (...args: string[]) => execFileAsync('git', args, { cwd: seedDir });
58+
await seedGit('config', 'user.email', 'test@cfp.test');
59+
await seedGit('config', 'user.name', 'cfp test');
60+
await seedGit('config', 'commit.gpgsign', 'false');
61+
await seedGit('config', 'core.hooksPath', '/dev/null');
62+
await seedGit('commit', '--allow-empty', '-m', 'initial');
63+
64+
await mkdir(join(seedDir, '.gitsheets'), { recursive: true });
5865
for (const [name, config] of Object.entries(SHEET_CONFIGS)) {
59-
await writeFile(join(dir, '.gitsheets', `${name}.toml`), config);
66+
await writeFile(join(seedDir, '.gitsheets', `${name}.toml`), config);
6067
}
61-
await git('add', '.gitsheets');
62-
await git('commit', '-m', 'chore: add all gitsheets sheet configs');
68+
await seedGit('add', '.gitsheets');
69+
await seedGit('commit', '-m', 'chore: add all gitsheets sheet configs');
70+
71+
await seedGit('remote', 'add', 'origin', bareDir);
72+
await seedGit('push', 'origin', 'main');
73+
await rm(seedDir, { recursive: true, force: true });
6374

6475
let cleaned = false;
6576
return {
66-
path: dir,
77+
path: bareDir,
6778
cleanup: async () => {
6879
if (cleaned) return;
6980
cleaned = true;
70-
await rm(dir, { recursive: true, force: true });
81+
await rm(root, { recursive: true, force: true });
7182
},
7283
};
7384
}

0 commit comments

Comments
 (0)