mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-11 22:53:04 +00:00
Some checks failed
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat: flat workspace index follows the checked-out branch (#2354) A plain `gitnexus analyze` now always targets the flat workspace slot, updating it incrementally across branch switches instead of auto-routing non-owner branches into `branches/<slug>/` sub-indexes (disk bloat) or nagging with the primary-inversion "run gitnexus clean" warning. No new CLI flag or config key: the smart behavior is the default. - Placement: only explicit `--branch` consults resolveBranchPlacement; plain runs resolve to the flat slot, `meta.branch` becomes an informational "last analyzed branch" label restamped each run. - Fast path: a same-commit clean-tree branch flip restamps the label and registry entry (adoptFlatBranchLabel, no-op for unregistered repos). - Shadow cleanup: when the flat slot adopts a label that has a pinned sub-index, the now-unreachable `branches/<slug>/` dir and its registry summary are removed together. - MCP: applyBranchScope always falls back to the on-disk flat meta before throwing "not indexed", so long-lived servers resolve a freshly restamped workspace branch. - status: no more "current branch not indexed" dead end — falls through to the workspace index with an informational line and the usual commit-based staleness verdict. - Deleted primaryInversionWarning; explicit `--branch` pinning, the checkout-mismatch guard, detached-HEAD/CI behavior, and `clean --branch` are unchanged. Supersedes the flag-based approaches in #2358/#2359. Closes #2354. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): check registry before deleting shadowed sub-index (#2364 review F2) adoptFlatBranchLabel ran the branches/<slug>/ rm before its own unregistered-repo no-op check, so a repo in the #2264 half-finalized state (up to date but unregistered) lost its pinned sub-index on a same-commit branch flip while the run still failed. The registry lookup now precedes the deletion, making the no-self-heal rule (#2264/#1169) cover disk as well as registry state. The 'never self-heals' unit test now materializes a sub-index dir and asserts it survives; the run-analyze #2354 fast-path test registers its repo under an isolated GITNEXUS_HOME (deletion is only legitimate for registered repos) with a new unregistered variant pinning dir survival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): keep branch summary when sub-index rm fails (#2364 review F4) The shadow-cleanup fs.rm swallowed every error while the registry summary was dropped unconditionally. On Windows an lbug held open by a live MCP server fails the rm with EBUSY/EPERM, and once the summary is gone 'clean --branch' can never target the leftover dir (it resolves solely via the recorded summary) — stranding the exact un-cleanable disk bloat adoptFlatBranchLabel exists to prevent. The summary is now dropped only when the directory is verifiably gone (post-rm existence check); on failure the summary is retained, a warning names the path and errno, and the informational branch label still restamps. Later adopts retry the rm. New repo-manager-rm-failure.test.ts uses the delegating fs/promises mock idiom (vi.spyOn cannot intercept ESM namespace exports). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(core): restamp fast path adopt-first and tolerate read-only storage (#2364 review F3) The fast-path label sync stamped meta before adoptFlatBranchLabel, so a crash or adopt failure between the two flipped the retry guard (existingMeta.branch !== branchLabel) and locked in the partial state: every subsequent same-commit run skipped the cleanup and branch-scoped queries kept routing to the stale pinned sub-index. The block also sat outside any try/catch, so a same-commit branch flip on a read-only .gitnexus mount (the documented Docker :ro workflow, #1549) failed a byte-for-byte-current analyze over a purely informational label sync. Adopt now runs first and saveMeta last — any partial failure leaves the guard true and the next run self-heals — and the whole sync is best-effort: read-only errors warn citing #1549, anything else warns and retries next run. Safe because the block only fires on a same-commit clean tree, where the flat DB content is byte-valid for both labels. isReadOnlyFilesystemError is now exported. New run-analyze-adopt-failure.test.ts covers retry-after-partial- failure, adopt-before-stamp ordering, and EROFS/EACCES/EPERM (gaps 4 and 7); a detached-HEAD fast-path pin lands in run-analyze.test.ts (gap 6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): make flat meta authoritative in applyBranchScope (#2364 review F1) applyBranchScope trusted two pieces of cached state before its flat- meta disk fallback, and the handle cache only refreshes on a resolve miss — never on a hit. Post-#2354 that stale window is the routine case: (i) the handle.branch early-return served the flat handle under the OLD label after a workspace flip, silently returning the new branch's content as the old branch (the pool staleness reinit hot- swaps content without updating handle.branch); (ii) a stale cached branches[] summary routed to a branches/<slug>/ dir that adoptFlatBranchLabel had already deleted (raw 'LadybugDB not found' or POSIX ghost reads with staleness detection blinded). The on-disk flat meta is now read before any cached-state trust. A branches[] summary is served only when its sub-index lbug actually exists (the lbug is what the pool opens — serviceability truth); the cached label is trusted only when no readable flat meta contradicts it (#2106 R4 legacy shapes preserved). One refreshRepos() fires on detected staleness so subsequent calls see fresh handles. Safe against mid-analyze reads: dirty stamps spread the existing meta, preserving the old label until the end-of-run atomic write. Fixtures now materialize the pinned sub-index lbug; new regressions cover the stale-old-label error, adopted-summary fall-through to flat, and the dangling-summary partial-failure window (test gaps 1-2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(core): make end-of-run branch-label sync best-effort (#2364 review F5) The end-of-run adoptFlatBranchLabel sat inside the pipeline try whose catch rethrows, so a registry write failure (ENOSPC, ~/.gitnexus perms) after a successful multi-minute analyze failed the whole run — even though the index was complete and registered, the neighbouring parse-cache save is deliberately wrapped for exactly this reason, and adopt retries unconditionally on the next plain analyze. It now warns and continues, mirroring the parse-cache wrapper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): correct branch-not-indexed guidance for workspace index (#2364 review F6) The error told users to 'Run: gitnexus analyze --branch <X>', but post-#2354 that command hard-errors unless X is checked out — and this message is now the common goodbye for a formerly-indexed branch whose sub-index the workspace slot adopted. The guidance now explains that the workspace index follows the checked-out branch and leads with the checkout; the '(primary only)' fallback becomes '(workspace only)'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: align primary/workspace vocabulary with the #2354 inversion (#2364 review F7) The review flagged pre-inversion 'primary/non-primary' wording that now misleads readers about the placement model: the isPrimaryBranch JSDoc (field name kept — public API surface), the two branches? JSDoc comments in local-backend, the base_ref gate comment in cli/analyze, and four branch-scope test names. Comment/JSDoc/test-name edits only; 'Registry-primary' and 'primary key' senses untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): clarify workspace index status wording (#2364 review F8) 'gitnexus analyze follows this branch' was ambiguous about WHICH branch analyze follows — the recorded one on the line or the current checkout. Both locales now say a re-run follows the current branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(storage): re-read registry after the shadow rm in adoptFlatBranchLabel The F2 reorder moved the registry read to the top of the function, so the whole-file writeRegistry at the bottom persisted a snapshot taken BEFORE the recursive rm of an entire sub-index — widening the unlocked read-modify-write window from microseconds to the duration of a multi- hundred-MB delete. A concurrent registerRepo/removeBranchIndex writer in that window was silently clobbered (the #2106 R9 lost-update class; registerRepo re-reads before writing for exactly this reason). The top read is now a cheap membership gate only (the F2 no-op guarantee); the mutate re-reads its own fresh snapshot after the rm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: treat only provably-absent errno as gone in the new existence probes Both probes added by this series inverted the codebase's provably- absent polarity (listRegisteredRepos validate prunes only on ENOENT/ENOTDIR): adoptFlatBranchLabel's dirGone check read ANY fs.access failure — including a transient EACCES/EIO on a surviving dir — as 'verifiably gone' and dropped the summary, recreating exactly the stranded-bloat bug F4 fixed; applyBranchScope's sub-index check read the same transient errors on a healthy pinned lbug as 'adopted/ deleted', producing a false 'not indexed' error. A resolved force:true rm now proves absence without a probe; on failure the probe treats only ENOENT/ENOTDIR as gone, and a non-missing lbug serves the handle so the pool open surfaces the real error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): harden applyBranchScope stale-state coherence Four residual gaps in the new arm structure, found by post-fix review: - The stale-label error listed the just-contradicted cached label as indexed ('not indexed: main. Indexed branches: main'). The message now derives the flat label from the authoritative meta and excludes the requested branch from the hint list. - A branch pinned AFTER the server cached its handle never triggered a refresh (resolve hits skip the miss-refresh), erroring until restart. Every miss now fires exactly one best-effort refreshRepos() before the error, so the next call resolves; a refresh-once guard keeps doubly-stale resolutions to a single registry re-scan. - A registry entry claiming the branch both as flat label and pinned summary (the rm-failed adopt-degraded state) could serve the stale- vintage pin under a label the flat slot owns; the summary arm now requires handle.branch !== branch and the degraded state errors honestly. - The flat-meta match path returned the cached handle's pre-restamp branch/commit/stats; the meta that decided routing now also supplies the metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(core): keep the real error visible in restamp warnings; correct the end-of-run retry claim The fast-path catch replaced the actual error with 'storage is read-only (#1549)' for any EACCES/EPERM — mislabeling ownership problems and transient Windows locks and discarding the only diagnostic signal. The warning now carries the real message with the #1549 hint appended. The end-of-run best-effort comment claimed adopt 'retries unconditionally on the next plain analyze'; same-commit runs take the fast path whose guard compares the already-stamped meta label, so the retry actually lands on the next content-changing run. The comment now states the true retry semantics and why the interim state is safe (flat meta stamped first; applyBranchScope trusts it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: unique tmpdir for the branch-scope fixture; drop redundant dynamic imports The branch-scope describe materialized its sub-index stub under a FIXED os.tmpdir()/gnx-2106-multi path — concurrent vitest runs on one host (the documented parallel-agents workflow) could rm each other's stub between beforeEach and the resolve under test, flaking the pinned-branch tests. The fixture root is now mkdtemp-unique per run with afterAll cleanup. run-analyze.test.ts dynamically imported repo-manager inside test bodies despite the module being statically imported at the top of the file (no vi.mock exists there to justify it); the three call sites now use the static import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
235 lines
11 KiB
TypeScript
235 lines
11 KiB
TypeScript
import { execSync, execFileSync } from 'child_process';
|
|
import fs from 'fs/promises';
|
|
import { existsSync } from 'fs';
|
|
import path from 'path';
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { getStoragePaths, loadMeta, listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
|
import { createTempDir } from '../helpers/test-db.js';
|
|
|
|
/**
|
|
* #2106/#2354 — branch handling end-to-end. Proves that a plain analyze
|
|
* always updates the flat workspace index (following the checked-out working
|
|
* tree, no `branches/` sub-directory, no slot-ownership friction), that an
|
|
* explicit `--branch` run pins a separate index under
|
|
* `.gitnexus/branches/<slug>/` without touching the flat slot, and that a
|
|
* pinned sub-index shadowed by a later plain analyze on the same branch is
|
|
* cleaned up.
|
|
*/
|
|
const git = (args: string[], cwd: string): string =>
|
|
execSync(['git', ...args].join(' '), { cwd, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
|
|
const commit = (cwd: string, message: string): void => {
|
|
git(['-c', 'user.name=test', '-c', 'user.email=test@test', 'commit', '-m', message], cwd);
|
|
};
|
|
|
|
describe('multi-branch analyze (#2106)', () => {
|
|
let tmpHome: Awaited<ReturnType<typeof createTempDir>>;
|
|
let savedGitnexusHome: string | undefined;
|
|
|
|
beforeEach(async () => {
|
|
// Isolate the global registry so the full analyze runs below don't write
|
|
// to the developer's real ~/.gitnexus/registry.json.
|
|
tmpHome = await createTempDir('gitnexus-multibranch-home-');
|
|
savedGitnexusHome = process.env.GITNEXUS_HOME;
|
|
process.env.GITNEXUS_HOME = tmpHome.dbPath;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (savedGitnexusHome === undefined) delete process.env.GITNEXUS_HOME;
|
|
else process.env.GITNEXUS_HOME = savedGitnexusHome;
|
|
await tmpHome.cleanup();
|
|
});
|
|
|
|
it('a plain analyze follows a branch switch into the flat workspace slot (#2354)', async () => {
|
|
const tmp = await createTempDir('gitnexus-multibranch-');
|
|
const repo = tmp.dbPath;
|
|
try {
|
|
git(['init'], repo);
|
|
await fs.writeFile(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'a');
|
|
// Normalise the branch name across git defaults (master vs main).
|
|
git(['branch', '-M', 'main'], repo);
|
|
const mainCommit = git(['rev-parse', 'HEAD'], repo);
|
|
|
|
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
|
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
|
|
|
// First analyze lands in the flat slot, byte-identical layout.
|
|
const flat = getStoragePaths(repo);
|
|
expect(path.dirname(flat.lbugPath)).toBe(flat.storagePath);
|
|
expect(existsSync(flat.lbugPath)).toBe(true);
|
|
const flatMeta = await loadMeta(flat.storagePath);
|
|
expect(flatMeta?.branch).toBe('main');
|
|
expect(flatMeta?.lastCommit).toBe(mainCommit);
|
|
|
|
// Switch to a feature branch with different content and re-analyze.
|
|
git(['checkout', '-b', 'feature/x'], repo);
|
|
await fs.writeFile(path.join(repo, 'b.ts'), 'export const b = 2;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'b');
|
|
const featureCommit = git(['rev-parse', 'HEAD'], repo);
|
|
expect(featureCommit).not.toBe(mainCommit);
|
|
|
|
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
|
|
|
// The flat workspace index followed the working tree — updated in place,
|
|
// no `branches/` sub-directory, no slot-ownership error or warning.
|
|
expect(existsSync(flat.lbugPath)).toBe(true);
|
|
const flatMetaAfter = await loadMeta(flat.storagePath);
|
|
expect(flatMetaAfter?.branch).toBe('feature/x');
|
|
expect(flatMetaAfter?.lastCommit).toBe(featureCommit);
|
|
expect(existsSync(path.join(flat.storagePath, 'branches'))).toBe(false);
|
|
|
|
// The registry follows along: one entry, relabelled, no branches[].
|
|
const entries = await listRegisteredRepos();
|
|
const entry = entries.find((e) => path.resolve(e.path) === path.resolve(repo));
|
|
expect(entry).toBeDefined();
|
|
expect(entry?.branch).toBe('feature/x');
|
|
expect(entry?.lastCommit).toBe(featureCommit);
|
|
expect(entry?.branches).toBeUndefined();
|
|
} finally {
|
|
await tmp.cleanup();
|
|
}
|
|
}, 180_000);
|
|
|
|
it('an explicit --branch run pins a sub-index; a later plain analyze on that branch reclaims it', async () => {
|
|
const tmp = await createTempDir('gitnexus-multibranch-pin-');
|
|
const repo = tmp.dbPath;
|
|
try {
|
|
git(['init'], repo);
|
|
await fs.writeFile(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'a');
|
|
git(['branch', '-M', 'main'], repo);
|
|
const mainCommit = git(['rev-parse', 'HEAD'], repo);
|
|
|
|
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
|
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
|
const flat = getStoragePaths(repo);
|
|
// main records its live chunk keys so a later branch prune can keep them.
|
|
const mainCacheKeys = (await loadMeta(flat.storagePath))?.cacheKeys ?? [];
|
|
expect(mainCacheKeys.length).toBeGreaterThan(0);
|
|
|
|
// Pin the feature branch into its own sub-index with explicit --branch.
|
|
git(['checkout', '-b', 'feature/x'], repo);
|
|
await fs.writeFile(path.join(repo, 'b.ts'), 'export const b = 2;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'b');
|
|
const featureCommit = git(['rev-parse', 'HEAD'], repo);
|
|
|
|
await runFullAnalysis(repo, { branch: 'feature/x' }, { onProgress: () => {} });
|
|
|
|
// The flat (main) index is untouched — NOT overwritten by the pinned run.
|
|
expect(existsSync(flat.lbugPath)).toBe(true);
|
|
const flatMetaAfter = await loadMeta(flat.storagePath);
|
|
expect(flatMetaAfter?.branch).toBe('main');
|
|
expect(flatMetaAfter?.lastCommit).toBe(mainCommit);
|
|
|
|
// The pinned index is a separate DB under branches/<slug>/.
|
|
const branchPaths = getStoragePaths(repo, 'feature/x');
|
|
const branchDir = path.dirname(branchPaths.lbugPath);
|
|
expect(branchDir.includes(path.join('.gitnexus', 'branches'))).toBe(true);
|
|
expect(existsSync(branchPaths.lbugPath)).toBe(true);
|
|
const branchMeta = await loadMeta(branchDir);
|
|
expect(branchMeta?.branch).toBe('feature/x');
|
|
expect(branchMeta?.lastCommit).toBe(featureCommit);
|
|
|
|
// #2106 R6: the pinned analyze must NOT have evicted main's chunks from
|
|
// the SHARED parse cache (they were unioned in via main's recorded keys).
|
|
const { loadParseCache } = await import('../../src/storage/parse-cache.js');
|
|
const sharedCache = await loadParseCache(flat.storagePath);
|
|
const onDisk = sharedCache.onDiskKeys ?? new Set<string>();
|
|
for (const k of mainCacheKeys) {
|
|
expect(onDisk.has(k), `main chunk ${k} survives the feature prune`).toBe(true);
|
|
}
|
|
|
|
// The global registry keeps one entry per path: flat label at top level,
|
|
// the pinned branch nested under branches[] (#2106 U4).
|
|
let entries = await listRegisteredRepos();
|
|
let entry = entries.find((e) => path.resolve(e.path) === path.resolve(repo));
|
|
expect(entry?.branch).toBe('main');
|
|
expect(entry?.branches?.map((b) => b.branch)).toEqual(['feature/x']);
|
|
|
|
// A plain analyze on the pinned branch adopts the flat workspace slot
|
|
// and removes the now-shadowed sub-index (#2354): the flat handle would
|
|
// always win for this label, leaving the sub-index unreachable bloat.
|
|
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
|
const reclaimed = await loadMeta(flat.storagePath);
|
|
expect(reclaimed?.branch).toBe('feature/x');
|
|
expect(reclaimed?.lastCommit).toBe(featureCommit);
|
|
expect(existsSync(branchDir)).toBe(false);
|
|
entries = await listRegisteredRepos();
|
|
entry = entries.find((e) => path.resolve(e.path) === path.resolve(repo));
|
|
expect(entry?.branch).toBe('feature/x');
|
|
expect(entry?.branches).toBeUndefined();
|
|
} finally {
|
|
await tmp.cleanup();
|
|
}
|
|
}, 180_000);
|
|
|
|
it('a detached-HEAD re-analyze preserves the primary stamp (no later overwrite)', async () => {
|
|
const tmp = await createTempDir('gitnexus-multibranch-detached-');
|
|
const repo = tmp.dbPath;
|
|
try {
|
|
git(['init'], repo);
|
|
await fs.writeFile(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'a');
|
|
git(['branch', '-M', 'main'], repo);
|
|
const mainCommit = git(['rev-parse', 'HEAD'], repo);
|
|
|
|
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
|
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
|
const flat = getStoragePaths(repo);
|
|
expect((await loadMeta(flat.storagePath))?.branch).toBe('main');
|
|
|
|
// Detach HEAD (what CI's actions/checkout does) and force a rebuild of the
|
|
// flat/primary index. The primary stamp must NOT be stripped.
|
|
git(['checkout', mainCommit], repo); // detached
|
|
await runFullAnalysis(repo, { force: true }, { onProgress: () => {} });
|
|
expect((await loadMeta(flat.storagePath))?.branch).toBe('main');
|
|
|
|
// Now an explicit --branch analyze must still route to a sub-dir (the
|
|
// stamp survived), leaving the flat index intact rather than updating it.
|
|
git(['checkout', '-b', 'feature/y'], repo);
|
|
await fs.writeFile(path.join(repo, 'b.ts'), 'export const b = 2;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'b');
|
|
await runFullAnalysis(repo, { branch: 'feature/y' }, { onProgress: () => {} });
|
|
|
|
const flatMeta = await loadMeta(flat.storagePath);
|
|
expect(flatMeta?.branch).toBe('main');
|
|
expect(flatMeta?.lastCommit).toBe(mainCommit); // flat NOT touched by the pinned run
|
|
expect(existsSync(getStoragePaths(repo, 'feature/y').lbugPath)).toBe(true);
|
|
} finally {
|
|
await tmp.cleanup();
|
|
}
|
|
}, 180_000);
|
|
|
|
it('an auto-detected branch the rules forbid lands on the flat slot (#2106 R1)', async () => {
|
|
const tmp = await createTempDir('gitnexus-multibranch-r1-');
|
|
const repo = tmp.dbPath;
|
|
try {
|
|
git(['init'], repo);
|
|
await fs.writeFile(path.join(repo, 'a.ts'), 'export const a = 1;\n');
|
|
git(['add', '-A'], repo);
|
|
commit(repo, 'a');
|
|
// A backtick is valid in a git ref but rejected by validateBranchName.
|
|
// execFileSync (no shell) so the backtick is not interpreted.
|
|
execFileSync('git', ['branch', '-M', 'feat`x'], { cwd: repo, stdio: 'pipe' });
|
|
|
|
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
|
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
|
|
|
// The forbidden ref was normalized to null → flat slot, no branch field,
|
|
// and no branches/ sub-directory created for an unqueryable slug.
|
|
const flat = getStoragePaths(repo);
|
|
expect(existsSync(flat.lbugPath)).toBe(true);
|
|
expect((await loadMeta(flat.storagePath))?.branch).toBeUndefined();
|
|
expect(existsSync(path.join(flat.storagePath, 'branches'))).toBe(false);
|
|
} finally {
|
|
await tmp.cleanup();
|
|
}
|
|
}, 180_000);
|
|
});
|