From a738eb73105ebfe0f21e78423b4216f64259abcb Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:30:05 +0200 Subject: [PATCH] fix(tests): clean up every test temp dir on process exit, not just the harness one --- CHANGELOG.md | 8 ++++ package.json | 2 +- tests/helpers/setup.js | 5 +++ tests/helpers/tmp-cleanup.js | 59 +++++++++++++++++++++++++++++ tests/tmp_cleanup.test.js | 72 ++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/helpers/tmp-cleanup.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 062ec493..989d0a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [Unreleased] + +### Fixed +- Die Testsuite ließ pro Lauf 65 temporäre Verzeichnisse in `/tmp` zurück. 59 von 70 Testdateien legen mit `fs.mkdtempSync` ein eigenes Verzeichnis an und entfernen es nie; der vorhandene Aufräummechanismus in `tests/helpers/setup.js` deckte nur dessen eigenes Präfix `gc-test-` ab. Nach genügend Läufen lief `/tmp` voll — und das äußerte sich in Symptomen ohne jeden Bezug zu Tests, bis hin zu Prozessen, die ihre Ausgabe nicht mehr schreiben konnten. + Statt 59 Dateien einzeln nachzurüsten wird `mkdtempSync` jetzt einmal zentral umhüllt (`tests/helpers/tmp-cleanup.js`, über `--require` in jeden Testprozess vorgeladen) und beim Prozessende geräumt. Künftige Testdateien sind damit automatisch abgedeckt. Betrifft ausschließlich die Testumgebung — der Produktivcode legt keine temporären Verzeichnisse an. + +--- + ## [1.118.9] — 2026-07-26 ### Fixed diff --git a/package.json b/package.json index 57dd1c6b..dcbeb15d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node src/server.js", "dev": "node --watch src/server.js", - "test": "node --test --test-force-exit tests/" + "test": "node --require ./tests/helpers/tmp-cleanup.js --test --test-force-exit tests/" }, "dependencies": { "@callmetechie/gatecontrol-config-hash": "^1.2.0", diff --git a/tests/helpers/setup.js b/tests/helpers/setup.js index 63a353af..b0f7283a 100644 --- a/tests/helpers/setup.js +++ b/tests/helpers/setup.js @@ -4,6 +4,11 @@ const path = require('node:path'); const fs = require('node:fs'); const os = require('node:os'); const supertest = require('supertest'); +// Räumt die temp-Verzeichnisse ALLER Testdateien auf, nicht nur das gc-test- +// dieser Datei. Wird zusätzlich über --require aus dem test-Skript vorgeladen; +// hier nochmals, damit auch ein direkter Einzeldatei-Lauf ohne npm aufräumt. +// Der require-Cache sorgt dafür, dass die Umhüllung nur einmal greift. +require('./tmp-cleanup'); // Set test env before any imports const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gc-test-')); diff --git a/tests/helpers/tmp-cleanup.js b/tests/helpers/tmp-cleanup.js new file mode 100644 index 00000000..bc127fff --- /dev/null +++ b/tests/helpers/tmp-cleanup.js @@ -0,0 +1,59 @@ +'use strict'; + +// Wird über --require aus dem test-Skript in JEDEN Testprozess vorgeladen. +// +// Hintergrund: 59 von 70 Testdateien legen mit fs.mkdtempSync ein Verzeichnis +// unter os.tmpdir() an und entfernen es nie. Jeder Suite-Lauf hinterlässt so +// hunderte Verzeichnisse; nach genügend Läufen ist /tmp voll — und das äußert +// sich in Symptomen, die nichts mit Tests zu tun haben (ENOSPC in beliebigen +// anderen Kommandos, bis hin zu Prozessen, die ihre Ausgabe nicht mehr +// schreiben können). +// +// Statt 59 Dateien einzeln nachzurüsten — und die nächste neue Datei wieder zu +// vergessen — wird mkdtempSync hier einmal umhüllt und beim Prozessende +// geräumt. Neue Tests sind damit automatisch abgedeckt, ohne dass jemand daran +// denken muss. +// +// Bewusst NUR im Testlauf aktiv: das Modul wird ausschließlich über das +// test-Skript vorgeladen, der Produktivcode ruft mkdtempSync nirgends auf. + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const created = new Set(); +const realMkdtempSync = fs.mkdtempSync; + +// Einmal auflösen: unter macOS ist /tmp ein Symlink auf /private/tmp, ein +// naiver Präfixvergleich würde dort nie greifen. +let tmpRoot; +try { + tmpRoot = fs.realpathSync(os.tmpdir()); +} catch { + tmpRoot = os.tmpdir(); +} + +fs.mkdtempSync = function mkdtempSync(prefix, ...rest) { + const dir = realMkdtempSync.call(this, prefix, ...rest); + try { + // Nur aufräumen, was wirklich unterhalb des Temp-Wurzelverzeichnisses + // liegt. Ein Test, der bewusst woandershin schreibt, bleibt unangetastet. + const resolved = fs.realpathSync(dir); + if (resolved.startsWith(tmpRoot + path.sep)) created.add(resolved); + } catch { + // Verzeichnis schon weg oder nicht auflösbar — dann gibt es nichts zu tun. + } + return dir; +}; + +process.on('exit', () => { + for (const dir of created) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch { + // Aufräumen darf einen Testlauf niemals scheitern lassen. + } + } +}); + +module.exports = { _created: created }; diff --git a/tests/tmp_cleanup.test.js b/tests/tmp_cleanup.test.js index 00629678..58bca009 100644 --- a/tests/tmp_cleanup.test.js +++ b/tests/tmp_cleanup.test.js @@ -41,3 +41,75 @@ test('cleanupStaleTestDirs removes stale dirs, keeps fresh / unrelated / exclude test('cleanupStaleTestDirs never throws on a missing root', () => { assert.doesNotThrow(() => cleanupStaleTestDirs(path.join(os.tmpdir(), 'gc-does-not-exist-xyz'), 'gc-test-', 1000, null)); }); + +// ── Generische Bereinigung für ALLE Testdateien ──────────────────────────── +// Der Sweeper oben deckt nur das Präfix `gc-test-` dieses Harness ab. 59 von 70 +// Testdateien legen jedoch mit eigenem Präfix an (`gc-caddy-retry-`, `gc-env-`, +// `gc-cc-*` …) und entfernen nie. Jeder Suite-Lauf hinterließ dadurch hunderte +// Verzeichnisse, bis /tmp volllief — mit Symptomen, die nichts mit Tests zu tun +// hatten (ENOSPC in beliebigen anderen Kommandos). +// +// Geprüft wird die EIGENSCHAFT, nicht die Aufrufliste: ein Prozess, der ein +// temp-Verzeichnis anlegt, hinterlässt keines. Damit ist auch jede künftige +// Testdatei abgedeckt, ohne dass jemand daran denken muss. + +const { execFileSync } = require('node:child_process'); +const ROOT = path.join(__dirname, '..'); +const PRELOAD = path.join(ROOT, 'tests', 'helpers', 'tmp-cleanup.js'); +const CHILD = ` +const fs = require('node:fs'), os = require('node:os'), path = require('node:path'); +const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gc-cleanup-probe-')); +fs.writeFileSync(path.join(d, 'inhalt.txt'), 'x'); +fs.mkdirSync(path.join(d, 'unterordner')); +process.stdout.write(d); +`; + +function runChild(preload) { + const args = preload ? ['--require', PRELOAD, '-e', CHILD] : ['-e', CHILD]; + return execFileSync(process.execPath, args, { encoding: 'utf8' }).trim(); +} + +test('without the preload a temp dir survives the process — the bug this guards', () => { + const dir = runChild(false); + try { + assert.equal(fs.existsSync(dir), true, 'Voraussetzung dieses Tests stimmt nicht mehr'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test('with the preload the temp dir — including its contents — is gone on exit', () => { + // Der Kindprozess legt Datei und Unterordner an: ein rmSync ohne `recursive` + // würde hier scheitern. + const dir = runChild(true); + assert.equal(fs.existsSync(dir), false, `Verzeichnis blieb liegen: ${dir}`); +}); + +test('the preload leaves directories outside the temp root alone', () => { + const outside = fs.mkdtempSync(path.join(ROOT, 'nicht-temp-')); + try { + const child = `const fs = require('node:fs'); + process.stdout.write(fs.mkdtempSync(${JSON.stringify(path.join(outside, 'x-'))}));`; + const dir = execFileSync(process.execPath, ['--require', PRELOAD, '-e', child], { encoding: 'utf8' }).trim(); + assert.equal(fs.existsSync(dir), true, 'ein Verzeichnis außerhalb von os.tmpdir() wurde entfernt'); + } finally { + fs.rmSync(outside, { recursive: true, force: true }); + } +}); + +test('mkdtempSync still behaves normally while the process runs', () => { + const out = execFileSync(process.execPath, ['--require', PRELOAD, '-e', + `const fs=require('node:fs'),os=require('node:os'),p=require('node:path'); + const d=fs.mkdtempSync(p.join(os.tmpdir(),'gc-probe-rc-')); + process.stdout.write(String(fs.existsSync(d)) + '|' + d);`], { encoding: 'utf8' }).trim(); + const [existedDuringRun, dir] = out.split('|'); + assert.equal(existedDuringRun, 'true', 'das Verzeichnis existierte während des Laufs nicht'); + assert.match(dir, /gc-probe-rc-/); +}); + +test('the npm test script preloads the cleanup', () => { + // Ohne diesen Eintrag greift die Bereinigung im Suite-Lauf nicht — und genau + // der Suite-Lauf hat /tmp gefüllt. + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + assert.match(pkg.scripts.test, /--require \.\/tests\/helpers\/tmp-cleanup\.js/); +});