Files
jason.woltje 7e0d3c93f3 Fix project inspectGit race with git maintenance (Vikunja #780)
Fixture helpers that commit to a repository the product later inspects now pass
maintenance.auto=false and gc.auto=0, as the product's own git calls already do,
so a detached git maintenance run no longer writes pack temp files mid-walk.

inspectGit now refuses a git metadata entry that vanishes between readdir and
lstat (or between listing a directory and reading it) with project-repository,
'git metadata changed during inspection', instead of leaking a raw ENOENT.
It still refuses rather than skipping or retrying. A missing .git keeps its
existing error.

A deterministic test removes the named entry from inside a replaced readdirSync,
for both a file and a directory.
2026-11-02 17:55:40 -06:00

65 lines
3.4 KiB
JavaScript

// Row 175 (#1673, mos-0872 Q2): `project init --tester` initializes only a repository under
// <dataRoot>/projects/<business>/, and its layout commit names `tester` as author and committer.
import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { initProject, testerProjectArea } from '../src/index.mjs';
const git = (repo, ...args) => execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'maintenance.auto=false', '-c', 'gc.auto=0', '-C', repo, ...args], { encoding: 'utf8' }).trim();
function context(t, repo) {
const root = mkdtempSync(join(tmpdir(), 'project-tester-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const dataRoot = join(root, 'data');
return { root, business: { id: 'acme', human: 'jason', projects: { stack: { root: join(root, 'code'), ...(repo ? { repo: repo(root, dataRoot) } : {}) } } }, project: 'stack', dataRoot };
}
test('row 175: init with author tester commits the layout as tester; without it, as the human', async (t) => {
const c = context(t);
const { repo, commit } = await initProject(c, { author: 'tester' });
assert.equal(repo, join(c.dataRoot, 'projects/acme/stack'));
assert.equal(git(repo, 'show', '-s', '--format=%an|%cn', commit), 'tester|tester');
assert.equal(git(repo, 'rev-list', '--count', 'HEAD'), '1');
const h = context(t);
const human = await initProject(h);
assert.equal(git(human.repo, 'show', '-s', '--format=%an|%cn', human.commit), 'jason|jason');
});
test('row 175: init refuses an author that is no id before it makes any directory', async (t) => {
const c = context(t);
for (const author of ['', 'two words', '../x', 7])
await assert.rejects(initProject(c, { author }), { code: 'project-id' }, String(author));
assert.equal(existsSync(c.dataRoot), false);
});
test('row 175: testerProjectArea passes a repository inside the business area and refuses the area itself and any other, touching nothing', (t) => {
const inside = [
null,
(root, dataRoot) => join(dataRoot, 'projects', 'acme', 'records'),
(root, dataRoot) => join(dataRoot, 'projects', 'acme', 'deep', 'records'),
];
for (const repo of inside) {
const c = context(t, repo);
assert.equal(testerProjectArea(c), repo ? repo(c.root, c.dataRoot) : join(c.dataRoot, 'projects', 'acme', 'stack'));
}
const outside = [
(root) => join(root, 'records'),
(root, dataRoot) => join(dataRoot, 'records'),
(root, dataRoot) => join(dataRoot, 'projects', 'other', 'stack'),
// A sibling whose name starts with the business id is not inside it.
(root, dataRoot) => join(dataRoot, 'projects', 'acme-2', 'stack'),
// The area itself: a repository there would hold the other projects' default repositories.
(root, dataRoot) => join(dataRoot, 'projects', 'acme'),
];
for (const repo of outside) {
const c = context(t, repo);
assert.throws(() => testerProjectArea(c), { code: 'tester-project-area' }, repo(c.root, c.dataRoot));
assert.equal(existsSync(c.dataRoot), false);
assert.equal(existsSync(repo(c.root, c.dataRoot)), false);
}
// projectRepo's own checks still come first.
assert.throws(() => testerProjectArea({ ...context(t), project: 'missing' }), { code: 'project-not-declared' });
});