Files
2026-07-13 12:29:49 +08:00

966 lines
40 KiB
Zig

//! 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());
}