diff --git a/dist/commands/demo.js b/dist/commands/demo.js index 79b5a8db..f8866096 100644 --- a/dist/commands/demo.js +++ b/dist/commands/demo.js @@ -43,6 +43,14 @@ const git = (args, cwd) => execFileSync('git', args, { GIT_COMMITTER_EMAIL: 'demo@commitlore.example', }, }).trim(); +/** + * Why a cleanup failed, in one line. + * + * `Error.message` from `fs` already carries the errno and the syscall -- + * `EACCES: permission denied, rmdir '/tmp/...'` -- which is the part that says + * whether the next occurrence is a race, a permission, or a mount. + */ +const reasonFor = (error) => error instanceof Error ? error.message : String(error); /** * Runs the demo scenario in a temporary repository. * @@ -59,11 +67,22 @@ export const runDemo = async (opts = {}) => { // Signal handler for cleanup on interrupt const cleanup = () => { if (tmpDir !== undefined) { + const removing = tmpDir; try { - rmSync(tmpDir, { recursive: true, force: true }); + // `maxRetries` because the failure being handled is a race with a + // writer rather than a permanent condition: node retries `EBUSY`, + // `EMFILE`, `ENFILE`, `ENOTEMPTY` and `EPERM` for this option, which is + // the set a concurrent writer produces. + rmSync(removing, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); } - catch { - // Best-effort cleanup + catch (error) { + // Reported, never rethrown. This also runs from a signal handler, where + // a throw has nowhere to go, and on the crash path it must not mask the + // error it is unwinding. What cannot happen again is losing it: the + // leftover directory reached CI with no cause attached, and the `catch` + // that discarded the errno was the only reason it could not be read + // (#1163). + process.stderr.write(`commitlore demo: could not remove ${removing}: ${reasonFor(error)}\n`); } tmpDir = undefined; } @@ -90,6 +109,17 @@ export const runDemo = async (opts = {}) => { git(['config', 'user.name', 'CommitLore Demo'], tmpDir); git(['config', 'user.email', 'demo@commitlore.example'], tmpDir); git(['config', 'commit.gpgsign', 'false'], tmpDir); + // A throwaway repository must not start anything that outlives the command. + // `git commit` may spawn background maintenance (`gc.auto`, + // `maintenance.auto`), and a git process still writing inside the directory + // while `rmSync` walks it is the most plausible reading of the one leftover + // directory CI has reported: a removal that raced a writer, not one that + // never ran (#1163). Written into the repository's own config rather than + // passed per invocation, so anything this demo starts later -- `runInit`'s + // hooks included -- inherits it, and so the setting is readable on a + // directory that outlived a failed cleanup. + git(['config', 'gc.auto', '0'], tmpDir); + git(['config', 'maintenance.auto', 'false'], tmpDir); // Create the target file so the path exists const targetFullPath = join(tmpDir, targetPath); mkdirSync(dirname(targetFullPath), { recursive: true }); diff --git a/dist/commands/demo.js.map b/dist/commands/demo.js.map index 734806ca..e74ca220 100644 --- a/dist/commands/demo.js.map +++ b/dist/commands/demo.js.map @@ -1 +1 @@ -{"version":3,"file":"demo.js","sourceRoot":"","sources":["../../src/commands/demo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAInD,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,UAAU,EACV,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAoB,MAAM,kBAAkB,CAAC;AAE9D,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AAEpE,MAAM,aAAa,GAAG,CAAC,QAAiB,EAAiB,EAAE;IACzD,MAAM,QAAQ,GAAG,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAC9C,IAAI,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACnD,OAAO,uCAAuC,QAAQ,yEAAyE,CAAC;AAClI,CAAC,CAAC;AA2BF,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E,MAAM,GAAG,GAAG,CAAC,IAAc,EAAE,GAAW,EAAU,EAAE,CAClD,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;IACxB,GAAG;IACH,QAAQ,EAAE,MAAM;IAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC/B,GAAG,EAAE;QACH,GAAG,OAAO,CAAC,GAAG;QACd,eAAe,EAAE,iBAAiB;QAClC,gBAAgB,EAAE,yBAAyB;QAC3C,kBAAkB,EAAE,iBAAiB;QACrC,mBAAmB,EAAE,yBAAyB;KAC/C;CACF,CAAC,CAAC,IAAI,EAAE,CAAC;AAEZ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,EAAE,OAAoB,EAAE,EAAuB,EAAE;IAC3E,8EAA8E;IAC9E,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3D,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAChD,CAAC;IAED,IAAI,MAA0B,CAAC;IAE/B,0CAA0C;IAC1C,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;YACD,MAAM,GAAG,SAAS,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAS,EAAE;QAC1B,OAAO,EAAE,CAAC;QACV,wCAAwC;QACxC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC,CAAC;IAEF,uDAAuD;IACvD,OAAO,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAChD,OAAO,CAAC,mBAAmB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEjD,IAAI,CAAC;QACH,oEAAoE;QACpE,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;QAEzE,uDAAuD;QACvD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACnD,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,WAAW,KAAK,OAAO,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC;YAC9G,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QACxF,CAAC;QAED,oDAAoD;QACpD,GAAG,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC1F,GAAG,CAAC,CAAC,QAAQ,EAAE,WAAW,EAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;QACxD,GAAG,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,yBAAyB,CAAC,EAAE,MAAM,CAAC,CAAC;QACjE,GAAG,CAAC,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;QAEnD,4CAA4C;QAC5C,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAChD,SAAS,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,aAAa,CAAC,cAAc,EAAE,2CAA2C,CAAC,CAAC;QAC3E,GAAG,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAE1B,2DAA2D;QAC3D,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,wBAAwB,CAAC,EAAE,MAAM,CAAC,CAAC;QAExD,sDAAsD;QACtD,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QAED,oFAAoF;QACpF,aAAa,CACX,cAAc,EACd,yFAAyF,CAC1F,CAAC;QACF,GAAG,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1B,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,sBAAsB,CAAC,EAAE,MAAM,CAAC,CAAC;QAEtD,0DAA0D;QAC1D,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QAEzB,gFAAgF;QAChF,MAAM,WAAW,GAAgB,QAAQ,CAAC;YACxC,GAAG,EAAE,MAAM;YACX,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,IAAI,IAAI,EAAE;SACf,CAAC,CAAC;QAEH,gDAAgD;QAChD,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,wCAAwC,UAAU,EAAE,CAAC,CAAC;QACjE,KAAK,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;QAC9E,KAAK,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAC;QAC1F,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,gFAAgF,CAAC,CAAC;QAC7F,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,IAAI,WAAW,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC5C,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,MAAM,IAAI,WAAW,CAAC,OAAO,EAAE,CAAC;gBACzC,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,IAAI,SAAS,CAAC;gBACxC,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;gBACnC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;gBAC1E,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,WAAW,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;gBACjF,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,SAAS,GAAG,CAAC,CAAC;gBAChD,IAAI,KAAK;oBAAE,KAAK,CAAC,IAAI,CAAC,cAAc,KAAK,EAAE,CAAC,CAAC;gBAC7C,IAAI,QAAQ;oBAAE,KAAK,CAAC,IAAI,CAAC,kBAAkB,QAAQ,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,6BAA6B,sBAAsB,aAAa,CAAC,CAAC;QAC7E,2EAA2E;QAC3E,uEAAuE;QACvE,qEAAqE;QACrE,0EAA0E;QAC1E,+DAA+D;QAC/D,KAAK,CAAC,IAAI,CAAC,yFAAyF,CAAC,CAAC;QACtG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;QACV,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC3C,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC;AAEF,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAgB,EAAQ,EAAE;IACjD,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,sFAAsF,CAAC;SACnG,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAC;QAC/B,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAC7C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC;QACD,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACrC,CAAC,CAAC,CAAC;AACP,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"demo.js","sourceRoot":"","sources":["../../src/commands/demo.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/commitlore.mjs b/dist/commitlore.mjs index 8b96dec2..529d64e6 100755 --- a/dist/commitlore.mjs +++ b/dist/commitlore.mjs @@ -28416,6 +28416,7 @@ var git = (args, cwd) => execFileSync("git", args, { GIT_COMMITTER_EMAIL: "demo@commitlore.example" } }).trim(); +var reasonFor2 = (error2) => error2 instanceof Error ? error2.message : String(error2); var runDemo = async (opts = {}) => { const platformError = checkPlatform(opts.platformOverride); if (platformError !== null) { @@ -28424,9 +28425,12 @@ var runDemo = async (opts = {}) => { let tmpDir; const cleanup = () => { if (tmpDir !== void 0) { + const removing = tmpDir; try { - rmSync9(tmpDir, { recursive: true, force: true }); - } catch { + rmSync9(removing, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); + } catch (error2) { + process.stderr.write(`commitlore demo: could not remove ${removing}: ${reasonFor2(error2)} +`); } tmpDir = void 0; } @@ -28448,6 +28452,8 @@ var runDemo = async (opts = {}) => { git(["config", "user.name", "CommitLore Demo"], tmpDir); git(["config", "user.email", "demo@commitlore.example"], tmpDir); git(["config", "commit.gpgsign", "false"], tmpDir); + git(["config", "gc.auto", "0"], tmpDir); + git(["config", "maintenance.auto", "false"], tmpDir); const targetFullPath = join21(tmpDir, targetPath); mkdirSync11(dirname14(targetFullPath), { recursive: true }); writeFileSync17(targetFullPath, "export const calculatePrice = () => {};\n"); diff --git a/installer/canonical-artifact.json b/installer/canonical-artifact.json index 5fb31db2..7992ef85 100644 --- a/installer/canonical-artifact.json +++ b/installer/canonical-artifact.json @@ -15,10 +15,10 @@ "tsconfig.json", "src" ], - "sha256": "01c4ceb94a72be4e24462a478af20300e3da5bfb7a885c630fcb20122a9c3762" + "sha256": "782e4d82ccc92f82289706def5f34ebe64630fe1795d256d1e35be257512106b" }, "artifact": { - "sha256": "148696fcc3cda54db7e539c2cb63ab40eebc325e7ff5c2ab7c82a8599ed75e8b", + "sha256": "67bcaf9020c78e2a9cee84d85c1bf8fc84a86399e5ae2172a3d346ef4f6811a8", "files": [ { "path": "dist/cli.d.ts", @@ -98,11 +98,11 @@ }, { "path": "dist/commands/demo.js", - "sha256": "075a2c08ef33c1f19da4c0a286f6e500376e72f6b2f6094cab7bcb8d67c476fb" + "sha256": "c09a47c98b42a9460babe4bb3d7a3bd25d0e58747fb014b14e230d322860ee63" }, { "path": "dist/commands/demo.js.map", - "sha256": "d257b82db28b64631dbb3f603cbeacd60bb38aa60feab5c320438074b099051a" + "sha256": "3ba91143c82428198f1150f45ac23ed5ba8b69b967878456a00596dbb58bfd36" }, { "path": "dist/commands/doctor.d.ts", @@ -682,7 +682,7 @@ }, { "path": "dist/commitlore.mjs", - "sha256": "9e3b770f9248d50e2abc86a5616de65bf195260cce9085faa7273036d5df5919" + "sha256": "3b5dc90fc9d14106afc5f822d08741c121d463f46eb75752d78dccc1863c2b3e" }, { "path": "dist/core/agent-configs.d.ts", diff --git a/src/commands/demo.ts b/src/commands/demo.ts index 520ef4c5..b5c0998a 100644 --- a/src/commands/demo.ts +++ b/src/commands/demo.ts @@ -83,6 +83,16 @@ const git = (args: string[], cwd: string): string => }, }).trim(); +/** + * Why a cleanup failed, in one line. + * + * `Error.message` from `fs` already carries the errno and the syscall -- + * `EACCES: permission denied, rmdir '/tmp/...'` -- which is the part that says + * whether the next occurrence is a race, a permission, or a mount. + */ +const reasonFor = (error: unknown): string => + error instanceof Error ? error.message : String(error); + /** * Runs the demo scenario in a temporary repository. * @@ -101,10 +111,21 @@ export const runDemo = async (opts: DemoOptions = {}): Promise => { // Signal handler for cleanup on interrupt const cleanup = (): void => { if (tmpDir !== undefined) { + const removing = tmpDir; try { - rmSync(tmpDir, { recursive: true, force: true }); - } catch { - // Best-effort cleanup + // `maxRetries` because the failure being handled is a race with a + // writer rather than a permanent condition: node retries `EBUSY`, + // `EMFILE`, `ENFILE`, `ENOTEMPTY` and `EPERM` for this option, which is + // the set a concurrent writer produces. + rmSync(removing, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); + } catch (error) { + // Reported, never rethrown. This also runs from a signal handler, where + // a throw has nowhere to go, and on the crash path it must not mask the + // error it is unwinding. What cannot happen again is losing it: the + // leftover directory reached CI with no cause attached, and the `catch` + // that discarded the errno was the only reason it could not be read + // (#1163). + process.stderr.write(`commitlore demo: could not remove ${removing}: ${reasonFor(error)}\n`); } tmpDir = undefined; } @@ -136,6 +157,17 @@ export const runDemo = async (opts: DemoOptions = {}): Promise => { git(['config', 'user.name', 'CommitLore Demo'], tmpDir); git(['config', 'user.email', 'demo@commitlore.example'], tmpDir); git(['config', 'commit.gpgsign', 'false'], tmpDir); + // A throwaway repository must not start anything that outlives the command. + // `git commit` may spawn background maintenance (`gc.auto`, + // `maintenance.auto`), and a git process still writing inside the directory + // while `rmSync` walks it is the most plausible reading of the one leftover + // directory CI has reported: a removal that raced a writer, not one that + // never ran (#1163). Written into the repository's own config rather than + // passed per invocation, so anything this demo starts later -- `runInit`'s + // hooks included -- inherits it, and so the setting is readable on a + // directory that outlived a failed cleanup. + git(['config', 'gc.auto', '0'], tmpDir); + git(['config', 'maintenance.auto', 'false'], tmpDir); // Create the target file so the path exists const targetFullPath = join(tmpDir, targetPath); diff --git a/test/demo.test.ts b/test/demo.test.ts index 562b2071..993bf3c3 100644 --- a/test/demo.test.ts +++ b/test/demo.test.ts @@ -11,7 +11,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { createTestRepo } from './git-fixtures.js'; import { runDemo } from '../src/commands/demo.js'; @@ -82,6 +82,90 @@ describe('commitlore demo', () => { expect(readdirSync(caseRoot)).toEqual([]); }); + /** + * bug-issue-1163. The crash-cleanup test went red once in CI naming a + * leftover `commitlore-demo-*` directory, passed on re-run of the same + * commit, and passed on the other node leg of the same run. Nothing said why, + * because `cleanup` discarded the `rmSync` error — so the one occurrence + * carried no errno, and a race, a permission and a full disk were + * indistinguishable from each other and from "the removal never ran". + * + * The failure is injected rather than provoked: a real race is not reliably + * reproducible, and a test that waits for one would be the flake it is meant + * to explain. What is pinned here is the reporting — a cleanup that fails + * says so, naming the directory and the reason — plus the repository setting + * that removes the most plausible writer. + */ + it('reports a cleanup failure instead of discarding it (bug-issue-1163)', async () => { + const caseRoot = mkdtempSync(join(demoRoot, 'cleanupfail-')); + const stderr: string[] = []; + + vi.resetModules(); + vi.doMock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: actual, + rmSync: (): never => { + throw Object.assign( + new Error(`EACCES: permission denied, rmdir '${caseRoot}/injected'`), + { code: 'EACCES' }, + ); + }, + }; + }); + + try { + const { runDemo: isolated } = await import('../src/commands/demo.js'); + const spy = vi + .spyOn(process.stderr, 'write') + .mockImplementation((chunk: unknown): boolean => { + stderr.push(String(chunk)); + return true; + }); + + let thrown: unknown; + try { + await isolated({ cwd: userRepo, crashTest: true, tmpRoot: caseRoot }); + } catch (error) { + thrown = error; + } finally { + spy.mockRestore(); + } + + // The error being unwound still reaches the caller: reporting the cleanup + // failure must not replace the reason the run ended. + expect((thrown as Error | undefined)?.message).toContain('simulated crash'); + + const reported = stderr.join(''); + expect(reported).toContain('could not remove'); + // The two things the CI occurrence lacked: which directory, and why. + expect(reported).toContain(caseRoot); + expect(reported).toContain('EACCES'); + } finally { + vi.doUnmock('node:fs'); + vi.resetModules(); + } + + // Arrival: the injection really did stop the removal, so the assertions + // above were made about a cleanup that failed rather than one that never + // happened. The directory is also the artifact the next assertion reads. + const leftOver = readdirSync(caseRoot); + expect(leftOver).toHaveLength(1); + const repo = join(caseRoot, leftOver[0] as string); + + // The demo's repository forbids background maintenance, so `git commit` + // cannot leave a process writing inside the directory that is about to be + // removed — the mechanism this issue's one occurrence is most consistent + // with. + const config = (key: string): string => + execFileSync('git', ['-C', repo, 'config', '--get', key], { encoding: 'utf8' }).trim(); + expect(config('gc.auto')).toBe('0'); + expect(config('maintenance.auto')).toBe('false'); + + rmSync(caseRoot, { recursive: true, force: true }); + }); + it('user repository is never written to (safety property)', async () => { await runDemo({ cwd: userRepo, tmpRoot: demoRoot }); // HEAD must be unchanged