Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions tests/helpers/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-'));
Expand Down
59 changes: 59 additions & 0 deletions tests/helpers/tmp-cleanup.js
Original file line number Diff line number Diff line change
@@ -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 };
72 changes: 72 additions & 0 deletions tests/tmp_cleanup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Loading