//! End-to-end: a GENUINELY TRANSPILED core (tests/ts-core/fixture.ts, //! emitted by the repo's own transpiler at build time — see the //! ts-core-e2e wiring in build.zig) driven through the real //! runtime-core dispatch path: the first-class `TsUiApp(core)` adapter //! (the committed TS model IS the app model — the view below reads it //! straight off the UiApp), the null platform's live timer services, a //! stub `HostCallBinding` standing in for host services, and the //! session recorder. Timers fire, requests round-trip, replace/cancel //! keep the wire contract, `Cmd.now` stamps synchronously, a REAL //! subprocess streams lines into the core (and dies to a mid-stream //! cancel), audio events flow the soundboard way (the fake channel's //! scripted feed), and recorded sessions — streams included — replay //! to identical state without a host call or a process launch. //! //! The markup-view / automation / pixel-fingerprint guarantees run in //! markup_e2e_tests.zig over a second transpiled core. const std = @import("std"); const builtin = @import("builtin"); const native_sdk = @import("native_sdk"); const fixture = @import("ts_core_fixture"); const runtime_ns = native_sdk.runtime; const Adapter = native_sdk.TsUiApp(fixture); /// The same instantiation the adapter drives (comptime memoization): /// assertions may read the committed model straight off the bridge. const Bridge = Adapter.Host; test { _ = @import("markup_e2e_tests.zig"); } const canvas_label = "ts-core-canvas"; const e2e_views = [_]native_sdk.ShellView{ .{ .label = canvas_label, .kind = .gpu_surface, .fill = true, .gpu_backend = .metal }, }; const e2e_windows = [_]native_sdk.ShellWindow{.{ .label = "main", .title = "TS Core", .width = 400, .height = 300, .views = &e2e_views, }}; const e2e_scene: native_sdk.ShellConfig = .{ .windows = &e2e_windows }; const App = Adapter.App; /// A hand-written builder view over the COMMITTED TS MODEL — the model /// parameter is the UiApp-held root the adapter refreshes each /// dispatch, so this view (and the replay fingerprints derived from /// what it renders) pins the transpiled core's state directly. fn e2eView(ui: *App.Ui, model: *const fixture.Model) App.Ui.Node { return ui.column(.{ .gap = 4, .padding = 8 }, .{ ui.text(.{}, ui.fmt("ticks {d} failures {d}", .{ model.ticks, model.failures })), ui.text(.{}, ui.fmt("status {s}", .{model.status})), }); } fn e2eCommand(name: []const u8) ?fixture.Msg { if (std.mem.eql(u8, name, "core.toggle")) return .toggle; if (std.mem.eql(u8, name, "core.refresh")) return .refresh; if (std.mem.eql(u8, name, "core.abort")) return .abort; if (std.mem.eql(u8, name, "core.stamp")) return .stamp; if (std.mem.eql(u8, name, "core.note")) return .note; if (std.mem.eql(u8, name, "core.save")) return .save; if (std.mem.eql(u8, name, "core.load")) return .load; if (std.mem.eql(u8, name, "core.get")) return .get; if (std.mem.eql(u8, name, "core.share")) return .share; if (std.mem.eql(u8, name, "core.paste")) return .paste; if (std.mem.eql(u8, name, "core.later")) return .later; if (std.mem.eql(u8, name, "core.halt")) return .halt; if (std.mem.eql(u8, name, "core.run")) return .run; if (std.mem.eql(u8, name, "core.hang")) return .hang; if (std.mem.eql(u8, name, "core.kill")) return .kill; if (std.mem.eql(u8, name, "core.play")) return .play; if (std.mem.eql(u8, name, "core.pause")) return .pause_music; if (std.mem.eql(u8, name, "core.volume")) return .set_volume; if (std.mem.eql(u8, name, "core.stopmusic")) return .stop_music; return null; } /// The fixture's literal store path (cwd-relative, under the zig /// cache like every tmp-dir artifact) and its parent, deleted around /// tests so every run starts from an absent store. const store_path = ".zig-cache/tmp/ts-core-e2e/store.bin"; const store_dir = ".zig-cache/tmp/ts-core-e2e"; fn removeStore() void { std.Io.Dir.cwd().deleteTree(std.testing.io, store_dir) catch {}; } fn e2eOptions() App.Options { return .{ .name = "ts-core-e2e", .scene = e2e_scene, .canvas_label = canvas_label, .view = e2eView, .on_command = e2eCommand, }; } /// The boot request's engine key: the bridge assigns table slot 0 to /// the first issued request, deterministically. const status_request_key: u64 = runtime_ns.ts_core_request_key_base + 0; /// The subscription timer's platform id: bridge timer slot 0 lands in /// engine timer slot 0. const tick_platform_id: u64 = runtime_ns.effect_timer_platform_id_base + 0; /// The first named engine op (readFile/writeFile/fetch/clipboardRead) /// takes bridge op slot 0, deterministically in issue order. const first_effect_key: u64 = runtime_ns.ts_core_effect_key_base + 0; /// The delay's platform id: with the subscription tick occupying /// engine timer slot 0 from boot, the first armed delay lands in /// engine timer slot 1. const delay_platform_id: u64 = runtime_ns.effect_timer_platform_id_base + 1; /// The stub host service: records sends and parks requests (name/key) /// for the test to answer through `feedHostResult` — an async host in /// miniature. const HostStub = struct { var send_count: usize = 0; var last_send_name: [64]u8 = undefined; var last_send_name_len: usize = 0; var last_send_payload: [64]u8 = undefined; var last_send_payload_len: usize = 0; var request_count: usize = 0; var last_request_name: [64]u8 = undefined; var last_request_name_len: usize = 0; var last_request_payload: [64]u8 = undefined; var last_request_payload_len: usize = 0; var last_request_key: u64 = 0; var cancel_count: usize = 0; fn reset() void { send_count = 0; request_count = 0; cancel_count = 0; } fn send(context: *anyopaque, name: []const u8, payload: []const u8) void { _ = context; send_count += 1; @memcpy(last_send_name[0..name.len], name); last_send_name_len = name.len; @memcpy(last_send_payload[0..payload.len], payload); last_send_payload_len = payload.len; } fn request(context: *anyopaque, name: []const u8, key: u64, payload: []const u8) void { _ = context; request_count += 1; @memcpy(last_request_name[0..name.len], name); last_request_name_len = name.len; @memcpy(last_request_payload[0..payload.len], payload); last_request_payload_len = payload.len; last_request_key = key; } fn cancelNotice(context: *anyopaque, key: u64) void { _ = context; _ = key; cancel_count += 1; } fn binding() native_sdk.HostCallBinding { return .{ .context = @ptrCast(&stub_context), .send_fn = send, .request_fn = request, .cancel_fn = cancelNotice, }; } var stub_context: u8 = 0; }; 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 whose effects channel runs the fake executor: named /// engine ops park in fake slots for `feed*` answers (the fetch /// tests — real-mode fetch would reach the network). fn createFake() !*Harness { return createFull(null, .fake); } fn createRecorded(recorder: ?*runtime_ns.SessionRecorder) !*Harness { return createFull(recorder, .real); } /// `recorder` (if any) attaches BEFORE start so the journal holds /// the app_start and installing-frame events — replay re-runs /// init_fx (and its boot request) from those. fn createFull(recorder: ?*runtime_ns.SessionRecorder, executor: runtime_ns.EffectExecutor) !*Harness { const self = try std.testing.allocator.create(Harness); errdefer std.testing.allocator.destroy(self); self.clock = .{}; self.clock.setWallMs(50_000); self.harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = native_sdk.geometry.SizeF.init(400, 300), }); errdefer self.harness.destroy(std.testing.allocator); self.harness.null_platform.gpu_surfaces = 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, .{}, e2eOptions()); // Bind the stub host services, the executor mode, and the // deterministic clock BEFORE install: init_fx issues the boot // request. self.app_state.effects.bindHostCalls(HostStub.binding()); 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 = native_sdk.geometry.SizeF.init(400, 300), .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 menu(self: *Harness, name: []const u8) !void { try self.harness.runtime.dispatchPlatformEvent(self.app, .{ .menu_command = .{ .name = name, .window_id = 1 } }); } fn wake(self: *Harness) !void { try self.harness.runtime.dispatchPlatformEvent(self.app, .wake); } fn fireTick(self: *Harness, timestamp_ns: u64) !bool { const event = self.harness.null_platform.fireTimer(tick_platform_id, timestamp_ns) orelse return false; try self.harness.runtime.dispatchPlatformEvent(self.app, event); return true; } fn tickArmed(self: *Harness) bool { const timer = self.harness.null_platform.startedTimer(tick_platform_id) orelse return false; return timer.active; } fn fireDelay(self: *Harness, timestamp_ns: u64) !bool { const event = self.harness.null_platform.fireTimer(delay_platform_id, timestamp_ns) orelse return false; try self.harness.runtime.dispatchPlatformEvent(self.app, event); return true; } fn delayArmed(self: *Harness) bool { const timer = self.harness.null_platform.startedTimer(delay_platform_id) orelse return false; return timer.active; } /// Wait for a real-executor worker's terminal to reach the queue /// WITHOUT dispatching events — the wait leaves no trace in a /// recorded session, so the one `wake` that drains afterwards /// keeps journals byte-identical across recordings. fn waitPending(self: *Harness) !void { const io = std.testing.io; var waited_ms: usize = 0; while (waited_ms < 20_000) : (waited_ms += 10) { if (self.app_state.effects.hasPending()) return; try std.Io.sleep(io, std.Io.Duration.fromMilliseconds(10), .awake); } return error.TestTimedOut; } /// Wait for every running effect to FINISH (not just for the first /// queued entry) — the streaming determinism wait: a spawned /// child's lines and exit all sit in the queue before the one /// `wake` drains them, so two recordings journal identical event /// boundaries regardless of worker timing. fn waitIdle(self: *Harness) !void { const io = std.testing.io; var waited_ms: usize = 0; while (waited_ms < 20_000) : (waited_ms += 10) { if (self.app_state.effects.activeCount() == 0 and self.app_state.effects.hasPending()) return; try std.Io.sleep(io, std.Io.Duration.fromMilliseconds(10), .awake); } return error.TestTimedOut; } }; test "the transpiled core boots through init_fx: boot request and subscription timer are live" { HostStub.reset(); const h = try Harness.create(); defer h.destroy(); // The init command reached the stub host service before the first // frame, with the bridge's deterministic engine key. try std.testing.expectEqual(@as(usize, 1), HostStub.request_count); try std.testing.expectEqualStrings("status.read", HostStub.last_request_name[0..HostStub.last_request_name_len]); try std.testing.expectEqualStrings("boot", HostStub.last_request_payload[0..HostStub.last_request_payload_len]); try std.testing.expectEqual(status_request_key, HostStub.last_request_key); // The model-declared subscription armed a REAL platform timer. try std.testing.expect(h.tickArmed()); // The boot model committed — and the UiApp-held root IS the // committed value (the adapter's refresh), not a shim. try std.testing.expect(Bridge.model().polling); try std.testing.expectEqual(@as(i64, 0), Bridge.model().ticks); try std.testing.expect(h.app_state.model.polling); try std.testing.expectEqual(@as(i64, 0), h.app_state.model.ticks); } test "requests round-trip, replace, and cancel through the real dispatch path" { HostStub.reset(); const h = try Harness.create(); defer h.destroy(); const fx = &h.app_state.effects; // The host answers the boot request; the ok arm lands on the next // drain and the bytes commit into the core's model heap — visible // through the bridge and the UiApp-held root alike. try fx.feedHostResult(status_request_key, true, "ready"); try h.wake(); try std.testing.expectEqualStrings("ready", Bridge.model().status); try std.testing.expectEqualStrings("ready", h.app_state.model.status); // refresh re-issues the same wire key: the stub sees a second // request under the SAME engine key (replace, not a new slot). try h.menu("core.refresh"); try std.testing.expectEqual(@as(usize, 2), HostStub.request_count); try std.testing.expectEqual(status_request_key, HostStub.last_request_key); try std.testing.expectEqualStrings("ready", HostStub.last_request_payload[0..HostStub.last_request_payload_len]); // The err route counts a failure. try fx.feedHostResult(status_request_key, false, "boom"); try h.wake(); try std.testing.expectEqual(@as(i64, 1), Bridge.model().failures); try std.testing.expectEqualStrings("ready", Bridge.model().status); // cancel drops the in-flight request silently: the host gets the // abort notice, a late answer finds nothing, neither arm runs. try h.menu("core.refresh"); try h.menu("core.abort"); try std.testing.expectEqual(@as(usize, 1), HostStub.cancel_count); try std.testing.expectError(error.EffectNotFound, fx.feedHostResult(status_request_key, true, "late")); try h.wake(); try std.testing.expectEqual(@as(i64, 1), Bridge.model().failures); try std.testing.expectEqualStrings("ready", Bridge.model().status); } test "subscription timers fire through the platform and reconcile on model changes" { HostStub.reset(); const h = try Harness.create(); defer h.destroy(); // A platform fire dispatches the tick arm with the time in ms. try std.testing.expect(try h.fireTick(250_000_000)); try std.testing.expectEqual(@as(i64, 1), Bridge.model().ticks); try std.testing.expectEqual(@as(f64, 250), Bridge.model().lastTickAt); // Pausing removes the timer from the platform; a stale fire event // dispatches nothing. try h.menu("core.toggle"); try std.testing.expect(!h.tickArmed()); try std.testing.expect(!try h.fireTick(300_000_000)); try std.testing.expectEqual(@as(i64, 1), Bridge.model().ticks); // Resuming re-arms the same deterministic slot. try h.menu("core.toggle"); try std.testing.expect(h.tickArmed()); try std.testing.expect(try h.fireTick(400_000_000)); try std.testing.expectEqual(@as(i64, 2), Bridge.model().ticks); try std.testing.expectEqual(@as(f64, 400), Bridge.model().lastTickAt); } test "Cmd.now stamps synchronously and host_bytes reaches the stub service" { HostStub.reset(); const h = try Harness.create(); defer h.destroy(); // now: the stamped arm ran within the dispatch, with the bound // (test) clock's journal-ready reading. try h.menu("core.stamp"); try std.testing.expectEqual(@as(f64, 50_000), Bridge.model().stampMs); // host_bytes: fire-and-forget to the named service. try h.menu("core.note"); try std.testing.expectEqual(@as(usize, 1), HostStub.send_count); try std.testing.expectEqualStrings("blob.put", HostStub.last_send_name[0..HostStub.last_send_name_len]); try std.testing.expectEqualStrings("hi", HostStub.last_send_payload[0..HostStub.last_send_payload_len]); } // -------------------------------------------------- named engine ops test "writeFile and readFile round-trip real disk through the transpiled core" { const io = std.testing.io; HostStub.reset(); removeStore(); defer removeStore(); const h = try Harness.create(); defer h.destroy(); const fx = &h.app_state.effects; // Give the model content to persist. try fx.feedHostResult(status_request_key, true, "ready"); try h.wake(); // save: the core's write_file record reaches the REAL executor and // the bytes land on disk whole; the payload-less ok arm counts. try h.menu("core.save"); try h.waitPending(); try h.wake(); try std.testing.expectEqual(@as(i64, 1), Bridge.model().saved); const on_disk = try std.Io.Dir.cwd().readFileAlloc(io, store_path, std.testing.allocator, .limited(4096)); defer std.testing.allocator.free(on_disk); try std.testing.expectEqualStrings("ready", on_disk); // Overwrite the model, then load: the read routes the ok arm with // the disk bytes and they commit into the model heap. try h.menu("core.refresh"); try fx.feedHostResult(status_request_key, true, "stale"); try h.wake(); try std.testing.expectEqualStrings("stale", Bridge.model().status); try h.menu("core.load"); try h.waitPending(); try h.wake(); try std.testing.expectEqualStrings("ready", Bridge.model().status); try std.testing.expectEqual(@as(i64, 0), Bridge.model().failures); // A missing store routes the err arm with the outcome name. removeStore(); try h.menu("core.load"); try h.waitPending(); try h.wake(); try std.testing.expectEqual(@as(i64, 1), Bridge.model().failures); try std.testing.expectEqualStrings("not_found", Bridge.model().lastErr); try std.testing.expectEqualStrings("ready", Bridge.model().status); } test "clipboardWrite and clipboardRead ride the platform pasteboard" { HostStub.reset(); const h = try Harness.create(); defer h.destroy(); const fx = &h.app_state.effects; try fx.feedHostResult(status_request_key, true, "ready"); try h.wake(); // share: fire-and-forget onto the real (null-platform) pasteboard. try h.menu("core.share"); try h.wake(); try std.testing.expectEqual(@as(usize, 1), h.harness.null_platform.clipboardWriteCount()); try std.testing.expectEqualStrings("ready", h.harness.null_platform.lastClipboardData()); // Change the model, then paste: the read's ok arm restores it. try h.menu("core.refresh"); try fx.feedHostResult(status_request_key, true, "fresh"); try h.wake(); try std.testing.expectEqualStrings("fresh", Bridge.model().status); try h.menu("core.paste"); try h.wake(); try std.testing.expectEqualStrings("ready", Bridge.model().status); try std.testing.expectEqual(@as(i64, 0), Bridge.model().failures); } test "fetch parks on the engine and routes the { status, body } record and err reasons" { HostStub.reset(); const h = try Harness.createFake(); defer h.destroy(); const fx = &h.app_state.effects; try fx.feedHostResult(status_request_key, true, "ready"); try h.wake(); // The fetch record decodes whole: verb, url, header pair, body // (the model's bytes), and the explicit timeout. try h.menu("core.get"); try std.testing.expectEqual(@as(usize, 1), fx.pendingFetchCount()); const request = fx.pendingFetchAt(0).?; try std.testing.expectEqual(first_effect_key, request.key); try std.testing.expectEqual(std.http.Method.POST, request.method); try std.testing.expectEqualStrings("https://status.test/feed", request.url); try std.testing.expectEqual(@as(usize, 1), request.headers.len); try std.testing.expectEqualStrings("accept", request.headers[0].name); try std.testing.expectEqualStrings("text/plain", request.headers[0].value); try std.testing.expectEqualStrings("ready", request.body); // A non-2xx response is still the ok route: the number field takes // the status, the bytes field the body. try fx.feedResponse(first_effect_key, 404, "feed data"); try h.wake(); try std.testing.expectEqual(@as(f64, 404), Bridge.model().code); try std.testing.expectEqualStrings("feed data", Bridge.model().status); // A transport failure routes the err arm with the outcome name. try h.menu("core.get"); try fx.feedResponseOutcome(first_effect_key, .timed_out, 0, ""); try h.wake(); try std.testing.expectEqual(@as(i64, 1), Bridge.model().failures); try std.testing.expectEqualStrings("timed_out", Bridge.model().lastErr); try std.testing.expectEqual(@as(f64, 404), Bridge.model().code); } test "a delay arms a real platform timer, fires once, re-arms on re-issue, and cancels" { HostStub.reset(); const h = try Harness.create(); defer h.destroy(); // later: one-shot platform timer in the delay slot (the tick // subscription holds engine timer slot 0 from boot). try h.menu("core.later"); try std.testing.expect(h.delayArmed()); const timer = h.harness.null_platform.startedTimer(delay_platform_id).?; try std.testing.expectEqual(@as(u64, 150 * std.time.ns_per_ms), timer.interval_ns); try std.testing.expect(!timer.repeats); // The fire dispatches the named arm with the time in fractional ms // and the slot retires (one-shots self-stop). try std.testing.expect(try h.fireDelay(500_000_000)); try std.testing.expectEqual(@as(f64, 500), Bridge.model().firedAt); // Re-issuing a live delay key re-arms the SAME slot; halt cancels // it silently — a later stale fire event dispatches nothing. try h.menu("core.later"); try h.menu("core.later"); try std.testing.expect(h.delayArmed()); try h.menu("core.halt"); try std.testing.expect(!h.delayArmed()); try std.testing.expect(!try h.fireDelay(900_000_000)); try std.testing.expectEqual(@as(f64, 500), Bridge.model().firedAt); try std.testing.expectEqual(@as(i64, 0), Bridge.model().failures); } // ------------------------------------------------------------- streams /// The first spawn stream's engine key: bridge stream slot 0, /// deterministic in issue order like every bridge table. const job_spawn_key: u64 = runtime_ns.ts_core_spawn_key_base + 0; test "a spawn stream runs a real subprocess: lines route in order and the exit code lands" { if (builtin.target.os.tag == .windows) return error.SkipZigTest; HostStub.reset(); const h = try Harness.create(); defer h.destroy(); // Retire the boot request first: the idle wait below watches the // engine's ACTIVE slots, and an unanswered host request would hold // one forever. try h.app_state.effects.feedHostResult(status_request_key, true, "ready"); try h.wake(); // A real /bin/sh child prints two lines and exits 0. Waiting for // the channel to go idle parks the whole stream (both lines, then // the exit) in the queue before one deterministic drain. try h.menu("core.run"); try h.waitIdle(); try h.wake(); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().lines), 2), Bridge.model().lines); try std.testing.expectEqualStrings("two", Bridge.model().lastLine); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().exitCode), 0), Bridge.model().exitCode); try std.testing.expectEqual(@as(i64, 0), Bridge.model().failures); // The exit retired the stream: the wire key is free for a rerun. try h.menu("core.run"); try h.waitIdle(); try h.wake(); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().lines), 4), Bridge.model().lines); } test "cancelling a spawn mid-stream ends the real child and routes the err arm" { if (builtin.target.os.tag == .windows) return error.SkipZigTest; HostStub.reset(); const h = try Harness.create(); defer h.destroy(); try h.app_state.effects.feedHostResult(status_request_key, true, "ready"); try h.wake(); // The child would sleep 30s; the wire cancel ends it now and the // engine's `.cancelled` exit routes the err arm — never silent. try h.menu("core.hang"); try h.menu("core.kill"); try h.waitIdle(); try h.wake(); try std.testing.expectEqual(@as(i64, 1), Bridge.model().failures); try std.testing.expectEqualStrings("cancelled", Bridge.model().lastErr); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().lines), 0), Bridge.model().lines); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().exitCode), -1), Bridge.model().exitCode); } test "audio playback streams events into the transpiled core through the fake channel" { HostStub.reset(); const h = try Harness.createFake(); defer h.destroy(); const fx = &h.app_state.effects; // play opens the stream: the engine channel records the request // whole under the bridge's audio key. try h.menu("core.play"); const request = fx.pendingAudio().?; try std.testing.expectEqual(runtime_ns.ts_core_audio_key_base, request.key); try std.testing.expectEqualStrings("music/track.mp3", request.path); // The scripted event feed (the same drive soundboard's tests use) // routes the six-field arm: loaded, position ticks, spectrum bands. try fx.feedAudioEvent(.loaded, 0, 183_000, true); try h.wake(); try std.testing.expect(Bridge.model().audioState == .loaded); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().durMs), 183_000), Bridge.model().durMs); try std.testing.expect(Bridge.model().playing); try fx.feedAudioEvent(.position, 1_500, 183_000, true); try h.wake(); try std.testing.expect(Bridge.model().audioState == .position); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().posMs), 1_500), Bridge.model().posMs); var bands: [32]u8 = undefined; for (&bands, 0..) |*b, i| b.* = @intCast(i * 7); try fx.feedAudioSpectrum(bands, 2_000, 183_000); try h.wake(); try std.testing.expect(Bridge.model().audioState == .spectrum); try std.testing.expectEqualSlices(u8, &bands, Bridge.model().bands); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().audioEvents), 3), Bridge.model().audioEvents); // Control verbs drive the channel in place; stop closes the stream // — a later feed finds no playback to receive it. try h.menu("core.pause"); try std.testing.expect(!fx.audioSnapshot().playing); try h.menu("core.volume"); try std.testing.expectApproxEqAbs(@as(f32, 0.8), fx.pendingAudio().?.volume, 0.001); try h.menu("core.stopmusic"); try std.testing.expect(!fx.audioSnapshot().active); try std.testing.expectError(error.EffectNotFound, fx.feedAudioEvent(.position, 3_000, 183_000, true)); try std.testing.expectEqual(@as(@TypeOf(Bridge.model().audioEvents), 3), Bridge.model().audioEvents); } // -------------------------------------------------------- record / replay const JournalBuffer = struct { bytes: [256 * 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 bridge's committed model (the committed /// slices live in the core's heap, which record and replay share — /// copy what outlives a session). const CoreSnapshot = struct { polling: bool, ticks: i64, lastTickAt: f64, stampMs: f64, failures: i64, saved: i64, code: f64, firedAt: f64, status: [32]u8, statusLen: usize, lastErr: [32]u8, lastErrLen: usize, fn take() CoreSnapshot { const m = Bridge.model(); var snapshot: CoreSnapshot = .{ .polling = m.polling, .ticks = m.ticks, .lastTickAt = m.lastTickAt, .stampMs = m.stampMs, .failures = m.failures, .saved = m.saved, .code = m.code, .firedAt = m.firedAt, .status = [_]u8{0} ** 32, .statusLen = @min(m.status.len, 32), .lastErr = [_]u8{0} ** 32, .lastErrLen = @min(m.lastErr.len, 32), }; @memcpy(snapshot.status[0..snapshot.statusLen], m.status[0..snapshot.statusLen]); @memcpy(snapshot.lastErr[0..snapshot.lastErrLen], m.lastErr[0..snapshot.lastErrLen]); return snapshot; } }; /// Record the reference session: boot answered ok, a refresh answered /// err, two timer fires, a synchronous stamp, a host_bytes send, a /// real-disk write/read round trip, a pasteboard write/read, and a /// one-shot delay fire. Real-executor waits poll WITHOUT dispatching /// events (Harness.waitPending), so two recordings stay byte-identical. fn recordSession(buffer: *JournalBuffer) !CoreSnapshot { 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 = "ts-core-e2e", .window_width = 400, .window_height = 300 }); HostStub.reset(); removeStore(); const h = try Harness.createRecorded(recorder); defer h.destroy(); const fx = &h.app_state.effects; try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); try fx.feedHostResult(status_request_key, true, "ready"); try h.wake(); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); try h.menu("core.refresh"); try fx.feedHostResult(status_request_key, false, "declined"); try h.wake(); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); _ = try h.fireTick(250_000_000); _ = try h.fireTick(350_000_000); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); try h.menu("core.stamp"); try h.menu("core.note"); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); // Named ops: write the model bytes to real disk, read them back, // share and paste through the pasteboard, then fire a delay. try h.menu("core.save"); try h.waitPending(); try h.wake(); try h.menu("core.load"); try h.waitPending(); try h.wake(); try h.menu("core.share"); try h.menu("core.paste"); try h.wake(); try h.menu("core.later"); _ = try h.fireDelay(450_000_000); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); recorder.finish(); try std.testing.expect(!recorder.failed); return CoreSnapshot.take(); } test "a recorded transpiled-core session replays byte-identically with no 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, 2), recorded.ticks); try std.testing.expectEqual(@as(f64, 50_000), recorded.stampMs); try std.testing.expectEqual(@as(i64, 1), recorded.failures); try std.testing.expectEqualStrings("ready", recorded.status[0..recorded.statusLen]); try std.testing.expectEqual(@as(i64, 1), recorded.saved); try std.testing.expectEqual(@as(f64, 450), recorded.firedAt); // 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: journaled `.host`/`.file`/`.clipboard` // results and the journaled clock feed the stub executor; the // platform timer events (subscription ticks AND the delay fire) // replay from the event log; the host binding is NEVER called. // Deleting the store first proves the replayed file ops touch no // disk — their results come from the journal alone. removeStore(); HostStub.reset(); const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = native_sdk.geometry.SizeF.init(400, 300), }); 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, .{}, e2eOptions()); defer app_state.deinit(); app_state.effects.bindHostCalls(HostStub.binding()); 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()); // Fed from the journal: the ok and err host answers, the clock // read, the file write and read terminals, and the clipboard read // (the fire-and-forget clipboard write routes to nobody, so it is // never journaled; timer fires ride the event log). Nothing // touched the stub host — and the deleted store proves nothing // touched the disk. try std.testing.expectEqual(@as(u64, 6), report.effects_fed); try std.testing.expectEqual(@as(usize, 0), HostStub.request_count); try std.testing.expectEqual(@as(usize, 0), HostStub.send_count); try std.testing.expectEqualDeep(recorded, CoreSnapshot.take()); } /// A value snapshot of the stream-facing model fields (see CoreSnapshot /// for the lifetime rule). const StreamSnapshot = struct { lines: i64, exitCode: i64, failures: i64, audioState: fixture.AudioState, posMs: i64, durMs: i64, playing: bool, audioEvents: i64, lastLine: [32]u8, lastLineLen: usize, lastErr: [32]u8, lastErrLen: usize, fn take() StreamSnapshot { const m = Bridge.model(); var snapshot: StreamSnapshot = .{ .lines = @intFromFloat(asF64(m.lines)), .exitCode = @intFromFloat(asF64(m.exitCode)), .failures = m.failures, .audioState = m.audioState, .posMs = @intFromFloat(asF64(m.posMs)), .durMs = @intFromFloat(asF64(m.durMs)), .playing = m.playing, .audioEvents = @intFromFloat(asF64(m.audioEvents)), .lastLine = [_]u8{0} ** 32, .lastLineLen = @min(m.lastLine.len, 32), .lastErr = [_]u8{0} ** 32, .lastErrLen = @min(m.lastErr.len, 32), }; @memcpy(snapshot.lastLine[0..snapshot.lastLineLen], m.lastLine[0..snapshot.lastLineLen]); @memcpy(snapshot.lastErr[0..snapshot.lastErrLen], m.lastErr[0..snapshot.lastErrLen]); return snapshot; } /// The emitted number fields class i64 or f64 per the subset's /// inference; normalize through f64 so the snapshot never chases /// the classing. fn asF64(value: anytype) f64 { return if (@TypeOf(value) == f64) value else @floatFromInt(value); } }; /// Record the stream session: a REAL subprocess whose lines and exit /// journal in order, a mid-stream cancel, and an audio stream driven by /// a scripted event feed — everything a streaming effect can journal. fn recordStreamSession(buffer: *JournalBuffer) !StreamSnapshot { 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 = "ts-core-e2e", .window_width = 400, .window_height = 300 }); HostStub.reset(); const h = try Harness.createRecorded(recorder); defer h.destroy(); const fx = &h.app_state.effects; try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); try fx.feedHostResult(status_request_key, true, "ready"); try h.wake(); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); // The real child: both lines and the exit drain in ONE wake (the // idle wait), so the journal's event boundaries are deterministic. try h.menu("core.run"); try h.waitIdle(); try h.wake(); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); // Cancel mid-stream: the journaled terminal is the `.cancelled` // exit the err arm consumed. try h.menu("core.hang"); try h.menu("core.kill"); try h.waitIdle(); try h.wake(); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); // The audio stream: a real playAudio against the null platform's // hermetic player, events scripted through the feed. try h.menu("core.play"); try fx.feedAudioEvent(.loaded, 0, 183_000, true); try fx.feedAudioEvent(.position, 1_500, 183_000, true); var bands: [32]u8 = undefined; for (&bands, 0..) |*b, i| b.* = @intCast(i * 7); try fx.feedAudioSpectrum(bands, 2_000, 183_000); try h.wake(); try h.menu("core.stopmusic"); try h.harness.runtime.dispatchPlatformEvent(h.app, .frame_requested); recorder.finish(); try std.testing.expect(!recorder.failed); return StreamSnapshot.take(); } test "a recorded stream session replays byte-identically with no process launches or host calls" { if (builtin.target.os.tag == .windows) return error.SkipZigTest; const buffer = try std.heap.page_allocator.create(JournalBuffer); defer std.heap.page_allocator.destroy(buffer); buffer.len = 0; const recorded = try recordStreamSession(buffer); try std.testing.expectEqual(@as(i64, 2), recorded.lines); try std.testing.expectEqualStrings("two", recorded.lastLine[0..recorded.lastLineLen]); try std.testing.expectEqual(@as(i64, 0), recorded.exitCode); try std.testing.expectEqual(@as(i64, 1), recorded.failures); try std.testing.expectEqualStrings("cancelled", recorded.lastErr[0..recorded.lastErrLen]); try std.testing.expectEqual(fixture.AudioState.spectrum, recorded.audioState); try std.testing.expectEqual(@as(i64, 3), recorded.audioEvents); // Replay into a fresh app: the spawn re-issues onto the FAKE // executor (no /bin/sh runs), the journaled lines, exits — the // cancelled one included — and audio events feed the parked // requests in recorded order, and the host binding is never // called. HostStub.reset(); const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{ .size = native_sdk.geometry.SizeF.init(400, 300), }); 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, .{}, e2eOptions()); defer app_state.deinit(); app_state.effects.bindHostCalls(HostStub.binding()); 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()); // Fed from the journal: the boot host answer (1), the run stream's // two lines and exit (3), the cancelled stream's exit (1), and the // three audio events (3). try std.testing.expectEqual(@as(u64, 8), report.effects_fed); try std.testing.expectEqual(@as(usize, 0), HostStub.request_count); try std.testing.expectEqual(@as(usize, 0), HostStub.send_count); try std.testing.expectEqualDeep(recorded, StreamSnapshot.take()); }