diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 241aded5..5e3a3f62 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -86,6 +86,34 @@ Ein Fehlschlag mit anschließendem PASS beweist weder einen harmlosen Flake noch Ursache, Fix oder Abschluss von KI-24. Weiter auf Windows messen, bevor Fristen oder Wiederholungen geändert werden; kein Produktfix-Paket aus diesem Nachtrag. +### Offener CI-Befund: selftest-win-signal ohne Timeout-Schlusszeile (22.10.2026) + +Weiter **offen**, Stufe niedrig, nur Prüfung: [Run 39834041335, +Linux-1-Job 120017507651](https://github.com/Cuarroc/ProjectA/actions/runs/39834041335/job/120017507651) +scheiterte in Queue #1847 für #1835 (`fc1d4662`) auf Basis `fa4f2d9a`, +Queue-Kopf `84f8d345421c338748a7261a68406f62b6d3e5f4`, am Gate +`selftest-win-signal` mit `never-finishing-run-times-out-with-3 (rc=3)`. +Jobfehler und Quellprädikat wurden von der Koordination unabhängig gelesen; +laut Roots Prüfung des Rohprotokolls stimmte der erwartete Exit-Code 3, +aber die exakte stdout-Zeile `conclusion=timeout` fehlte. Diese Rohbeobachtung +stammt von Root; `gh` verweigerte die Ausgabe wegen roher Terminal-Steuerzeichen. +`scripts/test-win-signal.sh` prüft beide Bedingungen und verwendet für diesen +Poll eine Sekunde Zeitlimit ohne Wartepause (`WIN_SIGNAL_SLEEP=0`). Ein +Timing-Wettlauf oder Flake ist eine Hypothese, keine bewiesene Ursache oder +Entwarnung; Timeout, Wiederholungen und Assertion bleiben unverändert. + +Der erfolgreiche [main-Lauf 39832698357](https://github.com/Cuarroc/ProjectA/actions/runs/39832698357) +auf `fa4f2d9a` traf in `linux_1` nur die Auswahlentscheidung; die eigentlichen +Gates waren `SKIPPED`. Er belegt keinen erfolgreichen Lauf dieses Selbsttests. +Root reihte über Queue #1848 erneut ein; #1835 wurde am 22.10. um 19:07:35Z +als `5a07fc0895ff6bb813df265fa9d730d38d1b1ed9` gemergt. Der Merge allein +beweist weder einen PASS dieses Selbsttests noch Ursache oder Fix; hier wird +kein solcher PASS behauptet. Run-/Jobstatus, Auswahl und Merge wurden per +`gh run view` und `gh pr view` am 22.10. erneut gelesen (Exit 0). +Folgeauftrag `d2d1a8a8-0882-435e-902b-c3cad4e695fb` bleibt offen, niedrig, +nur Prüfung; kein Fix in TASK/ASSIGN. Dieser Eintrag dokumentiert den CI-Befund; +die bestehenden R908-Testzeitfenster sind damit nicht als neuer Fehler belegt. + ## Behoben Historische Befunde samt vollständigen Belegen stehen im diff --git a/docs/PLAN.md b/docs/PLAN.md index 86ab7b5a..0983f15c 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -441,7 +441,7 @@ Stand 15.10.: Testmodule der vier Nähte durch SLIM-M1/S1/A1/P1 ausgelagert (SLI | V16-CHANGELOG-b | [Archiv: Historie][historie] | bestehender Owner | V16-CHANGELOG-a | Server | C | Erledigt #861 (merged 2026-10-09T15:02:01Z) | siehe Archiv | – | | V16-RELEASE | Tag v1.6.0 (Beta-Regel x.y.0) | Nutzer | Z1 (bis DR-17), V16-KI30 (Fix-Abnahme), D5-GUARD, V16-UPD-CANCEL, V16-06, V16-CHANGELOG; #728 gemergt | PC | – | Erledigt: Tag v1.6.0 = d2582ceb (09.10. 23:22Z), release.yml 38004103979 grün, latest.json anonym 1.6.0, PC installiert; ersetzt: Freigabe: Nutzer 09.10. ~19:42Z an Root; Tag durch Root nach Merge dieses PRs (V16-RELEASE-PREP); ersetzt: Geplant: E2/E3 beantwortet; konkrete Freigabe dieses Releases weiterhin offen | jeder Release braucht Nutzerfreigabe; Environment release mit Required Reviewer Cuarroc am 09.10. per gh api bestätigt; danach Release-Pipeline grün | – | | V16-FU | Nachläufer aus v1.6.0-Reviews und Release-Prüfung | Stabschef | v1.6.0 veröffentlicht (V16-RELEASE) | Server | B/C je Punkt | Erledigt 10.10.: SRV-2/SRV-5-Portschutz #931, SRV-3 #952, OWN-3 #930, NOT-1 und R912-FU-1 #941, NOT-2/NOT-3 #928, SRV-4 #927, REL-3 #926, REL-4 #929, R910-FU #937, R889-FU #899 (09.10.); Rest verschoben nach V16-FU-2; ursprünglich: Offen: R908-FU-1 (Wiring-Test stop→limitDrainForSignal), R908-FU-2 (Drain-Kommentar/Idempotenz); R912-FU-1 (existed-Flag statt stat, io-Seam); R910-FU, R896-FU, R899-FU K2, R889-FU; REL-3 (scripts/release.cmd veraltet), REL-4 (Rollback/releases-latest), NOT-1/2/3, SRV-2..5, OWN-3 | je Punkt eigenes Paket mit eigener Abnahme | – | -| V16-FU-2 | Rest aus V16-FU: SRV-5 gegenseitige Authentifizierung (laut #931 weiterhin geparkt), R908-FU-1 (Wiring-Test stop→limitDrainForSignal), R908-FU-2 (Drain-Kommentar/Idempotenz), R896-FU, R899-FU K2 | Stabschef | V16-FU | Server | B/C je Punkt | Geplant: nicht disponiert; ob schon anderswo erledigt: Unbekannt (zu prüfen) | je Punkt eigenes Paket mit eigener Abnahme | – | +| V16-FU-2 | Rest aus V16-FU: SRV-5 gegenseitige Authentifizierung (laut #931 weiterhin geparkt), R908-FU-1 (Wiring-Test stop→limitDrainForSignal), R908-FU-2 (Drain-Kommentar/Idempotenz), R896-FU, R899-FU K2 | Stabschef | V16-FU | Server | B/C je Punkt | In Arbeit: Teillieferung R908-FU-1 nach Merge von #1838: Wiring-Test am echten CLI-Einstieg für Linux-SIGTERM/SIGHUP und prozessseitig emittiertes SIGBREAK; kein nativer Windows-Signalnachweis. Neuer Kopf wartet auf erneute Root-Abnahme. SRV-5 gegenseitige Authentifizierung bleibt geparkt; R908-FU-2, R896-FU und R899-FU K2 bleiben offen | je Punkt eigenes Paket mit eigener Abnahme | – | | V16-NACHBEOB | Zwei Wochenmessungen und 40 Queue-Läufe; Messgrößen-Leitfaden | Stabschef | v1.6.0 | Server | C | Verschoben; KI-30-Schluss (#874: 42 Queue-Läufe ab 09.10., 0 KI-30) liegt vor, sein Fenster weicht vom Originalkriterium (Fenster ab #518, 05.10.) ab — Abgleich: Unbekannt (zu prüfen); zwei Wochenmessungen ohne vollständigen Fensterbeleg | Wochenberichte BENCH-01/02 im Ticket | b8b16ec8 | KI-30-Quellenkonflikt (Basis `03542ef`, geprüft 09.10.): diff --git a/tools/denkraum/server-entry-drain.test.mjs b/tools/denkraum/server-entry-drain.test.mjs index 5f3ec148..abc04dca 100644 --- a/tools/denkraum/server-entry-drain.test.mjs +++ b/tools/denkraum/server-entry-drain.test.mjs @@ -32,14 +32,14 @@ async function freePort() { const { port } = probe.address(); await new Promise(r => probe.close(r)); return port; } // freePort() can race with another binder; retry EADDRINUSE ("konnte nicht starten") on a fresh port, like server-stop/lifecycle. -async function startEntry(t, file, { webhook = false, args = [] } = {}) { +async function startEntry(t, file, { webhook = false, args = [], ipc = false } = {}) { for (let attempt = 1; ; attempt++) { const port = await freePort(); const env = { ...process.env, DECISION_DESK_STATE: file, DECISION_DESK_PORT: String(port), DECISION_DESK_ROOT_AGENT_ID: 'root-test', DECISION_DESK_ROOT_RECEIPT_TOKEN: strongSecret(), DECISION_DESK_WEBHOOK_SECRET: strongSecret() }; delete env.DECISION_DESK_WEBHOOK_URL; if (webhook) env.DECISION_DESK_WEBHOOK_URL = 'https://agentsroom.dev/api/triggers/t_abc123'; - const child = spawn(process.execPath, [...args, entry], { env, stdio: ['ignore', 'pipe', 'pipe'] }); + const child = spawn(process.execPath, [...args, entry], { env, stdio: ['ignore', 'pipe', 'pipe', ...(ipc ? ['ipc'] : [])] }); const out = { stdout: '', stderr: '' }, waiters = []; const feed = (key, d) => { out[key] += d; for (const w of waiters) w(); }; child.stdout.on('data', d => feed('stdout', d)); child.stderr.on('data', d => feed('stderr', d)); @@ -95,20 +95,33 @@ test('DRSEC-G4b: SIGTERM lets an in-flight request finish within the budget and assert.ok(await missing(`${file}.owner`)); }); -test('DRSEC-G4b: SIGTERM with a notification send that never answers prints one short line and exits non-zero and keeps the owner record', { skip: posixOnly }, async t => { - const dir = await workdir(t), file = join(dir, 'ledger.json'), store = new DeskStore(file, { rootAgentId: 'root-test' }); - await store.putQuestion(question('q1')); await store.migrate((await store.read()).revision); - await store.answer({ questionId: 'q1', questionRevision: 1, expectedAnswerId: null, requestId: 'req1', action: 'answer', selected: ['a'], note: 'x' }); - await store.close(); - // The webhook transport is the global fetch; this preload makes it announce itself and never settle. - const preload = join(dir, 'hang-fetch.mjs'); - await writeFile(preload, "globalThis.fetch = () => { process.stdout.write('SEND-STARTED\\n'); return new Promise(() => {}); };\n"); - const run = await startEntry(t, file, { webhook: true, args: ['--import', pathToFileURL(preload).href] }); - assert.equal(await run.output(/SEND-STARTED/), true); - run.child.kill('SIGTERM'); - const result = await within(run.done, 20000); - assert.equal(result.code, 1, result.stderr); - assert.equal(result.stderr.trim().split('\n').length, 1, result.stderr); - assert.match(result.stderr, /^Entscheidungsseite: Beenden fehlgeschlagen: DRAIN_TIMEOUT$/m); - assert.equal(await missing(`${file}.owner`), false, 'a stuck send keeps the owner record (intended)'); -}); +for (const signal of ['SIGTERM', 'SIGHUP', 'SIGBREAK']) { + test(`DRSEC-G4b: ${signal} with a notification send that never answers prints one short line and exits non-zero and keeps the owner record`, { skip: posixOnly }, async t => { + const dir = await workdir(t), file = join(dir, 'ledger.json'), store = new DeskStore(file, { rootAgentId: 'root-test' }); + await store.putQuestion(question('q1')); await store.migrate((await store.read()).revision); + await store.answer({ questionId: 'q1', questionRevision: 1, expectedAnswerId: null, requestId: 'req1', action: 'answer', selected: ['a'], note: 'x' }); + await store.close(); + // The webhook transport is the global fetch; this preload makes it announce itself and never settle. + const preload = join(dir, 'hang-fetch.mjs'); + // POSIX cannot deliver SIGBREAK. Bridge only that OS event through Node's public process emitter; + // this exercises the real CLI listener and stop closure, without claiming Windows delivery coverage. + await writeFile(preload, "globalThis.fetch = () => { process.stdout.write('SEND-STARTED\\n'); return new Promise(() => {}); };\n" + + "process.on('message', signal => { if (signal === 'SIGBREAK') process.emit('SIGBREAK', 'SIGBREAK'); });\n"); + const run = await startEntry(t, file, { webhook: true, args: ['--import', pathToFileURL(preload).href], ipc: signal === 'SIGBREAK' }); + assert.equal(await run.output(/SEND-STARTED/), true); + const started = performance.now(); + if (signal === 'SIGBREAK') run.child.send(signal); + else assert.equal(run.child.kill(signal), true); + const result = await within(run.done, 20000); + const elapsed = performance.now() - started; + t.diagnostic(`${signal} drain: ${elapsed.toFixed(0)} ms`); + assert.equal(result.code, 1, result.stderr); + assert.equal(result.stderr.trim().split('\n').length, 1, result.stderr); + assert.match(result.stderr, /^Entscheidungsseite: Beenden fehlgeschlagen: DRAIN_TIMEOUT$/m); + assert.equal(await missing(`${file}.owner`), false, 'a stuck send keeps the owner record (intended)'); + // The specified totals are 6 s for SIGTERM and 4 s for console-close signals. A 500 ms lower + // tolerance and a 1.5 s upper margin distinguish those budgets without increasing either one. + const [min, max] = signal === 'SIGTERM' ? [5500, 7500] : [3500, 5500]; + assert.ok(elapsed >= min && elapsed < max, `${signal} drain must take ${min}–${max} ms, got ${elapsed} ms`); + }); +}