From 4af02536c57f9b53ad9959981e6f26f8eb268468 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 19 Oct 2026 21:24:21 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A8=20Keep=20generated=20pagination=20?= =?UTF-8?q?fixtures=20free=20of=20automatic=20maintenance=20(#948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useGeneratedFixture` writes 1002 blobs and commits them, and a commit returns before the maintenance it starts does. Nothing in the fixture asked Git not to start it, so the generated checkout could leave a detached writer running past the command that launched it — repacking objects while a read enumerates them, and recreating `objects/info` after a removal had already passed `objects`. That is the ENOTEMPTY the saved trace caught. The fixture now sets `maintenance.auto` to false in its own checkout, immediately after `git init` and before the first object is written, so no command it runs — its own commit, or any later ordinary `fixture.run` — has an automatic launch to make. The policy is local to that temporary checkout and the helper's already neutralized environment, so nothing outside it is configured. The new GO5 case is a discriminator rather than a reading of the configuration. On a real 1002-file fixture holding no pack and at least 1002 loose objects, an empty commit that selects Git's own loose-object task and keeps it in the foreground leaves the pack set empty; the identical command with `maintenance.auto=true` added for that one command writes a new `loose-*.pack`. Configuration text alone cannot satisfy both halves, and an absence of maintenance work cannot pass on its own. Production code, the shared Git and temporary-directory helpers and every existing observation are unchanged. No read or removal error is tolerated, retried or caught here, and the temporary directory keeps its existing owner. Co-Authored-By: Claude Opus 5 --- packages/git/tests/git-observations.test.ts | 47 ++++++++++++++++++++- packages/git/tests/support/observations.ts | 3 ++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/git/tests/git-observations.test.ts b/packages/git/tests/git-observations.test.ts index 14439de91..b18c97e32 100644 --- a/packages/git/tests/git-observations.test.ts +++ b/packages/git/tests/git-observations.test.ts @@ -16,7 +16,7 @@ import { InMemoryStream } from "@executablemd/durable-streams"; import { scoped, until } from "effection"; import { join } from "node:path"; import { utimes } from "node:fs/promises"; -import { lstat } from "@effectionx/fs"; +import { lstat, readdir } from "@effectionx/fs"; import { collect, execute, inlineSource, registerComponents } from "@executablemd/core"; import { canonicalJson } from "@executablemd/workflow"; import { COMPOSITION_ORIGIN } from "../src/composition/definitions.ts"; @@ -833,6 +833,51 @@ describe("GO5 — pages are bounded, complete, and about one observation", () => expect([...(windows[0] ?? "")]).toHaveLength(70000); expect(whole).toBe(fixture.output("diff", "--no-ext-diff", "--no-textconv", "--no-color")); }); + + it("keeps generated fixture commits free of automatic maintenance", function* () { + const fixture = yield* useGeneratedFixture(1002); + const directory = join(fixture.root, ".git", "objects", "pack"); + const packs = function* (): Operation { + return (yield* readdir(directory)).filter((name) => name.endsWith(".pack")); + }; + + expect(yield* packs()).toEqual([]); + const counted = /^count: (\d+)$/m.exec(fixture.run("count-objects", "-v")); + expect(counted).not.toBe(null); + expect(Number(counted?.[1])).toBeGreaterThanOrEqual(1002); + + // The loose-object task alone, in the foreground, so what a commit starts + // automatically is this command's own work rather than a detached writer's. + const task = [ + "-c", + "maintenance.strategy=gc", + "-c", + "maintenance.gc.enabled=false", + "-c", + "maintenance.loose-objects.enabled=true", + "-c", + "maintenance.autoDetach=false", + "-c", + "gc.autoDetach=false", + ]; + + fixture.run(...task, "commit", "--allow-empty", "-m", "maintenance-disabled"); + expect(yield* packs()).toEqual([]); + + // The same corpus and the same command with the policy overridden for this + // one command, which is what proves the empty set above was the policy. + fixture.run( + ...task, + "-c", + "maintenance.auto=true", + "commit", + "--allow-empty", + "-m", + "maintenance-enabled-control", + ); + const written = (yield* packs()).filter((name) => name.startsWith("loose-")); + expect(written.length).toBeGreaterThanOrEqual(1); + }); }); describe("GO6 — a changed observation cannot be paged over", () => { diff --git a/packages/git/tests/support/observations.ts b/packages/git/tests/support/observations.ts index db5da3471..646653edf 100644 --- a/packages/git/tests/support/observations.ts +++ b/packages/git/tests/support/observations.ts @@ -228,6 +228,9 @@ export function* useGeneratedFixture(count: number): Operation const root = join(parent, "checkout"); yield* ensureDir(root); git(["init", "--initial-branch=main", "."], root, home); + // A commit returns before the maintenance it starts does, so a detached + // writer would outlive this fixture's own commands and race what reads it. + git(["config", "--local", "maintenance.auto", "false"], root, home); git(["config", "user.name", "Fixture"], root, home); git(["config", "user.email", "fixture@example.invalid"], root, home); const paths: string[] = [];