Runner: Dev-Server Start/Abbruch-Korrekturen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71f9243bf2
commit
6cd50bba9f
2 files changed
+103
-15
No files matched your search
+73
-6
@@ -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']);
|
||||
});
|
||||
Reference in new issue
Block a user