diff --git a/additional/python/tests/test_fixture_leak_check.py b/additional/python/tests/test_fixture_leak_check.py index 060b6e0..496a9ee 100644 --- a/additional/python/tests/test_fixture_leak_check.py +++ b/additional/python/tests/test_fixture_leak_check.py @@ -26,8 +26,9 @@ # this list is flagged as a leak (e.g. foo.bar.ai, github.com). The list # feeds the detection pattern — it is NOT an allowlist of harmless # suffixes; TLDs outside it are simply not matched (known heuristic gap, -# see the README "Leak-Check" section). -ALLOWED_TLDS = r"com|net|org|io|ai|dev|de|eu|info|biz|co|me|app|xyz|example" +# see the README "Leak-Check" section). Named FLAGGED_TLDS (was +# ALLOWED_TLDS, #110-B): the old name suggested the opposite reading. +FLAGGED_TLDS = r"com|net|org|io|ai|dev|de|eu|info|biz|co|me|app|xyz|example" # The one allowed URL placeholder; findings that point at it are dropped. ALLOWED_URL = "example.invalid" @@ -45,7 +46,7 @@ # name the redaction rules keep. re.compile(r"/(?:home|media)/(?!user\b)[^/\s]+"), # 4. URL/domain with a TLD from the recognized-TLD list. - re.compile(r"[a-zA-Z0-9][a-zA-Z0-9.-]*\.(?:" + ALLOWED_TLDS + r")\b"), + re.compile(r"[a-zA-Z0-9][a-zA-Z0-9.-]*\.(?:" + FLAGGED_TLDS + r")\b"), # 5. user@host re.compile(r"[A-Za-z0-9._-]+@[a-zA-Z0-9][a-zA-Z0-9.-]*\b"), # 7. port in the unambiguous port= / port: forms (--port=41641, diff --git a/docs/mla-next/VERIFY.md b/docs/mla-next/VERIFY.md index a312ebd..eff4285 100644 --- a/docs/mla-next/VERIFY.md +++ b/docs/mla-next/VERIFY.md @@ -351,7 +351,7 @@ in `deb/DEBIAN/control`, `la_core` nirgends installiert. **Zustandsmodell.** `ProbeState { unknown, running, ok, stale, failed }`; `stale` nur über `markStale()` aus `ok` (behält `data`/`observedAt`), kein Pfad zurück zu `ok` ohne frische Observation — 8 neue Tests, Vertragslage `IPC_CONTRACT.md:16`. -**Schwärzung/Leak-Check.** Capture exakt der Produktions-Aufrufe (df ohne LC_ALL, uptime/free mit `LC_ALL=C`, ps `-eo pcpu,args --sort=-pcpu`, `/proc/loadavg`), alles unprivilegiert. Schwärzung: `/home|/media/` → `/user`-Platzhalter, Usernamen → `user`, URLs/Hosts → `example.invalid`; über die Regeln hinaus zusätzlich geschwärzt: QEMU-SMBIOS-Serial, MAC-Adresse, Xwayland-Authority-Suffix, eine Konto-URL (`.ai`-TLD, vom Leak-Check nicht abgedeckt — Lücke in `test/fixtures/README.md` dokumentiert, manuelle Durchsicht bleibt Pflicht). Leak-Check läuft bei jedem CI-Lauf mit (Trigger ausschließlich Push/PR) und ist auf allen zwölf `*.txt` leer. Final-Review-Fix (eigener Commit): die Brave-Crash-Reporter-Client-ID — persistent pro Installation, in der ersten Fassung fälschlich als Session-Zufallswert geführt — wurde 4× in `zorin_ps.txt` zu `` geschwärzt; der Leak-Check prüft nun zusätzlich Ports in `port=`-/`port:`- und `host:port`-Form, wodurch `--port=41641`, `telnet:localhost:7100` und `tcp::7149` gleichfalls geschwärzt wurden; die verbleibenden bloßen Port-Zahlen (`websocket=5700`, `--port 7000`) sind in `test/fixtures/README.md` dokumentiert. +**Schwärzung/Leak-Check.** Capture exakt der Produktions-Aufrufe (df ohne LC_ALL, uptime/free mit `LC_ALL=C`, ps `-eo pcpu,args --sort=-pcpu`, `/proc/loadavg`), alles unprivilegiert. Schwärzung: `/home|/media/` → `/user`-Platzhalter, Usernamen → `user`, URLs/Hosts → `example.invalid`; über die Regeln hinaus zusätzlich geschwärzt: QEMU-SMBIOS-Serial, MAC-Adresse, Xwayland-Authority-Suffix, eine Konto-URL (`.ai`-TLD — zum Capture-Zeitpunkt nicht abgedeckt, seit 643da93 in der Flag-Liste; die heute vom Leak-Check nicht erkannten Suffixe sind in `test/fixtures/README.md` dokumentiert, manuelle Durchsicht bleibt Pflicht). Leak-Check läuft bei jedem CI-Lauf mit (Trigger ausschließlich Push/PR) und ist auf allen zwölf `*.txt` leer. Final-Review-Fix (eigener Commit): die Brave-Crash-Reporter-Client-ID — persistent pro Installation, in der ersten Fassung fälschlich als Session-Zufallswert geführt — wurde 4× in `zorin_ps.txt` zu `` geschwärzt; der Leak-Check prüft nun zusätzlich Ports in `port=`-/`port:`- und `host:port`-Form, wodurch `--port=41641`, `telnet:localhost:7100` und `tcp::7149` gleichfalls geschwärzt wurden; die verbleibenden bloßen Port-Zahlen (`websocket=5700`, `--port 7000`) sind in `test/fixtures/README.md` dokumentiert. **Reviewer.** Je Task A (Korrektheit) + B (Vollständigkeit) parallel: - Task 1: A APPROVED / B SPEC_OK — Werte und Scope unabhängig verifiziert; Minor: TLD-Allowlist-Lücke, `/opt/brave.com`-Fehlalarm (im Fixture neutralisiert), keine `subTest`s. @@ -506,6 +506,40 @@ unabhängig reproduziert). **Rückfallplan.** `git revert 530ea0f b13bdd7` genügt: 2 la_core-Dateien (1 Implementierung + 1 neuer Test), kein Produktionscode außerhalb la_core, keine Unit, kein Packaging, polkit-Trinität unberührt. **Grenzen (bewusst offen).** `deactivateAll` deckt Module, die beim Aufruf `starting` sind (noch nicht in `_activationOrder`), weiterhin nicht ab — pre-existing, außerhalb des Diff-Scopes (Reviewer 1 + Fixer-Scopenotiz). Der Kommentar am erweiterten Guard nennt „stopping/starting"; erreichbar ist nur der `stopping`-Fall (Re-Review-Minor, reiner Kommentar-Wortlaut). Issue #110 Position C (Spawn-PATH-Fixierung) bleibt offen und braucht eine menschliche Sicherheitsentscheidung. Der Cross-ID-Race-Teil der #110-A-Checkbox ist über Test 3/5 abgedeckt (gemeinsamer Dep), nicht als eigener Cross-ID-Stresstest. +## Handoff #110-B — Review-Minors aus dem Follow-up-Pool (Nachtlauf 2026-09-30) + +**Status:** Pool-Item B aus Issue #110 vollständig umgesetzt auf Branch `night/pool-b` (Basis-SHA `506eb88`, Commit `64ba632` + Review-Fix-Commits). Lokaler Nachtlauf (Automation), kein Push/PR — Freigabe wie immer beim Menschen. Umfang: 4 Dateien +45/−11 (vor Review-Fixes) + 2 Minor-Fixes aus Review A. + +**Umsetzung je #110-B-Checkbox:** +1. **README-Zeilenref-Drift** — Paritäts-Tabelle nachgezogen (`linux_system.dart` :22→:26, :13-14→:13-15, :53→:57), Bug-Notiz-Ref :11→:13-15 (beide free-Call-Referenzen, Tabelle + Notiz). Zusätzlich im Geist des Items korrigiert: „TLD-Allowlist"→„TLD-Flag-Liste" (:103, :120) und die stale `.ai`-Raster-Aussage (:120) — `.ai` ist seit `643da93` in der Liste; Beispiel jetzt `.store` (verifiziert nicht in FLAGGED_TLDS). +2. **count≤0-Kante** — Entscheidung: PIN. Neuer Test `count 0 or negative yields every entry, not an empty list` in `packages/la_core/test/parsers_test.dart` pinnt count=0 und count=-1 → alle Einträge. Mutationsbeleg gefahren: Mutante `count<=0 → return empty` failt den Test (`+0 -1`), Original grün. Reviewer A hat die „kein Produktions-Caller"-Aussage verifiziert (einziger Call-Path `system_stats_service.dart:192` mit `count=5`). +3. **Spawn-Test Fall 6** — voller Fehler-Vertrag statt `isNotEmpty`: `output, isEmpty` (der Runner hardcodet `""` auf dem ProcessException-Pfad, command_helper.dart:65) + `error contains "No such file or directory"` (auf dieser Toolchain gemessen; Dart lokalisiert errno-Strings nicht, CI pinned Linux). +4. **Spawn-Test 8** — exakte Präfixkette statt `startsWith("PATH=/")`: Kind sieht exakt `PATH=$parentPath` (Parent-PATH 1:1), strengere Aussage über die Merge-Semantik. +5. **ALLOWED_TLDS-Misnomer** — Rename zu `FLAGGED_TLDS` (Definition + einzige Usage-Stelle), Kommentar dokumentiert den alten Namen; README-Prosa an 2 Stellen mitgezogen. + +**Reviewer.** Dual-Review auf Branch-Diff (2 parallele Subagenten): +- A (Korrektheit): NEEDS_FIXES mit 2 Minors → F1 Parser-Doccomment ergänzt („A `count` of 0 or less disables the limit" — der Test-Kommentar-Bezug ist damit wahr; vorher widersprach der Doc der Kante milde) → F2 VERIFY.md:354 stale `.ai`-Gegenwartsform korrigiert („seit 643da93 in der Flag-Liste"). Beide gefixt. +- B (Vollständigkeit/Spec): READY_FOR_PR — alle 5 Checkboxen SATISFIED (Nr. 1 exceeded: auch die :120-Stale-Aussage), Scope exakt 4 Dateien, alle Gates eigenständig nachgefahren und grün reproduziert (inkl. snap-dart-Format-Check heute 123/0 — der Laufzeit-Befund „111 changed" war ein Binärpfad-Artefakt des Snap-dart, mit Flutter-Dart 3.13.4 konsequent 0 changed). +- Re-Review A: beide Minors gefixt (einzeilig, siehe Fix-Commits), damit kein offenes Critical/Important — gesamt **READY_FOR_PR**. + +**Gates — tatsächlich ausgeführt (2026-09-30, Nachtlauf, auf `64ba632`):** + +| Gate | Ausgabe | +|---|---| +| `bash tool/check-versions.sh` | `version 0.8.0 is consistent` | +| Root `dart format --output=none --set-exit-if-changed lib test` | `Formatted 123 files (0 changed) in 0.50 seconds.` | +| `flutter analyze` | `No issues found! (ran in 6.2s)` | +| la_core `dart format` | `Formatted 24 files (0 changed) in 0.04 seconds.` | +| la_core `dart analyze` | `No issues found!` | +| la_core `dart test` | `00:00 +62: All tests passed!` (61 + neuer Pin) | +| `flutter test` (voller Lauf) | `00:10 +208: All tests passed!` | +| `python3 -m unittest discover -s tests -t .` (additional/python) | `Ran 53 tests in 0.101s` / `OK` | + +**Rote/übersprungene Gates.** Keine roten. Übersprungen: `build-deb.sh` (kein Packaging-Bezug; CI baut beim späteren PR), Branch-CI (läuft mit dem späteren PR), token-gate (betrifft #91-Dateien, nicht im Scope), manuelle Gate-0-Checks (bleiben Bastis Aufgabe). + +**Rückfallplan.** `git revert` der #110-B-Commits genügt: 2 Test-Dateien (Asserts + 1 neuer Test), 1 README, 1 Python-Test, 1 Parser-Doccomment, 1 VERIFY-Zeile — kein Produktionscode bis auf den Doccomment, keine Unit, kein Packaging. + +**Grenzen (bewusst offen).** README:137 `:19`-Ref (Kommentarzeile statt :20 REPO_ROOT-Zuweisung) — vorbestehend, außerhalb B-Buchstabe (nur linux_system.dart-Refs), als Minor bei Reviewer B notiert. `No such file or directory`-Pin ist CI-Linux-garantiert (Dart lokalisiert errno nicht, Runner hardcodet `""`+`e.message`). Der 23:30-Schwesternlauf (Automation) arbeitet mit derselben Queue; #110-B gilt über die Handoff-Sektion als abgeschlossen. ## Agenten-Handoff diff --git a/packages/la_core/lib/src/parsers/process.dart b/packages/la_core/lib/src/parsers/process.dart index a8030f5..963e880 100644 --- a/packages/la_core/lib/src/parsers/process.dart +++ b/packages/la_core/lib/src/parsers/process.dart @@ -12,6 +12,7 @@ class ProcessStat { /// Lines that carry a metric but no command are skipped rather than throwing; /// `ps` emits those for kernel threads on some systems. Skipped lines do not /// count towards `count` — the result holds up to `count` valid entries. +/// A `count` of 0 or less disables the limit: every valid entry is returned. List parsePsOutput(String cmdResult, int count) { var processes = List.empty(growable: true); for (var line in cmdResult.split("\n").skip(1)) { diff --git a/packages/la_core/test/parsers_test.dart b/packages/la_core/test/parsers_test.dart index 11b5775..550edc0 100644 --- a/packages/la_core/test/parsers_test.dart +++ b/packages/la_core/test/parsers_test.dart @@ -113,6 +113,31 @@ void main() { expect(processes.map((p) => p.processName), ["a", "c", "d"]); }); + + test("count 0 or negative yields every entry, not an empty list", () { + // Pinned edge, documented in the parser's doc comment but previously + // untested and without production callers: the early-exit compare + // `processes.length == count` never fires for count <= 0, so the + // parser degenerates to "all entries". If that is ever deemed a bug + // and changed to "empty", this pin makes the change visible instead + // of silent. + const output = + "%CPU COMMAND\n" + " 42.0 /usr/bin/a\n" + " 3.2 /usr/bin/c\n" + " 5.0 /usr/bin/d\n"; + + expect(parsePsOutput(output, 0).map((p) => p.processName), [ + "a", + "c", + "d", + ]); + expect(parsePsOutput(output, -1).map((p) => p.processName), [ + "a", + "c", + "d", + ]); + }); }); group("uptime parser", () { diff --git a/test/fixtures/README.md b/test/fixtures/README.md index 5adc35c..28a8641 100644 --- a/test/fixtures/README.md +++ b/test/fixtures/README.md @@ -27,9 +27,9 @@ im Betrieb verwendet (Quellen in Klammern). Nicht abwandeln — insbesondere die |---|---|---| | `zorin_df.txt` | `df -h` (ohne LC_ALL — Produktionsdefault) | `lib/linux/linux_filesystem.dart:9` | | `zorin_ps.txt` | `ps -eo pcpu,args --sort=-pcpu` | `lib/linux/linux_process.dart:10` (metric=pcpu) | -| `zorin_uptime.txt` | `LC_ALL=C /usr/bin/uptime` | `lib/linux/linux_system.dart:22` | -| `zorin_free.txt` | `LC_ALL=C /usr/bin/free -m` | `lib/linux/linux_system.dart:13-14` | -| `zorin_loadavg.txt` | `cat /proc/loadavg` | `lib/linux/linux_system.dart:53` | +| `zorin_uptime.txt` | `LC_ALL=C /usr/bin/uptime` | `lib/linux/linux_system.dart:26` | +| `zorin_free.txt` | `LC_ALL=C /usr/bin/free -m` | `lib/linux/linux_system.dart:13-15` | +| `zorin_loadavg.txt` | `cat /proc/loadavg` | `lib/linux/linux_system.dart:57` | Dokumentierte Abweichung: `zorin_ps.txt` wurde auf die ersten 60 Zeilen gekürzt (das Capture hatte 682; die Top-Liste braucht der Parser nur in @@ -41,7 +41,7 @@ Capture lief ohne `-m` und enthielt KiB-Werte, während `MemoryInfo.*Mb` (Doku: „free -m, in mebibytes") und die Formatter (`/1024`, MiB→GiB) Mebibytes erwarten — auf diesem Capture-Pfad wären alle RAM-/Swap-Werte 1024× zu hoch angezeigt worden. Der Produktionsaufruf in -`lib/linux/linux_system.dart:11` wurde auf `/usr/bin/free -m` korrigiert und +`lib/linux/linux_system.dart:13-15` wurde auf `/usr/bin/free -m` korrigiert und die Fixture damit neu aufgezeichnet (Follow-up aus dem #94-Review-Minor zu MemoryInfo MiB/KiB + Ad-hoc-Befund 2026-09-30). Der Dashboard-Poller (`lib/services/system_stats_service.dart`) führte `free -m` bereits korrekt; @@ -100,7 +100,7 @@ geschwärzt, siehe Liste oben) sowie generische Pfade unter `additional/python/tests/test_fixture_leak_check.py` läuft mit der Python-Testsuite in CI und prüft jede `*.txt`-Datei hier gegen Musterkategorien (IPv4, IPv6-Heuristik, `/home`/`/media` mit erlaubtem Namen `user`, Domains mit -TLD-Allowlist, `user@host`, Ports in `port=`-/`port:`-Form, `host:port`). +TLD-Flag-Liste, `user@host`, Ports in `port=`-/`port:`-Form, `host:port`). `README.md` wird nicht gescannt — sie dokumentiert die Regeln selbst und enthielte damit die zu findenden Muster per Design. @@ -117,7 +117,7 @@ sind ohne Fehlalarme nicht erkennbar und nicht identifizierend unterschieden). **Der Check ist heuristisch und ersetzt keine manuelle Durchsicht:** Die -TLD-Allowlist ist bewusst kurz (`.ai` fällt z. B. durch das Raster), hex- und +TLD-Flag-Liste ist bewusst kurz (`.store` fällt z. B. durch das Raster), hex- und ziffernähnliche Kennungen wie Serials oder MACs erkennt kein der Muster. Jede neue echte Ausgabe vor dem Einchecken selbst nach Usernamen, Rechnernamen, UIDs, Pfaden in `ps`-Argumenten und Browser-URLs durchsehen. diff --git a/test/process_command_runner_test.dart b/test/process_command_runner_test.dart index 4bf7f8c..d31a0d3 100644 --- a/test/process_command_runner_test.dart +++ b/test/process_command_runner_test.dart @@ -191,7 +191,11 @@ exit 0 expect(result.success, isFalse); expect(result.exitCode, -1); - expect(result.error, isNotEmpty); + // The full error contract of the ProcessException path: no stdout was + // ever produced, and the message is the OS lookup failure (measured on + // this toolchain: "No such file or directory"), not a generic blob. + expect(result.output, isEmpty); + expect(result.error, contains("No such file or directory")); }); test( @@ -236,6 +240,10 @@ exit 0 // prefix still resolves. Real /bin/sh — unprivileged, echo only. final ProcessCommandRunner runner = ProcessCommandRunner(); + // Exact prefix chain instead of a bare startsWith("PATH=/"): the child + // sees the parent's PATH verbatim (not merely "some absolute path"), + // so echo it once and pin the full line. + final String parentPath = Platform.environment["PATH"]!; final CommandResult result = await runner.run( "sh", [r"-c", r"echo PATH=$PATH"], @@ -243,7 +251,7 @@ exit 0 ); expect(result.success, isTrue); - expect(result.output.trim(), startsWith("PATH=/")); + expect(result.output.trim(), "PATH=$parentPath"); }); }); }