//! End-to-end proof battery for examples/soundboard-ts — the launch-gate //! port: a soundboard-complexity app authored in TypeScript + Native //! markup with ZERO hand-written Zig. The build transpiles the example's //! REAL core (examples/soundboard-ts/src/core.ts) and this suite drives //! it through `TsUiApp` with the example's SHIPPING markup //! (app.native, staged beside this file), so every pin here is the //! product path, not a fixture: //! //! - the markup view binds the ported catalog (grid, counts, the //! now-playing bar's idle state); //! - playback flows end to end: play -> the engine's audio request //! (prepared path, hosted URL, manifest byte size) -> loaded -> //! position ticks -> the rendered clock -> auto-advance at the //! natural end, with the play-next queue winning the advance; //! - the duration rule: the platform's estimate never replaces the //! manifest total; //! - the one-channel stale-event window: position/completed reports //! from a replaced playback are dropped by the model guard; //! - the rendered-clock subscription arms a REAL platform timer only //! while audio moves; //! - Copy Title lands on the clipboard channel; //! - search drives the full byte-splice text engine from raw platform //! text input; //! - a recorded session (user input + scripted audio events) replays //! byte-identically with zero host calls; //! - dispatch stays far under the frame budget at the rendered-clock //! cadence (the "on_frame cadence untested" flag, measured). //! //! Only this TEST wiring is Zig — the command mapper below exists so the //! suite can dispatch payload-carrying Msgs through the journaled //! menu-command path (the app itself dispatches them from markup). const std = @import("std"); const builtin = @import("builtin"); const native_sdk = @import("native_sdk"); const core = @import("ts_soundboard_core"); const runtime_ns = native_sdk.runtime; const canvas = native_sdk.canvas; const geometry = native_sdk.geometry; const Adapter = native_sdk.TsUiApp(core); const App = Adapter.App; const Bridge = Adapter.Host; const app_markup = @embedFile("app.native"); const CompiledAppView = canvas.CompiledMarkupView(core.Model, core.Msg, app_markup); const canvas_label = "soundboard-canvas"; /// The playclock subscription's platform timer id: the bridge's timer /// slot 0 lands in engine timer slot 0. const playclock_platform_id: u64 = runtime_ns.effect_timer_platform_id_base + 0; const app_views = [_]native_sdk.ShellView{ .{ .label = canvas_label, .kind = .gpu_surface, .fill = true, .gpu_backend = .metal }, }; const app_windows = [_]native_sdk.ShellWindow{.{ .label = "main", .title = "Soundboard TS", .width = 1080, .height = 720, .views = &app_views, }}; const app_scene: native_sdk.ShellConfig = .{ .windows = &app_windows }; /// TEST-ONLY command mapper: lets the suite dispatch the payload-carrying /// Msgs the app's markup dispatches from presses and context menus, but /// through the journaled menu-command path (record/replay needs every /// input in the journal). "sb.play.12" plays track 12, etc. fn testCommand(name: []const u8) ?core.Msg { if (std.mem.eql(u8, name, "sb.toggle")) return .toggle_play; if (std.mem.eql(u8, name, "sb.next")) return .next_track; if (std.mem.eql(u8, name, "sb.prev")) return .prev_track; if (std.mem.eql(u8, name, "sb.songs")) return .show_songs; if (std.mem.eql(u8, name, "sb.albums")) return .show_albums; if (commandId(name, "sb.play.")) |id| return .{ .play_track = id }; if (commandId(name, "sb.playalbum.")) |id| return .{ .play_album = id }; if (commandId(name, "sb.open.")) |id| return .{ .open_album = id }; if (commandId(name, "sb.queue.")) |id| return .{ .queue_track = id }; if (commandId(name, "sb.copy.")) |id| return .{ .copy_title = id }; return null; } fn commandId(name: []const u8, prefix: []const u8) ?i64 { if (!std.mem.startsWith(u8, name, prefix)) return null; return std.fmt.parseInt(i64, name[prefix.len..], 10) catch null; } fn appOptions() App.Options { return .{ .name = "soundboard-ts-e2e", .scene = app_scene, .canvas_label = canvas_label, // The comptime-compiled engine over the example's shipping markup // — the whole view tier of the app under test. .view = CompiledAppView.build, .on_command = testCommand, // The manifest theme channel, mirrored from the example's // app.zon exactly as the generated wiring resolves it // (manifestThemePack + manifestThemeAccent): the geist pack // under the Zig original's pink accent identity. .theme = .geist, .theme_accent = canvas.Color.rgb8(0xdf, 0x26, 0x70), }; } const Harness = struct { harness: *native_sdk.TestHarness(), app_state: *App, app: native_sdk.App, clock: native_sdk.TestClock, fn create() !*Harness { return createFull(null, .real); } /// A harness on the FAKE effects executor: clipboard requests park in /// fake slots for assertions instead of reaching platform services. fn createFake() !*Harness { return createFull(null, .fake); } fn createRecorded(recorder: ?*runtime_ns.SessionRecorder) !*Harness { return createFull(recorder, .real); } fn createFull(recorder: ?*runtime_ns.SessionRecorder, executor: runtime_ns.EffectExecutor) !*Harness { return createConfigured(recorder, executor, .{}); } /// A harness with adapter-owned knobs (boot images, env overrides) — /// the CoreOptions the generated wiring passes from app.zon assets /// and the launch environment. fn createConfigured(recorder: ?*runtime_ns.SessionRecorder, executor: runtime_ns.EffectExecutor, core_options: Adapter.CoreOptions) !*Harness { const self = try std.testing.allocator.create(Harness); errdefer std.testing.allocator.destroy(self); self.clock = .{}; self.clock.setWallMs(60_000); self.harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = geometry.SizeF.init(1080, 720), }); errdefer self.harness.destroy(std.testing.allocator); self.harness.null_platform.gpu_surfaces = true; self.harness.null_platform.image_decode = true; self.harness.runtime.options.session_recorder = recorder; self.app_state = try std.testing.allocator.create(App); errdefer std.testing.allocator.destroy(self.app_state); self.app_state.* = Adapter.init(std.heap.page_allocator, core_options, appOptions()); self.app_state.effects.executor = executor; self.app_state.effects.clock = self.clock.clock(); self.app = self.app_state.app(); try self.harness.start(self.app); try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .gpu_surface_frame = .{ .label = canvas_label, .size = geometry.SizeF.init(1080, 720), .scale_factor = 1, .frame_index = 1, .timestamp_ns = 1_000_000, } }); try std.testing.expect(self.app_state.installed); return self; } fn destroy(self: *Harness) void { self.app_state.deinit(); std.testing.allocator.destroy(self.app_state); self.harness.destroy(std.testing.allocator); std.testing.allocator.destroy(self); } fn wake(self: *Harness) !void { try self.harness.runtime.dispatchPlatformEvent(self.app, .wake); } /// Feed one scripted audio event and drain it into the core (the /// engine parks events until a dispatchable wake). fn audio(self: *Harness, kind: anytype, position_ms: u64, duration_ms: u64, playing: bool) !void { try self.app_state.effects.feedAudioEvent(kind, position_ms, duration_ms, playing); try self.wake(); } fn audioBuffering(self: *Harness, kind: anytype, position_ms: u64, duration_ms: u64, playing: bool, buffering: bool) !void { try self.app_state.effects.feedAudioEventBuffering(kind, position_ms, duration_ms, playing, buffering); try self.wake(); } fn menu(self: *Harness, name: []const u8) !void { try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .menu_command = .{ .name = name, .window_id = 1 } }); } fn hasText(self: *Harness, text: []const u8) bool { return findTextIn(self.app_state.tree.?.root, text); } fn findId(self: *Harness, kind: canvas.WidgetKind, text: []const u8) ?canvas.ObjectId { return findKindText(self.app_state.tree.?.root, kind, text); } fn findLabel(self: *Harness, label: []const u8) ?canvas.ObjectId { return findByLabel(self.app_state.tree.?.root, label); } /// Click a rendered widget through the automation verb — the same /// headless path `native automate` drives. fn click(self: *Harness, id: canvas.ObjectId) !void { var buffer: [96]u8 = undefined; const command = try std.fmt.bufPrint(&buffer, "widget-click {s} {d}", .{ canvas_label, id }); try self.harness.runtime.dispatchAutomationCommand(self.app, command); } fn textInput(self: *Harness, text: []const u8) !void { try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .gpu_surface_input = .{ .window_id = 1, .label = canvas_label, .kind = .text_input, .text = text, } }); } fn keyDown(self: *Harness, key: []const u8) !void { try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .gpu_surface_input = .{ .window_id = 1, .label = canvas_label, .kind = .key_down, .key = key, } }); } fn firePlayclock(self: *Harness, timestamp_ns: u64) !bool { const event = self.harness.null_platform.fireTimer(playclock_platform_id, timestamp_ns) orelse return false; try self.harness.runtime.dispatchPlatformEvent(self.app, event); return true; } fn playclockArmed(self: *Harness) bool { const timer = self.harness.null_platform.startedTimer(playclock_platform_id) orelse return false; return timer.active; } }; fn findKindText(widget: canvas.Widget, kind: canvas.WidgetKind, text: []const u8) ?canvas.ObjectId { if (widget.kind == kind and std.mem.eql(u8, widget.text, text)) return widget.id; for (widget.children) |child| { if (findKindText(child, kind, text)) |id| return id; } return null; } fn findTextIn(widget: canvas.Widget, text: []const u8) bool { if (std.mem.indexOf(u8, widget.text, text) != null) return true; for (widget.children) |child| { if (findTextIn(child, text)) return true; } return false; } fn findByLabel(widget: canvas.Widget, label: []const u8) ?canvas.ObjectId { if (std.mem.eql(u8, widget.semantics.label, label)) return widget.id; for (widget.children) |child| { if (findByLabel(child, label)) |id| return id; } return null; } // -------------------------------------------------------- markup binding test "the shipping markup binds the ported catalog: grid, counts, and the idle bar" { const h = try Harness.create(); defer h.destroy(); // The album grid page with the full committed catalog. try std.testing.expect(h.hasText("8 of 8")); try std.testing.expect(h.hasText("Exit Signs")); try std.testing.expect(h.hasText("Channel Surfing")); try std.testing.expect(h.hasText("Harbor Sleep")); try std.testing.expect(!h.hasText("Playing")); // The idle now-playing bar: prompt title, placeholder clocks. try std.testing.expect(h.hasText("Nothing playing")); try std.testing.expect(h.hasText("Pick an album or a song to start")); try std.testing.expect(h.hasText("-:--")); // The Songs page mounts every catalog track as a markup row with the // artist-album subtitle (the byte-built em-dash join). try h.menu("sb.songs"); try std.testing.expect(h.hasText("68 of 68")); try std.testing.expect(h.hasText("Mile Marker West")); try std.testing.expect(h.hasText("Harbor Sleep — Exit Signs")); // A markup album tile press opens the detail page: heading, the // byte-built meta line, and the record's rows (numbers, durations). try h.menu("sb.albums"); try h.click(h.findLabel("Exit Signs").?); try std.testing.expect(h.hasText("Harbor Sleep · 2025 · 8 tracks")); try std.testing.expect(h.hasText("Winter Birds")); try std.testing.expect(h.hasText("2:44")); // 164832ms, the manifest duration try std.testing.expect(h.findId(.button, "Play album") != null); // Back to the grid through the markup back button. try h.click(h.findId(.button, "Back to albums").?); try std.testing.expect(h.hasText("8 of 8")); } test "the runtime markup interpreter builds the emitted model exactly like the compiled engine" { // The PRODUCT wiring runs app.native through the runtime interpreter // (hot reload); this suite compiles it at comptime. Hold the two // engines text-identical over the booted model so the product path // can never drift from the tested one. const h = try Harness.create(); defer h.destroy(); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); const model = h.app_state.model; const AppUi = canvas.Ui(core.Msg); var interpreter_view = try canvas.MarkupView(core.Model, core.Msg).init(arena, app_markup); var interpreter_ui = AppUi.init(arena); const interpreted = try interpreter_ui.finalize(try interpreter_view.build(&interpreter_ui, &model)); var compiled_ui = AppUi.init(arena); const compiled = try compiled_ui.finalize(CompiledAppView.build(&compiled_ui, &model)); var interpreted_texts: std.ArrayListUnmanaged(u8) = .empty; defer interpreted_texts.deinit(std.testing.allocator); var compiled_texts: std.ArrayListUnmanaged(u8) = .empty; defer compiled_texts.deinit(std.testing.allocator); try collectTexts(interpreted.root, &interpreted_texts, std.testing.allocator); try collectTexts(compiled.root, &compiled_texts, std.testing.allocator); try std.testing.expectEqualStrings(interpreted_texts.items, compiled_texts.items); try std.testing.expect(std.mem.indexOf(u8, compiled_texts.items, "Nothing playing") != null); try std.testing.expect(std.mem.indexOf(u8, compiled_texts.items, "Exit Signs") != null); } fn collectTexts(widget: canvas.Widget, out: *std.ArrayListUnmanaged(u8), allocator: std.mem.Allocator) !void { try out.appendSlice(allocator, widget.text); try out.append(allocator, '\n'); for (widget.children) |child| { try collectTexts(child, out, allocator); } } // ------------------------------------------------------------- playback test "playback flows end to end: play, the duration rule, the rendered clock, auto-advance, and the queue" { const h = try Harness.create(); defer h.destroy(); const fx = &h.app_state.effects; // A markup row press starts REAL playback: the engine channel holds // the request whole — the prepared local path first, the hosted URL // fallback, and the manifest's byte size as the cache integrity gate. try h.click(h.findLabel("Exit Signs").?); try h.click(h.findId(.button, "Play album").?); const request = fx.pendingAudio().?; try std.testing.expectEqual(runtime_ns.ts_core_audio_key_base, request.key); try std.testing.expectEqualStrings("assets/music/exit-signs/mile-marker-west.mp3", request.path); try std.testing.expectEqualStrings("https://xksenynjs1imkkii.public.blob.vercel-storage.com/music/exit-signs/mile-marker-west.mp3", request.url); try std.testing.expectEqual(@as(u64, 3982923), request.expected_bytes); try std.testing.expect(Bridge.model().playing); try std.testing.expect(Bridge.model().loadPending); // The duration rule: the platform's own estimate (165s) never // replaces the manifest total (164832ms renders as 2:44 in the bar // AND the track list — the two surfaces agree by construction). try h.audio(.loaded, 0, 165_000, true); try std.testing.expect(!Bridge.model().loadPending); try std.testing.expectEqual(@as(i64, 164_832), Bridge.model().nowDurationMs); try std.testing.expectEqual(@as(i64, 165_000), Bridge.model().platformDurationMs); try std.testing.expect(h.hasText("2:44")); try std.testing.expect(h.hasText("0:00")); // Position ticks move the rendered clock; the elapsed label follows. try h.audio(.position, 1_500, 165_000, true); try std.testing.expectEqual(@as(i64, 1_500), Bridge.model().elapsedMs); try std.testing.expect(h.hasText("0:01")); // The never-rewind rule: a small backward tick holds flat while the // clock is in motion; a past-slack disagreement snaps. try h.audio(.position, 1_400, 165_000, true); try std.testing.expectEqual(@as(i64, 1_500), Bridge.model().elapsedMs); try h.audio(.position, 100, 165_000, true); try std.testing.expectEqual(@as(i64, 100), Bridge.model().elapsedMs); // The rendered-clock subscription is a REAL platform timer that // exists exactly while audio moves; each fire advances one interval. try std.testing.expect(h.playclockArmed()); try std.testing.expect(try h.firePlayclock(2_000_000)); try std.testing.expectEqual(@as(i64, 350), Bridge.model().elapsedMs); // The honest buffering state reads where the artist normally does, // and the motion-gated subscription reconciles away. try h.audioBuffering(.position, 400, 165_000, true, true); try std.testing.expect(Bridge.model().buffering); try std.testing.expect(h.hasText("buffering...")); try std.testing.expect(!h.playclockArmed()); try h.audioBuffering(.position, 900, 165_000, true, false); try std.testing.expect(!Bridge.model().buffering); try std.testing.expect(h.playclockArmed()); // Queue a track from the songs library (the markup dispatches this // from the row's context menu): the badge appears and the natural // end advances into the QUEUE, not the album order. try h.menu("sb.queue.16"); try std.testing.expect(h.hasText("1 queued")); // The queued track's row wears the Up-next badge (it lives in the // songs library - a different album than the open detail page). try h.menu("sb.songs"); try std.testing.expect(h.hasText("Up next")); try h.menu("sb.albums"); try h.audio(.completed, 164_832, 165_000, false); const queued_request = fx.pendingAudio().?; try std.testing.expectEqualStrings("assets/music/second-nature/velvet-jackpot.mp3", queued_request.path); try std.testing.expectEqual(@as(?i64, 16), Bridge.model().now); try std.testing.expectEqual(@as(usize, 0), Bridge.model().queue.len); try std.testing.expect(!h.hasText("1 queued")); // With the queue drained, completion advances through the album in // manifest order (Velvet Jackpot -> Second Nature). try h.audio(.loaded, 0, 131_000, true); try h.audio(.completed, 130_872, 131_000, false); try std.testing.expectEqual(@as(?i64, 17), Bridge.model().now); try std.testing.expectEqualStrings("assets/music/second-nature/second-nature.mp3", fx.pendingAudio().?.path); // Pause parks the transport (optimistic flag, engine verb, timer // gone); resume brings all three back. try h.audio(.loaded, 0, 80_000, true); try h.menu("sb.toggle"); try std.testing.expect(!Bridge.model().playing); try std.testing.expect(!fx.audioSnapshot().playing); try std.testing.expect(!h.playclockArmed()); try h.menu("sb.toggle"); try std.testing.expect(Bridge.model().playing); try std.testing.expect(fx.audioSnapshot().playing); // prev within the first 3 seconds goes to the PREVIOUS track; // past 3 seconds it restarts the current one through the seek verb. try h.audio(.position, 1_000, 80_000, true); try h.menu("sb.prev"); try std.testing.expectEqual(@as(?i64, 16), Bridge.model().now); try h.audio(.loaded, 0, 131_000, true); try h.audio(.position, 5_000, 131_000, true); try h.menu("sb.prev"); try std.testing.expectEqual(@as(?i64, 16), Bridge.model().now); try std.testing.expectEqual(@as(i64, 0), Bridge.model().elapsedMs); try std.testing.expectEqual(@as(u64, 0), fx.audioSnapshot().position_ms); } test "one audio channel: stale reports from a replaced playback drop by the model guard" { const h = try Harness.create(); defer h.destroy(); const fx = &h.app_state.effects; // Track 1 plays and reports; then track 3 replaces it. Until the new // playback's `loaded` acknowledgment, position and completed reports // can only belong to the replaced track — the guard drops both. try h.menu("sb.play.1"); try h.audio(.loaded, 0, 165_000, true); try h.audio(.position, 9_000, 165_000, true); try h.menu("sb.play.3"); try std.testing.expect(Bridge.model().loadPending); try std.testing.expectEqual(@as(i64, 0), Bridge.model().elapsedMs); // A straggler position from the old playback: the rendered clock // must not scrub the new track. try h.audio(.position, 9_500, 165_000, true); try std.testing.expectEqual(@as(i64, 0), Bridge.model().elapsedMs); // A straggler completion: the player must not double-advance. try h.audio(.completed, 164_832, 165_000, false); try std.testing.expectEqual(@as(?i64, 3), Bridge.model().now); // The new playback's own acknowledgment reopens the stream. try h.audio(.loaded, 0, 71_000, true); try std.testing.expect(!Bridge.model().loadPending); try h.audio(.position, 2_000, 71_000, true); try std.testing.expectEqual(@as(i64, 2_000), Bridge.model().elapsedMs); // Spectrum frames are consciously ignored (parity with the Zig // original — the soundboard's identity is the clean catalog). var bands: [32]u8 = undefined; for (&bands, 0..) |*b, i| b.* = @intCast(i * 3); try fx.feedAudioSpectrum(bands, 3_000, 71_000); try h.wake(); try std.testing.expectEqual(@as(i64, 2_000), Bridge.model().elapsedMs); // A failure is never silence: playback clears and the honest stream // notice reads where the track title did. try h.audio(.failed, 0, 0, false); try std.testing.expectEqual(@as(?i64, null), Bridge.model().now); try std.testing.expect(Bridge.model().streamFailed); try std.testing.expect(h.hasText("stream unavailable")); try std.testing.expect(h.hasText("check the connection and try again")); // Browsing keeps working, and the next play clears the notice. try std.testing.expect(h.hasText("8 of 8")); try h.menu("sb.play.1"); try std.testing.expect(!Bridge.model().streamFailed); } // -------------------------------------------------------------- clipboard test "Copy Title lands on the clipboard" { const h = try Harness.createFake(); defer h.destroy(); const fx = &h.app_state.effects; try h.menu("sb.play.5"); try h.audio(.loaded, 0, 195_000, true); // Copy Title (the markup row's context-menu item) writes the track // title to the clipboard channel — fire-and-forget, one request. // (The app carries no volume control — parity with the Zig original; // the audioSetVolume verb stays proven by the host e2e suite's // fixture core.) try std.testing.expectEqual(@as(usize, 0), fx.pendingClipboardCount()); try h.menu("sb.copy.5"); try std.testing.expectEqual(@as(usize, 1), fx.pendingClipboardCount()); try std.testing.expectEqualStrings("Untitled", fx.pendingClipboardAt(0).?.text); try std.testing.expectEqual(@as(i64, 1), Bridge.model().copiesRequested); } // --------------------------------------------------------------- search test "search drives the full text engine from raw platform input and filters both libraries" { const h = try Harness.create(); defer h.destroy(); // Focus the markup search field and type through the platform's // text-input channel — the core's byte-splice engine applies each // event at the caret the runtime reports. try h.click(h.findId(.search_field, "").?); try h.textInput("night"); try std.testing.expectEqualStrings("night", Bridge.model().search.bytes); try std.testing.expect(h.hasText("1 of 8")); try std.testing.expect(h.hasText("Night Bloom")); try std.testing.expect(!h.hasText("Exit Signs")); // The songs library filters on title, artist, AND album title: every // Night Bloom track matches through its album. try h.menu("sb.songs"); try std.testing.expect(h.hasText("8 of 68")); try std.testing.expect(h.hasText("Japanese Maple")); // Word-delete clears the term through the caret-aware path; the // catalog returns whole. try h.keyDown("backspace"); try std.testing.expectEqualStrings("nigh", Bridge.model().search.bytes); try std.testing.expect(h.hasText("8 of 68")); try h.textInput("XYZ"); try std.testing.expect(h.hasText("0 of 68")); try std.testing.expect(h.hasText("No matches for \"nighXYZ\"")); try std.testing.expect(h.hasText("Try an album, artist, or song title.")); // Deleting the term restores the catalog whole. for (0..7) |_| try h.keyDown("backspace"); try std.testing.expectEqualStrings("", Bridge.model().search.bytes); try std.testing.expect(h.hasText("68 of 68")); } test "automation set_text replaces the search term: the select-all sentinel translates into the core's i64 mirror" { const h = try Harness.create(); defer h.destroy(); // Seed a term through real typing so the replace verb's select-all // has bytes to select. try h.click(h.findId(.search_field, "").?); try h.textInput("night"); try std.testing.expectEqualStrings("night", Bridge.model().search.bytes); // `native automate widget-action set_text` routes focus, // cmd/ctrl+A, then the replacement through the REAL input path. // Select-all synthesizes `set_selection` carrying the // `focus = maxInt(usize)` "to the end" sentinel; this core declares // its TextSelection mirror as i64, so the declared-union translation // must SATURATE the sentinel — @intCast panicked "integer does not // fit in destination type" here, killing every set_text against a // TS app on the live-GUI smoke (real typing was fine; this is the // headless twin of that crash). var buffer: [96]u8 = undefined; const command = try std.fmt.bufPrint(&buffer, "widget-action {s} {d} set-text bloom", .{ canvas_label, h.findId(.search_field, "night").? }); try h.harness.runtime.dispatchAutomationCommand(h.app, command); // The core's byte-splice engine heard the whole sequence: the snapped // select-all plus the inserted bytes REPLACE the term (append would // read "nightbloom" — that would mean the selection mirror was lost). try std.testing.expectEqualStrings("bloom", Bridge.model().search.bytes); try std.testing.expect(h.hasText("Night Bloom")); } // ------------------------------------------------------- record / replay const JournalBuffer = struct { bytes: [512 * 1024]u8 = undefined, len: usize = 0, fn sink(self: *JournalBuffer) runtime_ns.SessionRecorderSink { return .{ .context = self, .write_fn = write }; } fn write(context: *anyopaque, bytes: []const u8) anyerror!void { const self: *JournalBuffer = @ptrCast(@alignCast(context)); if (self.len + bytes.len > self.bytes.len) return error.NoSpaceLeft; @memcpy(self.bytes[self.len .. self.len + bytes.len], bytes); self.len += bytes.len; } fn journalBytes(self: *const JournalBuffer) []const u8 { return self.bytes[0..self.len]; } }; /// A value snapshot of the committed soundboard model (committed slices /// live in the core's heap — copy what outlives a session). const SoundboardSnapshot = struct { now: ?i64, playing: bool, elapsed_ms: i64, now_duration_ms: i64, queue_len: usize, copies_requested: i64, fn take() SoundboardSnapshot { const m = Bridge.model(); return .{ .now = m.now, .playing = m.playing, .elapsed_ms = m.elapsedMs, .now_duration_ms = m.nowDurationMs, .queue_len = m.queue.len, .copies_requested = m.copiesRequested, }; } }; /// One reference session: journaled user input (menu commands) plus the /// scripted audio event feed — play, load, ticks, a queue, the /// completion advance, a clipboard copy. fn recordSession(buffer: *JournalBuffer) !SoundboardSnapshot { const recorder = try std.heap.page_allocator.create(runtime_ns.SessionRecorder); defer std.heap.page_allocator.destroy(recorder); recorder.* = runtime_ns.SessionRecorder.init(buffer.sink()); recorder.begin(.{ .platform_name = "test", .app_name = "soundboard-ts-e2e", .window_width = 1080, .window_height = 720 }); const h = try Harness.createRecorded(recorder); defer h.destroy(); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); try h.menu("sb.play.2"); try h.audio(.loaded, 0, 47_000, true); try h.audio(.position, 1_500, 47_000, true); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); try h.menu("sb.queue.16"); try h.audio(.completed, 46_944, 47_000, false); try h.audio(.loaded, 0, 131_000, true); try h.menu("sb.copy.16"); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); recorder.finish(); try std.testing.expect(!recorder.failed); return SoundboardSnapshot.take(); } test "a recorded soundboard session replays byte-identically with zero host calls" { const buffer = try std.heap.page_allocator.create(JournalBuffer); defer std.heap.page_allocator.destroy(buffer); buffer.len = 0; const recorded = try recordSession(buffer); try std.testing.expectEqual(@as(?i64, 16), recorded.now); try std.testing.expect(recorded.playing); try std.testing.expectEqual(@as(i64, 130_872), recorded.now_duration_ms); try std.testing.expectEqual(@as(i64, 1), recorded.copies_requested); // Determinism pin: the same driven session records byte-identical // journal bytes. const second = try std.heap.page_allocator.create(JournalBuffer); defer std.heap.page_allocator.destroy(second); second.len = 0; const recorded_again = try recordSession(second); try std.testing.expectEqualDeep(recorded, recorded_again); try std.testing.expectEqualSlices(u8, buffer.journalBytes(), second.journalBytes()); // Replay into a fresh app: the journaled audio events feed the // re-issued (parked) playback in recorded order — no platform player, // no network, no host calls. const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = geometry.SizeF.init(1080, 720), }); defer harness.destroy(std.testing.allocator); harness.null_platform.gpu_surfaces = true; const app_state = try std.testing.allocator.create(App); defer std.testing.allocator.destroy(app_state); app_state.* = Adapter.init(std.heap.page_allocator, .{}, appOptions()); defer app_state.deinit(); const report = try runtime_ns.replaySession(&harness.runtime, app_state.app(), buffer.journalBytes(), .{ .verify = true, .require_same_platform = false, }); try std.testing.expect(report.ok()); try std.testing.expect(report.events_replayed > 0); // The journaled effect results are exactly the four audio events. try std.testing.expectEqual(@as(u64, 4), report.effects_fed); try std.testing.expectEqualDeep(recorded, SoundboardSnapshot.take()); } // ------------------------------------------------------------------ perf test "dispatch at the rendered-clock cadence stays far under the frame budget" { const h = try Harness.create(); defer h.destroy(); try h.menu("sb.play.1"); try h.audio(.loaded, 0, 165_000, true); // Whole-pipeline dispatches (update + commit + command walk + // subscription reconcile + view rebuild) at the clock cadence — the // hot path a per-frame Zig app exercises through on_frame. The // position feed is the heavier arm (it re-renders the transport). const iterations: usize = 400; const start_ns = runtime_ns.monotonicNanoseconds(); for (0..iterations) |index| { try h.audio(.position, @intCast(1_000 + index * 250), 165_000, true); } const elapsed_ns = runtime_ns.monotonicNanoseconds() - start_ns; const per_dispatch_ns = elapsed_ns / iterations; try std.testing.expectEqual(@as(i64, 1_000 + 399 * 250), Bridge.model().elapsedMs); // The core alone (update + commit + command walk + subscription // reconcile), without the runtime pipeline around it - the cost the // transpiled tier adds per tick. const fx = &h.app_state.effects; const core_start_ns = runtime_ns.monotonicNanoseconds(); for (0..iterations) |index| { Bridge.dispatch(fx, .{ .clock_tick = @floatFromInt(60_000 + index * 250) }); } const core_elapsed_ns = runtime_ns.monotonicNanoseconds() - core_start_ns; const per_core_dispatch_ns = core_elapsed_ns / iterations; h.app_state.model = Bridge.model().*; // Every whole-pipeline dispatch (update + commit + effects + the full // 68-row markup rebuild) must fit ONE 60Hz frame budget even in a // Debug build on loaded CI hardware; the core alone must be // microseconds. Measured on an M-class laptop (Debug): ~3.3ms whole // pipeline, ~450ns core-only. try std.testing.expect(per_dispatch_ns < 16_000_000); try std.testing.expect(per_core_dispatch_ns < 1_000_000); } // -------------------------------------------- round 7B: parity closure // The four launch-gate blockers the port named, closed and pinned here: // scrub-to-seek (the value-payload change event), controlled scroll (the // declared ScrollState mirror), and the generated-wiring channels the // app adopted (frame width -> grid columns, the key fallback, cover // assets, the launch env override). test "scrub-to-seek: the slider's value event seeks the engine and jumps the clock" { const h = try Harness.create(); defer h.destroy(); try h.menu("sb.play.1"); try h.audio(.loaded, 0, 165_000, true); try std.testing.expectEqual(@as(i64, 164_832), Bridge.model().nowDurationMs); // Step the seek slider through the automation increment verb — the // keyboard set_value intent resolving through markup's on-change // VALUE constructor (the applied f32 0.05 widening into the core's // one-number float arm). const slider = h.findLabel("Seek").?; var buffer: [96]u8 = undefined; const command = try std.fmt.bufPrint(&buffer, "widget-action {s} {d} increment", .{ canvas_label, slider }); try h.harness.runtime.dispatchAutomationCommand(h.app, command); // fraction 0.05 -> permille 50 of the manifest total's thousandth // (164ms): the whole-integer target of the core's two-domain split. try std.testing.expectEqual(@as(i64, 8_200), Bridge.model().elapsedMs); // And the engine's single player actually sought there (the audio_ctl // seek verb, key-gated to the open stream). try std.testing.expectEqual(@as(u64, 8_200), h.app_state.effects.audio.position_ms); // The rendered clock renders the jump. try std.testing.expect(h.hasText("0:08")); } test "controlled scroll: the offset echoes into the model and album open resets it" { const h = try Harness.create(); defer h.destroy(); // Wheel the album grid: the runtime applies the offset, and the // declared ScrollState mirror arm echoes the applied value into the // committed model (never fighting the reconcile rule). try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_input = .{ .window_id = 1, .label = canvas_label, .timestamp_ns = 1_000_000_000, .kind = .scroll, .x = 540, .y = 300, .delta_y = 240, } }); try std.testing.expect(Bridge.model().libraryScrollTop > 0); // Opening an album is a page change: the model resets the offset (the // port's named consequence — the detail page opens at its top). try h.click(h.findLabel("Exit Signs").?); try std.testing.expectEqual(@as(f64, 0), Bridge.model().libraryScrollTop); } test "the frame channel adapts the album grid to the canvas width" { const h = try Harness.create(); defer h.destroy(); // The installing frame is excluded by the UiApp contract; the first // presented frame delivers 1080 and the Zig original's width rule // answers four columns. try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{ .label = canvas_label, .size = geometry.SizeF.init(1080, 720), .scale_factor = 1, .frame_index = 2, .timestamp_ns = 2_000_000, } }); try std.testing.expectEqual(@as(i64, 1080), Bridge.model().canvasWidth); try std.testing.expectEqual(@as(usize, 4), findGrid(h.app_state.tree.?.root).?.layout.columns); // A live resize re-derives: 1400pt fits five 232pt tiles. try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{ .label = canvas_label, .size = geometry.SizeF.init(1400, 720), .scale_factor = 1, .frame_index = 3, .timestamp_ns = 3_000_000, } }); try std.testing.expectEqual(@as(i64, 1400), Bridge.model().canvasWidth); try std.testing.expectEqual(@as(usize, 5), findGrid(h.app_state.tree.?.root).?.layout.columns); } test "the key fallback drives the transport: space toggles, arrows change tracks" { const h = try Harness.create(); defer h.destroy(); // Nothing focused: space falls through the widget precedence rule to // the core's keyMsg (delivered lowercased) and starts playback. try h.keyDown("Space"); try h.audio(.loaded, 0, 0, true); try std.testing.expect(Bridge.model().playing); try std.testing.expectEqual(@as(?i64, 1), Bridge.model().now); // Right arrow advances within the album; space pauses. try h.keyDown("ArrowRight"); try std.testing.expectEqual(@as(?i64, 2), Bridge.model().now); try h.keyDown("Space"); try std.testing.expect(!Bridge.model().playing); } test "cover assets register at install and bind the grid avatars by album id" { // PNG twins through the adapter's boot-image channel (the committed // art is JPEG for real hosts' codecs; the null platform's strict // decoder takes the engine's own PNG output — same register path the // generated wiring drives from app.zon). const rgba = [_]u8{ 200, 40, 40, 255 } ** 4; var encoded: [256]u8 = undefined; var png_writer = std.Io.Writer.fixed(&encoded); try canvas.png.writeRgba8(&png_writer, 2, 2, &rgba); const boot_images = [_]Adapter.BootImage{.{ .id = 1, .bytes = png_writer.buffered() }}; const h = try Harness.createConfigured(null, .real, .{ .boot_images = &boot_images }); defer h.destroy(); // Registered on the installing frame, before the first view build. try std.testing.expectEqual(@as(usize, 1), h.harness.runtime.canvas_image_count); // The grid avatar binds the album id as its ImageId (unregistered ids // keep the initials fallback, so the other seven tiles degrade // honestly). var covers: usize = 0; countAvatarImageIds(h.app_state.tree.?.root, 1, &covers); try std.testing.expectEqual(@as(usize, 1), covers); } test "the launch env override rides the envMsgs channel: local-only failures report the assets notice" { // An EMPTY NATIVE_SDK_MUSIC_URL_BASE turns streaming off — the Zig // original's launch split, delivered here as one journaled Msg. const env_values = [_]Adapter.EnvValue{.{ .msg = "url_base_set", .value = "" }}; const h = try Harness.createConfigured(null, .real, .{ .env_values = &env_values }); defer h.destroy(); try h.menu("sb.play.1"); // The issued play carries NO stream URL (local-only). try std.testing.expectEqual(@as(usize, 0), h.app_state.effects.audio.url_len); try h.audio(.failed, 0, 0, false); try std.testing.expect(h.hasText("music assets not prepared")); try std.testing.expect(!h.hasText("stream unavailable")); } test "a non-empty env base replaces the stream host wholesale" { const env_values = [_]Adapter.EnvValue{.{ .msg = "url_base_set", .value = "https://mirror.test/" }}; const h = try Harness.createConfigured(null, .real, .{ .env_values = &env_values }); defer h.destroy(); try h.menu("sb.play.1"); // Trailing slashes trim; the track path's "assets" prefix drops. try std.testing.expectEqualStrings( "https://mirror.test/music/exit-signs/mile-marker-west.mp3", h.app_state.effects.audio.url_buffer[0..h.app_state.effects.audio.url_len], ); // A failure with the stream configured stays the stream notice. try h.audio(.failed, 0, 0, false); try std.testing.expect(h.hasText("stream unavailable")); } test "album tiles are quiet on hover (the original's quiet-tile treatment)" { const h = try Harness.create(); defer h.destroy(); // Every grid tile carries the quiet-surface knob: no hover wash on // image-forward cover art, while press/selection fills, the focus // ring, and hit testing keep their own channels. Track rows keep // their washes (the wash IS the affordance on acting controls). const grid = findGrid(h.app_state.tree.?.root).?; try std.testing.expect(grid.children.len > 0); for (grid.children) |tile| { try std.testing.expectEqual(canvas.WidgetKind.list_item, tile.kind); try std.testing.expect(tile.style.quiet_hover); } try h.menu("sb.songs"); var row_count: usize = 0; countLoudListItems(h.app_state.tree.?.root, &row_count); try std.testing.expect(row_count >= 60); } fn countLoudListItems(widget: canvas.Widget, count: *usize) void { if (widget.kind == .list_item and !widget.style.quiet_hover) count.* += 1; for (widget.children) |child| { countLoudListItems(child, count); } } fn findGrid(widget: canvas.Widget) ?canvas.Widget { if (widget.kind == .grid) return widget; for (widget.children) |child| { if (findGrid(child)) |found| return found; } return null; } fn countAvatarImageIds(widget: canvas.Widget, id: canvas.ImageId, count: *usize) void { if (widget.kind == .avatar and widget.image_id == id) count.* += 1; for (widget.children) |child| { countAvatarImageIds(child, id, count); } } // --------------------------------------------------- transport bar layout fn transportBarFrame(h: *Harness) ?geometry.RectF { const layout = h.harness.runtime.canvasWidgetLayout(1, canvas_label) catch return null; for (layout.nodes) |node| { if (std.mem.eql(u8, node.widget.semantics.label, "Now playing bar")) return node.frame; } return null; } fn expectBarFlushAtBottom(h: *Harness, canvas_height: f32) !void { const frame = transportBarFrame(h) orelse return error.TransportBarMissing; try std.testing.expectEqual(@as(f32, 76), frame.height); try std.testing.expectApproxEqAbs(canvas_height - 76, frame.y, 0.01); } test "the transport bar stays flush at the bottom across a rebuild storm" { const h = try Harness.create(); defer h.destroy(); // Playing state: the rebuild storm below is the live app's worst // case — clock ticks and position events rebuild every 250ms. try h.menu("sb.playalbum.2"); try h.audio(.loaded, 0, 0, true); try expectBarFlushAtBottom(h, 720); // The regression this pins (the runtime's presented-size adoption): // during a live resize the presented FRAMES carry the drawable's new // size before the window-manager resize event lands. Every // dispatch-driven rebuild inside that gap used to lay out at the // stale bounds, painting the transport bar below the shrunk // window's bottom edge (cut off) until the resize event arrived. try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{ .label = canvas_label, .size = geometry.SizeF.init(900, 600), .scale_factor = 1, .frame_index = 2, .timestamp_ns = 2_000_000, } }); try expectBarFlushAtBottom(h, 600); // A storm of dispatch-driven rebuilds in the gap (no resize event // yet): clock ticks, a position correction, page changes, search // keystrokes, a queue change — the bar must hold the new bottom // through every one. _ = try h.firePlayclock(3_000_000_000); try expectBarFlushAtBottom(h, 600); try h.audio(.position, 1500, 0, true); try expectBarFlushAtBottom(h, 600); try h.menu("sb.open.2"); try expectBarFlushAtBottom(h, 600); try h.menu("sb.albums"); try expectBarFlushAtBottom(h, 600); try h.menu("sb.songs"); try expectBarFlushAtBottom(h, 600); try h.menu("sb.queue.9"); try expectBarFlushAtBottom(h, 600); try h.click(h.findLabel("Search library").?); try h.textInput("g"); try expectBarFlushAtBottom(h, 600); // The late resize event is a no-reflow confirmation. try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_resized = .{ .window_id = 1, .label = canvas_label, .frame = geometry.RectF.init(0, 0, 900, 600), .scale_factor = 1, } }); try expectBarFlushAtBottom(h, 600); // Growing back mid-playback holds the same law from the other side. try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{ .label = canvas_label, .size = geometry.SizeF.init(1080, 720), .scale_factor = 1, .frame_index = 3, .timestamp_ns = 3_000_000, } }); try expectBarFlushAtBottom(h, 720); _ = try h.firePlayclock(4_000_000_000); try expectBarFlushAtBottom(h, 720); } // ------------------------------------------------------------ theme accent test "the manifest accent layers the original's pink over the pack (high contrast skips it)" { const h = try Harness.create(); defer h.destroy(); // The resolved rebuild tokens carry the accent identity bundle: the // pink accent under white knockout ink, the focus ring moved with // the identity, and the seek slider's filled range restated — the // Zig original's tokens_fn, reached through app.zon's theme_accent. const pink = canvas.Color.rgb8(0xdf, 0x26, 0x70); const tokens = h.app_state.effectiveTokens(); try std.testing.expectEqual(pink, tokens.colors.accent); try std.testing.expectEqual(canvas.Color.rgb8(255, 255, 255), tokens.colors.accent_text); try std.testing.expectEqual(pink, tokens.colors.focus_ring); try std.testing.expectEqual(pink, tokens.controls.slider.active_background); // High contrast takes the pack's own loud register with no brand // layer (accessibility beats brand — the original's rule). try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .appearance_changed = .{ .color_scheme = .light, .high_contrast = true, } }); const loud = h.app_state.effectiveTokens(); try std.testing.expect(!std.meta.eql(pink, loud.colors.accent)); } // --------------------------------------------------- parity captures // Env-gated parity capture (skipped by default, never in CI): renders // the port OFFSCREEN through the deterministic reference renderer via // the automation screenshot artifact — the side-by-side proof against // the Zig original's identical states. PNGs land in // /tmp/parity-shots/ts-{grid,detail,songs}-{0,1}-artifacts/. To use // (the magick loop prepares RGBA twins of the committed covers once, // the homepage-shots recipe): // // mkdir -p /tmp/soundboard-art // for f in examples/soundboard/src/art/*.jpg; do // magick "$f" -depth 8 rgba:/tmp/soundboard-art/"$(basename "${f%.jpg}")".rgba // done // PARITY_SHOTS=1 zig build test-ts-core-e2e test "render parity screenshots (env-gated)" { if (!envGateSet("PARITY_SHOTS")) return error.SkipZigTest; const io = std.testing.io; const sizes = [_]geometry.SizeF{ geometry.SizeF.init(1080, 720), geometry.SizeF.init(1400, 800) }; const cover_stems = [_][]const u8{ "exit-signs", "blue-season", "second-nature", "no-good-way-out", "glass-flowers", "night-bloom", "motion-picture", "channel-surfing", }; for (sizes, 0..) |size, size_index| { const h = try Harness.create(); defer h.destroy(); // The standard macOS tall hidden-inset chrome geometry: the // resize re-query dispatches it through the core's chromeMsg. h.harness.null_platform.window_chrome = .{ .insets = .{ .top = 52, .left = 78 }, .buttons = geometry.RectF.init(20, 19, 52, 14), }; try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_resized = .{ .window_id = 1, .label = canvas_label, .frame = geometry.RectF.init(0, 0, size.width, size.height), .scale_factor = 1, } }); // Real covers on the real decode->register path: RGBA twins of // the committed JPEG art through the engine's own PNG writer // (the null platform's strict decoder refuses JPEG honestly). var art_arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer art_arena_state.deinit(); const art_arena = art_arena_state.allocator(); for (cover_stems, 1..) |stem, album_id| { var path_buffer: [128]u8 = undefined; const path = try std.fmt.bufPrint(&path_buffer, "/tmp/soundboard-art/{s}.rgba", .{stem}); const rgba = std.Io.Dir.cwd().readFileAlloc(io, path, art_arena, .limited(8 * 1024 * 1024)) catch continue; const side = std.math.sqrt(rgba.len / 4); const encoded = try art_arena.alloc(u8, try canvas.png.encodedRgba8ByteLen(side, side)); var png_writer = std.Io.Writer.fixed(encoded); try canvas.png.writeRgba8(&png_writer, side, side, rgba); _ = try h.app_state.effects.registerImageBytes(album_id, png_writer.buffered()); } // The shared reference state (identical on the Zig original's // side): Summer Rental (track 9, album 2) playing at 24636ms of // its 98544ms manifest total — exactly 0.25 on both progress // derivations — under the light scheme. try h.menu("sb.play.9"); try h.audio(.loaded, 0, 0, true); try h.audio(.position, 24_636, 0, true); try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .appearance_changed = .{ .color_scheme = .light } }); try presentParityFrame(h, size, 2); try parityShot(h, io, "ts-grid", size_index); try h.menu("sb.open.2"); try presentParityFrame(h, size, 3); try parityShot(h, io, "ts-detail", size_index); try h.menu("sb.albums"); try h.menu("sb.songs"); try presentParityFrame(h, size, 4); try parityShot(h, io, "ts-songs", size_index); } } /// Env-gated dump switch (the original suite's helper): `std.c.getenv` /// needs libc, which this test build links on macOS; when absent the /// gate reads unset and the gated test skips. fn envGateSet(name: [*:0]const u8) bool { if (comptime !@import("builtin").link_libc) return false; return std.c.getenv(name) != null; } fn presentParityFrame(h: *Harness, size: geometry.SizeF, frame_index: u64) !void { try h.harness.runtime.dispatchPlatformEvent(h.app, .{ .gpu_surface_frame = .{ .label = canvas_label, .size = size, .scale_factor = 1, .frame_index = frame_index, .timestamp_ns = frame_index * 1_000_000, .nonblank = true, } }); } fn parityShot(h: *Harness, io: std.Io, comptime name: []const u8, size_index: usize) !void { var dir_buffer: [96]u8 = undefined; const dir = try std.fmt.bufPrint(&dir_buffer, "/tmp/parity-shots/{s}-{d}-artifacts", .{ name, size_index }); h.harness.runtime.options.automation = native_sdk.automation.Server.init(io, dir, "SoundboardTS"); try h.harness.runtime.dispatchAutomationCommand(h.app, "screenshot soundboard-canvas 2"); }