Runner: Dev-Server Start/Abbruch-Korrekturen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 21:53:41 +00:00
1 parent 71f9243bf2
commit 6cd50bba9f
2 files changed
+103 -15

No files matched your search

+73 -6
View File
@@ -5,14 +5,14 @@ import { createDev } from '../src/dev.js';
const MIN = 60e3;
const GB = 1024 ** 3;
function setup({ mem = 10 * GB, connect, detected, prep } = {}) {
function setup({ mem = 10 * GB, connect, detected, prep, rmImpl } = {}) {
const config = { workRoot: '/w/work', cacheRoot: '/w/cache', baseHost: 'project.webklar.com', minFreeBytes: 1.5 * GB, devIdleMs: 20 * MIN, devMax: 3, devReadyMs: 180e3, giteaToken: 'tok_GEHEIM' };
const clock = { t: 1_000_000 };
const runs = []; const removed = []; const held = new Map(); const prepared = [];
const rmDirs = []; const runs = []; const removed = []; const held = new Map(); const prepared = [];
let psResult = [];
const docker = {
run: async (args, opts) => { if (args[0] === 'run') runs.push({ args, opts }); else opts?.onLine?.('container-log-zeile', 'out'); return { code: 0 }; },
rm: async (n) => { removed.push(n); return { code: 0, lines: [] }; },
rm: async (n) => { removed.push(n); if (rmImpl) await rmImpl(n); return { code: 0, lines: [] }; },
ps: async () => psResult,
};
const jobs = {
@@ -25,8 +25,9 @@ function setup({ mem = 10 * GB, connect, detected, prep } = {}) {
connect: connect ?? (async (h, p) => { connects.push([h, p]); return true; }),
setTimer: (fn, ms) => { clock.t += ms; queueMicrotask(fn); return 0; },
prepareDirs: async () => {},
rmDir: async (p) => { rmDirs.push(p); },
});
return { dev, clock, runs, removed, held, prepared, connects, config, setPs: (v) => { psResult = v; } };
return { rmDirs, dev, clock, runs, removed, held, prepared, connects, config, setPs: (v) => { psResult = v; } };
}
const job = (sub, o = {}) => ({ sub, repo: `own/${sub}`, sha: 'abcdef1', branch: 'main', mode: 'dev', env: { API_KEY: 'geheim-wert' }, ...o });
@@ -158,13 +159,13 @@ test('Cache wird gehalten solange der Container läuft, stop gibt frei', async (
assert.deepEqual(s.dev.list(), []);
});
test('stop während des Starts: Container entfernt, nie online', async () => {
test('stop während des Starts: nie online, kein Container', async () => {
const s = setup();
await s.dev.ensure('a', job('a'));
await s.dev.stop('a'); await s.dev.idle();
assert.equal(s.dev.status('a').state, 'off');
assert.equal(s.held.get('own/a'), 0);
assert.ok(s.removed.includes('preview-dev-a'));
assert.equal(s.runs.length, 0); // prepare läuft hier evtl. schon (nicht abbrechbar), der Container nie
});
test('Protokoll maskiert Variablenwerte und ist auf 300 Zeilen begrenzt', async () => {
@@ -182,3 +183,69 @@ test('Start: verwaiste preview-dev-*-Container werden entfernt', async () => {
await s.dev.cleanupOrphans();
assert.deepEqual(s.removed, ['preview-dev-x', 'preview-dev-y']);
});
const tick = () => new Promise((r) => setImmediate(r));
test('stop während Verdrängungs-rm: kein Cache-Leck, kein prepare, Arbeitsordner entfernt', async () => {
let gate; const opened = new Promise((r) => { gate = r; });
let slow = false;
const s = setup({ rmImpl: async () => { if (slow) await opened; } });
for (const n of ['a', 'b', 'c']) { await s.dev.ensure(n, job(n)); await s.dev.idle(); s.clock.t += MIN; }
slow = true;
await s.dev.ensure('d', job('d')); // verdrängt a, wartet auf dessen rm
await tick();
const stopping = s.dev.stop('d');
gate(); await stopping; await s.dev.idle();
assert.equal(s.held.get('own/d'), 0);
assert.ok(!s.prepared.some((p) => p.job.sub === 'd'));
assert.equal(s.dev.status('d').state, 'off');
assert.ok(s.rmDirs.includes('/w/work/dev/d'));
});
test('sha-Wechsel während des Starts: genau ein Container des neuen sha, alter entfernt neuen nie', async () => {
let release; const gate = new Promise((r) => { release = r; });
let first = true;
const s = setup({ prep: async (j, o) => { if (first) { first = false; await gate; } return { workDir: o.workDir, detected: { pm: 'npm', devCmd: ['npm', 'run', 'dev'], devPort: 5173 } }; } });
await s.dev.ensure('a', job('a'));
await s.dev.ensure('a', job('a', { sha: '1234567' }));
release(); await s.dev.idle();
assert.equal(s.runs.length, 1);
assert.equal(s.removed.length, 0); // alter Eintrag hat nie einen Container angelegt
assert.equal(s.dev.status('a').sha, '1234567'); assert.equal(s.dev.status('a').state, 'online');
assert.equal(s.held.get('own/a'), 1);
});
test('gleichzeitig sweep + ensure(neuer sha): ein Container, neuer bleibt online', async () => {
const s = setup();
await s.dev.ensure('a', job('a')); await s.dev.idle();
s.clock.t += 21 * MIN;
const p = Promise.all([s.dev.sweep(), s.dev.ensure('a', job('a', { sha: '1234567' }))]);
await p; await s.dev.idle();
assert.equal(s.runs.length, 2);
assert.equal(s.dev.status('a').state, 'online'); assert.equal(s.dev.status('a').sha, '1234567');
assert.equal(s.held.get('own/a'), 1);
// das letzte rm gehört dem alten Container und liegt vor dem zweiten run
assert.ok(s.removed.every((n) => n === 'preview-dev-a'));
});
test('failed-Eintrag mit gleichem sha: Neuversuch frühestens nach 60 s', async () => {
let ok = false;
const s = setup({ connect: async () => { if (!ok) throw new Error('nein'); return true; } });
await s.dev.ensure('a', job('a')); await s.dev.idle();
assert.equal(s.dev.status('a').state, 'failed');
const prepared = s.prepared.length;
assert.deepEqual(await s.dev.ensure('a', job('a')), { state: 'failed' });
assert.equal(s.prepared.length, prepared);
s.clock.t += 61e3; ok = true;
assert.deepEqual(await s.dev.ensure('a', job('a')), { state: 'starting' });
await s.dev.idle();
assert.equal(s.dev.status('a').state, 'online');
});
test('cleanupOrphans: ein fehlschlagendes rm stoppt die übrigen nicht', async () => {
const s = setup({ rmImpl: async (n) => { if (n === 'preview-dev-x') throw new Error('kaputt'); } });
s.setPs(['preview-dev-x', 'preview-dev-y']);
const orig = console.error; console.error = () => {};
try { await s.dev.cleanupOrphans(); } finally { console.error = orig; }
assert.deepEqual(s.removed, ['preview-dev-x', 'preview-dev-y']);
});