From ce4f956ec0d75774b6fb4ae634e4d4b48efe6a6f Mon Sep 17 00:00:00 2001 From: caezium <113233555+caezium@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:33:24 -0700 Subject: [PATCH 1/3] wip(repoint): bundle burrow-engine as the sole binary + NDJSON stream reducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FUNCTIONAL CORE of the full repoint (GUI shells burrow-engine directly, dropping the burrow-cli conductor + burrow-digger engine). LOCALLY BUILDABLE via build.sh with BURROW_ENGINE_SRC pointed at a burrow-engine checkout; NOT yet wired into the release pipeline or submodules (deliberate follow-up after local validation). - macos/scripts/bundle-burrow.sh: cargo-builds the ONE burrow-engine binary universal (arm64+x86_64 lipo) → Resources/burrow (kept the name so BurrowConductor.executableURL is unchanged). Replaces the digger Go build. - macos/project.yml: dropped the 'Bundle burrow (conductor)' phase; the 'Bundle burrow-engine' phase now cargo-builds the engine from BURROW_ENGINE_SRC. fclones phase kept (dupes still shells it via $BURROW_FCLONES). - MoleCLI.bundledExecutable: Resources/engine/mole → Resources/burrow. - BurrowStreamReport.swift (new): reduces the engine's clean/optimize --stream NDJSON (removed/would_remove/failed/protected/task/would_run + done) into the same TaskRunReport the views render; OperationFlow.moleStream now uses it instead of the human-text parseTaskReport. Every envelope-based pane (analyze/status/net/orphans/ photos/dupes/history/uninstall) is UNCHANGED — the engine emits the same envelope. KNOWN REMAINING (needs your build+test loop): 1. mo→engine argv inversion for the DIRECT/ELEVATED .mo path: mo 'clean' = live but engine 'clean' = dry-run (+--apply). streamArgv handles it for the streaming path (default), but elevated/streaming-off clean/optimize/uninstall/purge/installer via resolveMo pass mo-style argv → wrong semantics. Needs a MoActions-level translation. 2. Submodules: swap the 3 (.gitmodules: burrow-cli x2 + burrow-digger) for a single caezium/burrow-engine submodule (macos + windows). 3. release.yml + windows-release.yml: build burrow-engine (ENGINE_PAT auth already present), bundle as Resources/burrow, drop the digger/conductor fetch + verify + sign. 4. analyze --progress streaming is already OFF by default (uses capture) — no change. --- macos/Sources/BurrowStreamReport.swift | 118 +++++++++++++++++++++++++ macos/Sources/MoleCLI.swift | 6 +- macos/Sources/OperationFlow.swift | 6 +- macos/project.yml | 53 ++++------- macos/scripts/bundle-burrow.sh | 51 ++++++----- 5 files changed, 172 insertions(+), 62 deletions(-) create mode 100644 macos/Sources/BurrowStreamReport.swift diff --git a/macos/Sources/BurrowStreamReport.swift b/macos/Sources/BurrowStreamReport.swift new file mode 100644 index 0000000..b7d7e45 --- /dev/null +++ b/macos/Sources/BurrowStreamReport.swift @@ -0,0 +1,118 @@ +// +// BurrowStreamReport.swift +// Burrow +// +// Reduces the NDJSON progress feed from `burrow-engine clean --stream` / `optimize --stream` +// into the (groups, summary) shape TaskReportView renders — the same TaskRunReport the old +// human-text `parseTaskReport` produced, so the Clean/Optimize views + completion notification +// are unchanged. The engine emits one JSON object per line: +// +// clean live: {"event":"removed","path":P,"bytes":N} | {"event":"failed",...} | +// {"event":"protected",...} then {"event":"done","freed_bytes":N, +// "freed_human":H,"removed":N,"failed":N,"protected":N} +// clean preview: {"event":"would_remove","path":P,"bytes":N} then {"event":"done", +// "dry_run":true,"would_free_bytes":N,"would_free_human":H,"count":N} +// optimize live: {"event":"task","name":T,"ok":B,"error":E|null} then {"event":"done", +// "ok":B,"tasks":N,"failed":N} +// optimize prev: {"event":"would_run","name":T,"description":D} then {"event":"done", +// "dry_run":true,"tasks":N} +// +// Parsed line-by-line with JSONSerialization (like BurrowEnvelope) — the reduce is called on the +// accumulated `[String]` after every streamed line (throttled) and once at exit. +// + +import Foundation + +enum BurrowStreamReport { + /// Reduce the accumulated NDJSON lines into a TaskRunReport. Unparseable / non-event lines are + /// skipped, so a stray warning on stderr never breaks the result screen. + static func reduce(_ lines: [String]) -> TaskRunReport { + var items: [TaskItem] = [] + var summary: TaskSummary? + + for line in lines { + guard let obj = object(from: line), let event = obj["event"] as? String else { continue } + switch event { + case "removed": + if let p = obj["path"] as? String { items.append(TaskItem(marker: .ok, text: p)) } + case "would_remove": + if let p = obj["path"] as? String { items.append(TaskItem(marker: .action, text: p)) } + case "failed": + if let p = obj["path"] as? String { items.append(TaskItem(marker: .error, text: p)) } + case "protected": + if let p = obj["path"] as? String { items.append(TaskItem(marker: .review, text: p)) } + case "task": + if let name = obj["name"] as? String { + let ok = obj["ok"] as? Bool ?? true + items.append(TaskItem(marker: ok ? .ok : .error, text: name)) + } + case "would_run": + if let name = obj["name"] as? String { items.append(TaskItem(marker: .action, text: name)) } + case "done": + summary = makeSummary(from: obj, itemCount: items.count) + default: + break + } + } + + let groups = items.isEmpty + ? [] + : [TaskGroup(title: NSLocalizedString("Cleanup", comment: "streamed op group title"), + items: items)] + return (groups: groups, summary: summary) + } + + /// A short human label for the HUD detail line, extracted from one NDJSON event. Empty for the + /// terminal `done` (the HUD keeps the last item line) or an unparseable line. + static func hudLine(_ line: String) -> String { + guard let obj = object(from: line), let event = obj["event"] as? String else { return "" } + switch event { + case "removed", "would_remove", "failed", "protected": + if let p = obj["path"] as? String { return (p as NSString).lastPathComponent } + case "task", "would_run": + if let name = obj["name"] as? String { return name } + default: + break + } + return "" + } + + // MARK: - internals + + private static func object(from line: String) -> [String: Any]? { + let t = line.trimmingCharacters(in: .whitespacesAndNewlines) + guard t.first == "{", let data = t.data(using: .utf8), + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + else { return nil } + return obj + } + + /// Build the summary from a `done` event, covering both the live and preview shapes of clean + /// and optimize. `itemCount` is the number of accumulated item lines (fallback when a specific + /// count field is absent). + private static func makeSummary(from done: [String: Any], itemCount: Int) -> TaskSummary { + // Freed disk (live clean) vs would-free (preview) vs no size (optimize). + let freedHuman = done["freed_human"] as? String + let wouldFreeHuman = done["would_free_human"] as? String + let space = freedHuman ?? wouldFreeHuman ?? "" + + // Item count: removed (clean live) → count (clean preview) → tasks (optimize) → accumulated. + let count = intField(done, "removed") + ?? intField(done, "count") + ?? intField(done, "tasks") + ?? itemCount + let items = count > 0 ? String(count) : "" + + // Live clean prints the actual freed size → surface it as the freed-space number. + let freeChange = freedHuman ?? "" + + return TaskSummary(space: space, items: items, categories: "", freeChange: freeChange, freeNow: "") + } + + /// Read an integer field regardless of whether JSONSerialization bridged it to Int or Double. + private static func intField(_ obj: [String: Any], _ key: String) -> Int? { + if let n = obj[key] as? Int { return n } + if let d = obj[key] as? Double { return Int(d) } + return nil + } +} diff --git a/macos/Sources/MoleCLI.swift b/macos/Sources/MoleCLI.swift index 0da2ab7..b484a4c 100644 --- a/macos/Sources/MoleCLI.swift +++ b/macos/Sources/MoleCLI.swift @@ -77,8 +77,10 @@ enum MoleCLI { /// over any system `mo`: users run our engine with zero install and never touch upstream /// (GPL-relicensed) mo. It's part of the signed app bundle, so it's a trusted location. static func bundledExecutable() -> String? { - guard let url = Bundle.main.url(forResource: "mole", withExtension: nil, - subdirectory: "engine") else { return nil } + // The single bundled `burrow-engine` binary, staged as Resources/burrow (bundle-burrow.sh). + // It replaced the old Resources/engine/mole digger — one binary that does the work AND + // speaks the envelope. Same file BurrowConductor.executableURL resolves. + guard let url = Bundle.main.url(forResource: "burrow", withExtension: nil) else { return nil } return FileManager.default.isExecutableFile(atPath: url.path) ? url.path : nil } diff --git a/macos/Sources/OperationFlow.swift b/macos/Sources/OperationFlow.swift index 1243155..5b16890 100644 --- a/macos/Sources/OperationFlow.swift +++ b/macos/Sources/OperationFlow.swift @@ -291,9 +291,11 @@ extension ToolOperation where Report == TaskRunReport { static func moleStream(_ args: [String], gate: Gate = .none, elevated: Bool = false, label: String?, notifyOnEnd: Bool = false) -> ToolOperation { + // The bundled engine streams NDJSON (clean/optimize --stream); reduce those events into the + // same (groups, summary) shape the human-text parser produced. See BurrowStreamReport. ToolOperation(label: label, arguments: args, gate: gate, elevated: elevated, - reduce: { parseTaskReport($0) }, - hudLine: { TaskReportText.line($0) }, + reduce: { BurrowStreamReport.reduce($0) }, + hudLine: { BurrowStreamReport.hudLine($0) }, notifyOnEnd: notifyOnEnd, finalDetail: { $0.summary?.completionLine ?? "" }) } diff --git a/macos/project.yml b/macos/project.yml index 906fdef..64aae8b 100644 --- a/macos/project.yml +++ b/macos/project.yml @@ -94,43 +94,28 @@ targets: - name: Bundle burrow-engine (MIT) basedOnDependencyAnalysis: false script: | - # Stage the MIT engine into the app's Resources so users run our engine with zero - # install (never upstream GPL mo). Activates when the engine source is present — - # a release/CI vendors it (vendor/burrow-engine) or sets BURROW_ENGINE_SRC; dev - # builds without it fall back to a system-installed engine. + # Build the ONE MIT `burrow-engine` binary and stage it as Resources/burrow — it does all + # the work natively AND emits the stable Burrow envelope + NDJSON streaming, so it replaces + # both the old burrow-cli conductor and the burrow-digger engine (a single binary, no + # separate engine dir). Activates when a burrow-engine checkout is present — a release/CI + # vendors it (vendor/burrow-engine) or sets BURROW_ENGINE_SRC; dev builds without it fall + # back to a system-installed engine. # - # NOTE: Xcode's CodeSign phase runs AFTER all build phases, so it re-signs the app - # over the freshly-added engine and the resource seal ends up stale. The final seal - # therefore happens POST-build: use scripts/build.sh locally, and the release - # pipeline's inside-out re-sign (#178) seals it for distribution. + # NOTE: Xcode's CodeSign phase runs AFTER all build phases, so it re-signs the app over the + # freshly-added binary and the resource seal ends up stale. The final seal therefore + # happens POST-build: use scripts/build.sh locally, and the release pipeline's inside-out + # re-sign (#178) seals it for distribution. ENGINE_SRC="${BURROW_ENGINE_SRC:-$SRCROOT/vendor/burrow-engine}" - # Require a real engine checkout, not just the directory: a plain - # (no-submodule) checkout — e.g. the PR `ci` job — leaves an EMPTY - # vendor/burrow-engine dir for the gitlink, so `[ -d ]` alone would be - # true and bundle-engine.sh would then fail building from missing - # source. Gate on mole + cmd/ so we bundle only a populated checkout - # and otherwise fall through to the system engine (build still green). - if [ -f "$ENGINE_SRC/mole" ] && [ -d "$ENGINE_SRC/cmd" ]; then - "$SRCROOT/scripts/bundle-engine.sh" "$ENGINE_SRC" "$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" + # Require a real engine checkout, not just the gitlink dir: a plain (no-submodule) + # checkout — e.g. the PR `ci` job — leaves an EMPTY vendor/burrow-engine dir, so `[ -d ]` + # alone would be true and the cargo build would fail from missing source. Gate on + # Cargo.toml + src/ + cargo so we bundle only a populated checkout, and otherwise fall + # through to the system engine (build still green). + if [ -f "$ENGINE_SRC/Cargo.toml" ] && [ -d "$ENGINE_SRC/src" ] && command -v cargo >/dev/null 2>&1; then + "$SRCROOT/scripts/bundle-burrow.sh" "$ENGINE_SRC" "$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" \ + || echo "warning: burrow-engine build failed — Burrow falls back to a system engine." else - echo "warning: burrow-engine source not present at $ENGINE_SRC — Burrow falls back to a system engine. Set BURROW_ENGINE_SRC or init the vendor/burrow-engine submodule." - fi - - name: Bundle burrow (conductor) - basedOnDependencyAnalysis: false - script: | - # Stage the FSL `burrow` conductor beside the MIT engine so the GUI can shell out to - # `burrow --json` (stable envelope + NDJSON streaming) instead of the engine - # directly. Gated on a POPULATED vendor/burrow-cli checkout: a plain (no-submodule) - # checkout — e.g. the PR `ci` job — leaves an EMPTY gitlink dir, so we bundle only when - # Cargo.toml is really present and otherwise fall through to the direct engine (build - # still green, exactly like the engine phase above). Final resource seal happens - # POST-build (scripts/build.sh locally; the release pipeline's inside-out re-sign). - BURROW_CLI_SRC="${BURROW_CLI_SRC:-$SRCROOT/vendor/burrow-cli}" - if [ -f "$BURROW_CLI_SRC/Cargo.toml" ] && [ -d "$BURROW_CLI_SRC/src" ] && command -v cargo >/dev/null 2>&1; then - "$SRCROOT/scripts/bundle-burrow.sh" "$BURROW_CLI_SRC" "$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" \ - || echo "warning: burrow conductor build failed — Burrow falls back to the direct engine." - else - echo "warning: burrow-cli source or cargo not present at $BURROW_CLI_SRC — Burrow falls back to the direct engine. Set BURROW_CLI_SRC or init the vendor/burrow-cli submodule." + echo "warning: burrow-engine source or cargo not present at $ENGINE_SRC — Burrow falls back to a system engine. Set BURROW_ENGINE_SRC or init the vendor/burrow-engine submodule." fi - name: Bundle fclones (MIT sidecar) basedOnDependencyAnalysis: false diff --git a/macos/scripts/bundle-burrow.sh b/macos/scripts/bundle-burrow.sh index daa6baa..3fff88e 100755 --- a/macos/scripts/bundle-burrow.sh +++ b/macos/scripts/bundle-burrow.sh @@ -1,32 +1,35 @@ #!/usr/bin/env bash # -# bundle-burrow.sh — stage the `burrow` CONDUCTOR binary into the app's Resources. +# bundle-burrow.sh — build the `burrow-engine` binary and stage it as the app's `Resources/burrow`. # -# The GUI shells out to this bundled conductor (`burrow --json`) instead of the engine -# directly, so it gets the stable Burrow envelope + NDJSON streaming contract with zero install. -# burrow locates its engine via $BURROW_ENGINE_DIR, which the app sets to the sibling-bundled -# Resources/engine (see bundle-engine.sh). The FSL-licensed conductor and the MIT engine stay -# arm's-length (separate processes); only the built binary travels — no Rust source. +# The GUI shells out to this ONE bundled binary (`burrow --json`) for the stable Burrow +# envelope, and `clean --stream` / `optimize --stream` for the live NDJSON progress feed. The new +# MIT `burrow-engine` does all the work natively (analyze/status/clean/optimize/uninstall/net/ +# orphans/slim-check/evict/dupes/photos/history/purge/installer), so there is no separate engine +# dir or conductor anymore — this single binary replaces both the old burrow-cli conductor and the +# burrow-digger engine. It's still staged under the name `burrow` so the app's resolution +# (BurrowConductor.executableURL → Resources/burrow) is unchanged. Only the built binary travels — +# no Rust source ships. `dupes` shells the sibling-bundled Resources/fclones via $BURROW_FCLONES. # -# Usage: bundle-burrow.sh -# BURROW_CLI_SRC a burrow-cli checkout (has Cargo.toml, src/) -# RESOURCES_DIR the app bundle's Resources dir (burrow is written directly inside it) +# Usage: bundle-burrow.sh +# BURROW_ENGINE_SRC a burrow-engine checkout (has Cargo.toml with the `burrow-engine` binary) +# RESOURCES_DIR the app bundle's Resources dir (the binary is written directly inside it) set -euo pipefail -SRC="${1:?burrow-cli source dir required}" +SRC="${1:?burrow-engine source dir required}" RESOURCES="${2:?resources dir required}" OUT="$RESOURCES/burrow" command -v cargo >/dev/null 2>&1 || { - echo "error: cargo not found — cannot build the burrow conductor (install Rust, or omit the vendor/burrow-cli submodule to fall back to the system engine)" + echo "error: cargo not found — cannot build burrow-engine (install Rust, or omit the vendor/burrow-engine submodule to fall back to a system engine)" exit 1 } -# Build UNIVERSAL (arm64 + x86_64) so the conductor runs on BOTH Apple Silicon and Intel Macs. -# An arch-only binary hangs the universal app on the other arch (same lesson as the engine's Go -# binaries, issue #221). Rust cross-compiles per target; we add both targets (rustup fetches the -# missing slice) and lipo them together. GIT_TERMINAL_PROMPT=0 + /dev/null 2>&1; then rustup target add "$A" "$X" >/dev/null 2>&1 || true fi - cargo build --release --target "$A" /dev/null \ || codesign --force --sign - --timestamp=none "$OUT" || true -echo "bundled burrow -> $OUT ($(lipo -archs "$OUT" 2>/dev/null || echo native); signed with '${IDENTITY}')" +echo "bundled burrow-engine -> $OUT ($(lipo -archs "$OUT" 2>/dev/null || echo native); signed with '${IDENTITY}')" From 51bed807f1e327e6e49ec7ff9f43b69eafddd965 Mon Sep 17 00:00:00 2001 From: caezium <113233555+caezium@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:41:17 -0700 Subject: [PATCH 2/3] test(repoint): update the streaming tests to the engine's NDJSON contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one CI failure: OperationFlowTests fed the OLD digger human-text ('Potential space: …') to moleStream's reducer, which now consumes the engine's NDJSON. Converted cannedClean to the equivalent clean --stream PREVIEW NDJSON (would_remove + a dry-run done with would_free_human) — it reduces to the same summary, so the existing assertions (space '383.8MB', groups 1, 'Cleaned 383.8MB · 372 items') still hold. Added BurrowStreamReportTests locking the contract directly: clean preview ('Cleaned'), clean live ('Freed', freed_human → freeChange), optimize task events, garbage-line tolerance, and hudLine label extraction. --- macos/Tests/BurrowStreamReportTests.swift | 74 +++++++++++++++++++++++ macos/Tests/OperationFlowTests.swift | 9 ++- 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 macos/Tests/BurrowStreamReportTests.swift diff --git a/macos/Tests/BurrowStreamReportTests.swift b/macos/Tests/BurrowStreamReportTests.swift new file mode 100644 index 0000000..c91fbb4 --- /dev/null +++ b/macos/Tests/BurrowStreamReportTests.swift @@ -0,0 +1,74 @@ +// +// BurrowStreamReportTests.swift +// BurrowTests +// +// Locks the NDJSON → TaskRunReport contract that OperationFlow uses for streamed +// clean/optimize. The engine emits one JSON object per line; the reducer must build the +// same (groups, summary) shape the views render. +// + +import XCTest +@testable import Burrow + +final class BurrowStreamReportTests: XCTestCase { + func testCleanPreview_yieldsCleanedSummary() { + let lines = [ + #"{"event":"would_remove","path":"/Users/x/Library/Caches/npm","bytes":201129000}"#, + #"{"event":"done","dry_run":true,"would_free_bytes":402438000,"would_free_human":"383.8MB","count":372}"#, + ] + let report = BurrowStreamReport.reduce(lines) + XCTAssertEqual(report.groups.count, 1) + XCTAssertEqual(report.groups.first?.items.count, 1) + XCTAssertEqual(report.summary?.space, "383.8MB") + XCTAssertEqual(report.summary?.items, "372") + // No freeChange on a preview → "Cleaned", not "Freed". + XCTAssertEqual(report.summary?.completionLine, "Cleaned 383.8MB · 372 items") + } + + func testCleanLive_yieldsFreedSummary() { + let lines = [ + #"{"event":"removed","path":"/a/x","bytes":10}"#, + #"{"event":"failed","path":"/a/y","error":"denied"}"#, + #"{"event":"protected","path":"/a/keep"}"#, + #"{"event":"done","freed_bytes":2048,"freed_human":"2.0KB","removed":1,"failed":1,"protected":1}"#, + ] + let report = BurrowStreamReport.reduce(lines) + XCTAssertEqual(report.groups.first?.items.count, 3, "removed + failed + protected are shown") + // A live done carries freed_human → freeChange → "Freed". + XCTAssertEqual(report.summary?.completionLine, "Freed 2.0KB · 1 items") + } + + func testOptimize_taskEvents() { + let lines = [ + #"{"event":"task","name":"flush_dns","ok":true,"error":null}"#, + #"{"event":"task","name":"restart_dock","ok":false,"error":"no proc"}"#, + #"{"event":"done","ok":false,"tasks":2,"failed":1}"#, + ] + let report = BurrowStreamReport.reduce(lines) + XCTAssertEqual(report.groups.first?.items.count, 2) + XCTAssertEqual(report.summary?.items, "2", "task count") + } + + func testGarbageLinesAreIgnored() { + // A stray non-JSON line (e.g. a warning) must not break the reduce. + let lines = [ + "warning: something on stderr", + #"{"event":"removed","path":"/a","bytes":1}"#, + "", + #"{"event":"done","freed_bytes":1,"freed_human":"1B","removed":1,"failed":0,"protected":0}"#, + ] + let report = BurrowStreamReport.reduce(lines) + XCTAssertEqual(report.groups.first?.items.count, 1) + XCTAssertNotNil(report.summary) + } + + func testHudLine_extractsReadableLabel() { + XCTAssertEqual( + BurrowStreamReport.hudLine(#"{"event":"removed","path":"/a/b/npm cache","bytes":1}"#), + "npm cache") + XCTAssertEqual( + BurrowStreamReport.hudLine(#"{"event":"task","name":"flush_dns","ok":true}"#), + "flush_dns") + XCTAssertEqual(BurrowStreamReport.hudLine("not json"), "") + } +} diff --git a/macos/Tests/OperationFlowTests.swift b/macos/Tests/OperationFlowTests.swift index 39363d7..2bc4060 100644 --- a/macos/Tests/OperationFlowTests.swift +++ b/macos/Tests/OperationFlowTests.swift @@ -38,10 +38,13 @@ final class OperationFlowTests: XCTestCase { } /// Canned dry-run output in mo's report shape (parseTaskReport-compatible). + // The engine's `clean --stream` NDJSON preview (would_remove per item + a dry-run done), + // which BurrowStreamReport.reduce turns into the (groups, summary) shape. A dry-run done + // carries `would_free_human` → summary.space (no freeChange), so the completion line reads + // "Cleaned 383.8MB · 372 items" — same as the old human-text "Potential space" preview. static let cannedClean: [ProcessEvent] = [ - .line("➤ Developer tools"), - .line(" → npm cache, 191.8MB"), - .line("Potential space: 383.8MB | Items: 372 | Categories: 20"), + .line(#"{"event":"would_remove","path":"/Users/x/Library/Caches/npm cache","bytes":201129000}"#), + .line(#"{"event":"done","dry_run":true,"would_free_bytes":402438000,"would_free_human":"383.8MB","count":372}"#), .exited(0), ] From b51e75e42300131d5e6eff752621db9ef19e3bbb Mon Sep 17 00:00:00 2001 From: caezium <113233555+caezium@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:47:15 -0700 Subject: [PATCH 3/3] test(repoint): keep cannedClean text for cleanOp; NDJSON script for moleStream The cleanOp() fixture uses the old parseTaskReport (text) reducer, while only the moleStream op uses the new NDJSON BurrowStreamReport reducer. Reverted cannedClean to its text form (so the cleanOp-based gate tests parse '383.8MB' again) and added a separate cannedCleanStream (NDJSON) used only by the moleStream final-detail test. --- macos/Tests/OperationFlowTests.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/macos/Tests/OperationFlowTests.swift b/macos/Tests/OperationFlowTests.swift index 2bc4060..eef1b02 100644 --- a/macos/Tests/OperationFlowTests.swift +++ b/macos/Tests/OperationFlowTests.swift @@ -38,11 +38,19 @@ final class OperationFlowTests: XCTestCase { } /// Canned dry-run output in mo's report shape (parseTaskReport-compatible). - // The engine's `clean --stream` NDJSON preview (would_remove per item + a dry-run done), - // which BurrowStreamReport.reduce turns into the (groups, summary) shape. A dry-run done - // carries `would_free_human` → summary.space (no freeChange), so the completion line reads - // "Cleaned 383.8MB · 372 items" — same as the old human-text "Potential space" preview. + // The human-text output the `cleanOp()` fixture's `parseTaskReport` reducer consumes. static let cannedClean: [ProcessEvent] = [ + .line("➤ Developer tools"), + .line(" → npm cache, 191.8MB"), + .line("Potential space: 383.8MB | Items: 372 | Categories: 20"), + .exited(0), + ] + + // The engine's `clean --stream` NDJSON preview, which the `moleStream(...)` op's + // BurrowStreamReport reducer consumes. A dry-run done carries `would_free_human` → + // summary.space (no freeChange), so the completion line reads "Cleaned 383.8MB · 372 items" + // — same summary the old human-text "Potential space" preview produced. + static let cannedCleanStream: [ProcessEvent] = [ .line(#"{"event":"would_remove","path":"/Users/x/Library/Caches/npm cache","bytes":201129000}"#), .line(#"{"event":"done","dry_run":true,"would_free_bytes":402438000,"would_free_human":"383.8MB","count":372}"#), .exited(0), @@ -169,7 +177,7 @@ final class OperationFlowTests: XCTestCase { // and the parsed summary replaces the last streamed line as the final // detail — that's the body a completion notification carries. func testNotifyOnEnd_andFinalDetail_reachOperationCenter() async throws { - let port = FakeProcessPort(script: Self.cannedClean) + let port = FakeProcessPort(script: Self.cannedCleanStream) let center = OperationCenter() let flow = OperationFlow(process: port, hasFullDiskAccess: { true }, resolveMo: { _ in "/usr/local/bin/mo" }, center: center)