zeno-mobile-runner 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +1 -1
  4. package/build.zig +10 -0
  5. package/build.zig.zon +2 -2
  6. package/clients/kotlin/README.md +1 -1
  7. package/clients/kotlin/build.gradle.kts +1 -1
  8. package/clients/python/pyproject.toml +1 -1
  9. package/clients/rust/Cargo.lock +1 -1
  10. package/clients/rust/Cargo.toml +1 -1
  11. package/clients/typescript/package.json +1 -1
  12. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  13. package/docs/protocol.md +10 -10
  14. package/package.json +3 -1
  15. package/prebuilds/darwin-arm64/zmr +0 -0
  16. package/prebuilds/darwin-x64/zmr +0 -0
  17. package/prebuilds/linux-arm64/zmr +0 -0
  18. package/prebuilds/linux-x64/zmr +0 -0
  19. package/scripts/create-react-native-expo-demo-app.sh +11 -13
  20. package/shims/ios/ZMRShim.swift +40 -12
  21. package/shims/ios/ZMRShimUITestCase.swift +135 -15
  22. package/src/android.zig +10 -9
  23. package/src/android_emulator.zig +22 -11
  24. package/src/android_screen_recording.zig +11 -7
  25. package/src/bundle.zig +10 -9
  26. package/src/bundle_redaction.zig +29 -28
  27. package/src/bundle_tar.zig +15 -12
  28. package/src/cli_devices.zig +7 -3
  29. package/src/cli_discover.zig +7 -3
  30. package/src/cli_doctor.zig +7 -3
  31. package/src/cli_draft.zig +51 -47
  32. package/src/cli_explore.zig +7 -3
  33. package/src/cli_import.zig +8 -4
  34. package/src/cli_info.zig +13 -6
  35. package/src/cli_init.zig +9 -5
  36. package/src/cli_inspect.zig +8 -4
  37. package/src/cli_run.zig +22 -16
  38. package/src/cli_serve.zig +3 -3
  39. package/src/cli_trace.zig +25 -12
  40. package/src/cli_validate.zig +8 -4
  41. package/src/command.zig +81 -99
  42. package/src/config.zig +2 -1
  43. package/src/config_diagnostics.zig +2 -1
  44. package/src/config_paths.zig +2 -1
  45. package/src/doctor.zig +8 -7
  46. package/src/doctor_hints.zig +1 -1
  47. package/src/errors.zig +5 -5
  48. package/src/importer.zig +8 -7
  49. package/src/ios.zig +98 -19
  50. package/src/ios_devices.zig +6 -5
  51. package/src/ios_lifecycle.zig +4 -4
  52. package/src/ios_shim.zig +12 -0
  53. package/src/json_rpc.zig +39 -40
  54. package/src/json_rpc_methods.zig +8 -8
  55. package/src/json_rpc_observation.zig +9 -8
  56. package/src/json_rpc_params.zig +1 -1
  57. package/src/json_rpc_trace.zig +22 -21
  58. package/src/main.zig +19 -10
  59. package/src/mcp.zig +28 -19
  60. package/src/mcp_trace.zig +30 -29
  61. package/src/report.zig +39 -36
  62. package/src/report_html.zig +5 -4
  63. package/src/runner.zig +2 -1
  64. package/src/runner_actions.zig +20 -17
  65. package/src/runner_diagnostics.zig +4 -4
  66. package/src/runner_events.zig +55 -51
  67. package/src/runner_native.zig +21 -19
  68. package/src/runner_waits.zig +46 -41
  69. package/src/scaffold.zig +25 -24
  70. package/src/scenario.zig +4 -3
  71. package/src/stdio.zig +129 -0
  72. package/src/trace.zig +34 -26
  73. package/src/trace_summary.zig +3 -2
  74. package/src/trace_summary_diagnostic.zig +15 -13
  75. package/src/validation.zig +5 -4
  76. package/src/version.zig +1 -1
package/src/ios.zig CHANGED
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const command = @import("command.zig");
3
4
  const ios_devices = @import("ios_devices.zig");
4
5
  const ios_lifecycle = @import("ios_lifecycle.zig");
@@ -15,6 +16,8 @@ const default_max_output = 32 * 1024 * 1024;
15
16
  const default_shim_timeout_ms = 5_400_000;
16
17
  const shim_timeout_env = "ZMR_IOS_SHIM_TIMEOUT_MS";
17
18
  const shim_best_effort_timeout_ms = 10_000;
19
+ const open_link_interruption_attempts = 3;
20
+ const open_link_interruption_retry_delay_ms = 1_000;
18
21
  const shim_command_attempts = 2;
19
22
  const shim_bootstrap_retry_delay_ms = 500;
20
23
 
@@ -190,11 +193,11 @@ pub const IosDevice = struct {
190
193
  .duration_ms = @as(u32, @intCast(@min(timeout_ms, std.math.maxInt(u32)))),
191
194
  });
192
195
  }
193
- std.Thread.sleep(timeout_ms * std.time.ns_per_ms);
196
+ stdio.sleepNs(timeout_ms * std.time.ns_per_ms);
194
197
  }
195
198
 
196
199
  pub fn snapshot(self: *IosDevice, writer: ?*trace.TraceWriter) !types.ObservationSnapshot {
197
- const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{std.time.milliTimestamp()});
200
+ const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{stdio.nowMs()});
198
201
  errdefer self.allocator.free(id);
199
202
 
200
203
  var screenshot_artifact: ?[]const u8 = null;
@@ -239,7 +242,7 @@ pub const IosDevice = struct {
239
242
 
240
243
  return .{
241
244
  .id = id,
242
- .timestamp_ms = std.time.milliTimestamp(),
245
+ .timestamp_ms = stdio.nowMs(),
243
246
  .viewport = viewport,
244
247
  .active_package = active_package,
245
248
  .active_activity = null,
@@ -257,21 +260,21 @@ pub const IosDevice = struct {
257
260
  defer self.allocator.free(response);
258
261
  return try ios_shim.parseScreenshotPng(self.allocator, response);
259
262
  }
260
- const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{std.time.nanoTimestamp()});
263
+ const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{stdio.nowNs()});
261
264
  defer self.allocator.free(path);
262
- defer std.fs.cwd().deleteFile(path) catch {};
265
+ defer std.Io.Dir.cwd().deleteFile(stdio.io(), path) catch {};
263
266
 
264
267
  const result = try self.runSimctl(&.{ "io", self.target(), "screenshot", path }, default_max_output);
265
268
  defer result.deinit(self.allocator);
266
269
  try result.ensureSuccess();
267
- return try std.fs.cwd().readFileAlloc(self.allocator, path, default_max_output);
270
+ return try stdio.readFileAlloc(self.allocator, path, default_max_output);
268
271
  }
269
272
 
270
273
  fn logDelta(self: *IosDevice) !?[]const u8 {
271
274
  if (self.target_kind == .physical) return null;
272
275
  const result = try self.runSimctl(&.{ "spawn", self.target(), "log", "show", "--style", "compact", "--last", "30s" }, 1024 * 1024);
273
276
  defer result.deinit(self.allocator);
274
- if (result.term != .Exited or result.term.Exited != 0) return null;
277
+ if (result.term != .exited or result.term.exited != 0) return null;
275
278
  return try self.allocator.dupe(u8, result.stdout);
276
279
  }
277
280
 
@@ -282,15 +285,15 @@ pub const IosDevice = struct {
282
285
  }
283
286
 
284
287
  fn recordSnapshotSemanticFailure(self: *IosDevice, writer: *trace.TraceWriter, screenshot_artifact: []const u8, err: anyerror) !void {
285
- var payload = std.ArrayList(u8).empty;
286
- defer payload.deinit(writer.allocator);
287
- const out = payload.writer(writer.allocator);
288
+ var payload: std.Io.Writer.Allocating = .init(writer.allocator);
289
+ defer payload.deinit();
290
+ const out = &payload.writer;
288
291
  try out.writeAll("{\"status\":\"failed\",\"artifactStatus\":\"captured\",\"semanticStatus\":\"failed\",\"error\":");
289
292
  try trace.writeJsonString(out, @errorName(err));
290
293
  try out.writeAll(",\"screenshotArtifact\":");
291
294
  try trace.writeJsonString(out, screenshot_artifact);
292
295
  try out.writeAll(",\"source\":\"ios-xctest-shim\"}");
293
- try writer.recordEvent("observe.snapshot.semanticExtraction", payload.items);
296
+ try writer.recordEvent("observe.snapshot.semanticExtraction", out.buffered());
294
297
  _ = self;
295
298
  }
296
299
 
@@ -308,7 +311,20 @@ pub const IosDevice = struct {
308
311
 
309
312
  fn acceptOpenURLConfirmationBestEffort(self: *IosDevice) void {
310
313
  if (self.shim_path == null) return;
311
- self.runShimActionWithTimeout(.{ .kind = .accept_system_alert, .text = "Open" }, shim_best_effort_timeout_ms) catch {};
314
+ var attempt: usize = 0;
315
+ while (attempt < open_link_interruption_attempts) {
316
+ attempt += 1;
317
+ if (self.acceptOpenURLConfirmationOnce() catch return) return;
318
+ if (attempt < open_link_interruption_attempts) {
319
+ stdio.sleepNs(open_link_interruption_retry_delay_ms * std.time.ns_per_ms);
320
+ }
321
+ }
322
+ }
323
+
324
+ fn acceptOpenURLConfirmationOnce(self: *IosDevice) !bool {
325
+ const response = try self.runShimWithTimeout(.{ .kind = .accept_system_alert, .text = "Open" }, shim_best_effort_timeout_ms);
326
+ defer self.allocator.free(response);
327
+ return try ios_shim.parseAcceptSystemAlertResponse(response);
312
328
  }
313
329
 
314
330
  fn appIsRunningFromShimBestEffort(self: *IosDevice) bool {
@@ -340,19 +356,19 @@ pub const IosDevice = struct {
340
356
  fn runShimWithTimeout(self: *IosDevice, shim_command: ios_shim.Command, timeout_ms: u64) ![]u8 {
341
357
  const path = self.shim_path orelse return error.IosXCTestShimRequired;
342
358
 
343
- var input = std.ArrayList(u8).empty;
344
- defer input.deinit(self.allocator);
345
- try ios_shim.writeCommandJson(input.writer(self.allocator), shim_command);
359
+ var input: std.Io.Writer.Allocating = .init(self.allocator);
360
+ defer input.deinit();
361
+ try ios_shim.writeCommandJson(&input.writer, shim_command);
346
362
 
347
363
  var attempt: usize = 0;
348
364
  while (attempt < shim_command_attempts) {
349
365
  attempt += 1;
350
- const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.items, 4 * 1024 * 1024, timeout_ms);
366
+ const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.writer.buffered(), 4 * 1024 * 1024, timeout_ms);
351
367
  defer result.deinit(self.allocator);
352
368
 
353
369
  result.ensureSuccess() catch |err| {
354
370
  if (attempt < shim_command_attempts and err == error.CommandFailed and isTransientShimBootstrapFailure(result)) {
355
- std.Thread.sleep(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
371
+ stdio.sleepNs(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
356
372
  continue;
357
373
  }
358
374
  return err;
@@ -391,7 +407,7 @@ pub const IosDevice = struct {
391
407
  fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
392
408
  if (result.timed_out) return false;
393
409
  switch (result.term) {
394
- .Exited => |code| if (code == 0) return false,
410
+ .exited => |code| if (code == 0) return false,
395
411
  else => return false,
396
412
  }
397
413
  return std.mem.indexOf(u8, result.stderr, "iOS shim server exited before it became ready") != null or
@@ -400,7 +416,7 @@ fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
400
416
  }
401
417
 
402
418
  fn shimTimeoutMs() u64 {
403
- return parseShimTimeoutMs(std.posix.getenv(shim_timeout_env));
419
+ return parseShimTimeoutMs(stdio.getenv(shim_timeout_env));
404
420
  }
405
421
 
406
422
  fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
@@ -410,6 +426,69 @@ fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
410
426
  return parsed;
411
427
  }
412
428
 
429
+ test "ios simulator openLink keeps sweeping delayed XCTest interruptions until accepted" {
430
+ const allocator = std.heap.page_allocator;
431
+ const argv = [_][*:0]const u8{"zmr-ios-test"};
432
+ stdio.initProcess(.{
433
+ .args = .{ .vector = argv[0..] },
434
+ .environ = .empty,
435
+ }, allocator);
436
+ defer stdio.deinitProcess();
437
+
438
+ var tmp = std.testing.tmpDir(.{});
439
+ defer tmp.cleanup();
440
+
441
+ var shim = try tmp.dir.createFile(stdio.io(), "fake-ios-shim-delayed.sh", .{ .truncate = true });
442
+ {
443
+ var buffer: [4096]u8 = undefined;
444
+ var writer = shim.writerStreaming(stdio.io(), &buffer);
445
+ try writer.interface.writeAll(
446
+ \\#!/usr/bin/env bash
447
+ \\set -euo pipefail
448
+ \\request="$(cat)"
449
+ );
450
+ const shim_tail = try std.fmt.allocPrint(allocator,
451
+ \\
452
+ \\printf '%s\n' "$request" >> ".zig-cache/tmp/{s}/shim.log"
453
+ \\count_file=".zig-cache/tmp/{s}/count.txt"
454
+ \\count=0
455
+ \\if [[ -f "$count_file" ]]; then
456
+ \\ count="$(cat "$count_file")"
457
+ \\fi
458
+ \\count=$((count + 1))
459
+ \\printf '%s' "$count" > "$count_file"
460
+ \\if [[ "$count" -lt 3 ]]; then
461
+ \\ printf '{{"status":"ok","accepted":false,"count":0}}\n'
462
+ \\else
463
+ \\ printf '{{"status":"ok","accepted":true,"label":"Brick Rewards Test","count":1}}\n'
464
+ \\fi
465
+ \\
466
+ , .{ tmp.sub_path, tmp.sub_path });
467
+ defer allocator.free(shim_tail);
468
+ try writer.interface.writeAll(shim_tail);
469
+ try writer.interface.flush();
470
+ }
471
+ shim.close(stdio.io());
472
+
473
+ const shim_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/fake-ios-shim-delayed.sh", .{tmp.sub_path});
474
+ defer allocator.free(shim_path);
475
+ const shim_path_z = try allocator.dupeZ(u8, shim_path);
476
+ defer allocator.free(shim_path_z);
477
+ if (std.c.chmod(shim_path_z, 0o755) != 0) return error.ChmodFailed;
478
+
479
+ var device = try IosDevice.initWithShim(allocator, "./tests/fake-xcrun.sh", "fake-ios-1", "com.example.mobiletest", shim_path);
480
+ defer device.deinit();
481
+
482
+ try device.openLink("exampleapp:///e2e-auth?probe=1");
483
+
484
+ const count_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/count.txt", .{tmp.sub_path});
485
+ defer allocator.free(count_path);
486
+ const count_raw = try stdio.readFileAlloc(allocator, count_path, 1024);
487
+ defer allocator.free(count_raw);
488
+ const count = try std.fmt.parseInt(u8, count_raw, 10);
489
+ try std.testing.expectEqual(@as(u8, 3), count);
490
+ }
491
+
413
492
  pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
414
493
  return try ios_devices.listSimulators(allocator, xcrun_path);
415
494
  }
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const command = @import("command.zig");
3
4
  const types = @import("types.zig");
4
5
 
@@ -39,7 +40,7 @@ pub fn runSimctlCommand(
39
40
  }
40
41
  result.deinit(allocator);
41
42
  attempt += 1;
42
- std.Thread.sleep(simctl_retry_delay_ms * std.time.ns_per_ms);
43
+ stdio.sleepNs(simctl_retry_delay_ms * std.time.ns_per_ms);
43
44
  }
44
45
  }
45
46
 
@@ -62,9 +63,9 @@ pub fn runDevicectlJsonCommand(
62
63
  xcrun_path: []const u8,
63
64
  extra: []const []const u8,
64
65
  ) ![]u8 {
65
- const path = try std.fmt.allocPrint(allocator, "/tmp/zmr-devicectl-{d}.json", .{std.time.nanoTimestamp()});
66
+ const path = try std.fmt.allocPrint(allocator, "/tmp/zmr-devicectl-{d}.json", .{stdio.nowNs()});
66
67
  defer allocator.free(path);
67
- defer std.fs.cwd().deleteFile(path) catch {};
68
+ defer std.Io.Dir.deleteFileAbsolute(stdio.io(), path) catch {};
68
69
 
69
70
  var argv = std.ArrayList([]const u8).empty;
70
71
  defer argv.deinit(allocator);
@@ -76,7 +77,7 @@ pub fn runDevicectlJsonCommand(
76
77
  const result = try command.run(allocator, argv.items, default_max_output);
77
78
  defer result.deinit(allocator);
78
79
  try result.ensureSuccess();
79
- return try std.fs.cwd().readFileAlloc(allocator, path, default_max_output);
80
+ return try std.Io.Dir.cwd().readFileAlloc(stdio.io(), path, allocator, .limited(default_max_output));
80
81
  }
81
82
 
82
83
  pub fn parseSimulatorsJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
@@ -158,7 +159,7 @@ pub fn findPidForBundleId(allocator: std.mem.Allocator, content: []const u8, app
158
159
  fn isRetriableSimctlFailure(result: command.ExecResult) bool {
159
160
  if (result.timed_out) return false;
160
161
  switch (result.term) {
161
- .Exited => |code| if (code == 0) return false,
162
+ .exited => |code| if (code == 0) return false,
162
163
  else => return false,
163
164
  }
164
165
  return std.mem.indexOf(u8, result.stderr, "CoreSimulatorService connection became invalid") != null or
@@ -65,7 +65,7 @@ pub fn uninstallPhysicalBestEffort(
65
65
 
66
66
  pub fn isMissingInstalledApp(result: command.ExecResult) bool {
67
67
  switch (result.term) {
68
- .Exited => |code| if (code == 0) return false,
68
+ .exited => |code| if (code == 0) return false,
69
69
  else => return false,
70
70
  }
71
71
  return std.mem.indexOf(u8, result.stderr, "No installed application with bundle identifier") != null;
@@ -73,7 +73,7 @@ pub fn isMissingInstalledApp(result: command.ExecResult) bool {
73
73
 
74
74
  pub fn isAppNotRunning(result: command.ExecResult) bool {
75
75
  switch (result.term) {
76
- .Exited => |code| if (code == 0) return false,
76
+ .exited => |code| if (code == 0) return false,
77
77
  else => return false,
78
78
  }
79
79
  return std.mem.indexOf(u8, result.stderr, "found nothing to terminate") != null;
@@ -89,7 +89,7 @@ test "simctl terminate missing running app is best-effort" {
89
89
  try std.testing.expect(isAppNotRunning(.{
90
90
  .stdout = stdout,
91
91
  .stderr = stderr,
92
- .term = .{ .Exited = 3 },
92
+ .term = .{ .exited = 3 },
93
93
  }));
94
94
  }
95
95
 
@@ -103,6 +103,6 @@ test "simctl terminate success is not classified as already stopped" {
103
103
  try std.testing.expect(!isAppNotRunning(.{
104
104
  .stdout = stdout,
105
105
  .stderr = stderr,
106
- .term = .{ .Exited = 0 },
106
+ .term = .{ .exited = 0 },
107
107
  }));
108
108
  }
package/src/ios_shim.zig CHANGED
@@ -128,6 +128,18 @@ pub fn parseOkResponse(content: []const u8) !void {
128
128
  if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
129
129
  }
130
130
 
131
+ pub fn parseAcceptSystemAlertResponse(content: []const u8) !bool {
132
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
133
+ defer arena.deinit();
134
+ const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
135
+ if (parsed.value != .object) return error.IosShimResponseMustBeObject;
136
+ const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
137
+ if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
138
+ const accepted = parsed.value.object.get("accepted") orelse return false;
139
+ if (accepted != .bool) return false;
140
+ return accepted.bool;
141
+ }
142
+
131
143
  pub const SelectorActionResponse = enum {
132
144
  ok,
133
145
  selector_unavailable,
package/src/json_rpc.zig CHANGED
@@ -1,9 +1,12 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const errors = @import("errors.zig");
3
4
  const methods = @import("json_rpc_methods.zig");
4
5
  const protocol = @import("json_rpc_protocol.zig");
5
6
  const trace = @import("trace.zig");
6
7
 
8
+ const net = std.Io.net;
9
+
7
10
  pub const ServeOptions = struct {
8
11
  transport: []const u8 = "stdio",
9
12
  };
@@ -13,12 +16,19 @@ pub fn serveStdio(allocator: std.mem.Allocator, device: anytype) !void {
13
16
  }
14
17
 
15
18
  pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_trace: ?*trace.TraceWriter) !void {
16
- var stdin = std.fs.File.stdin().deprecatedReader();
17
- const stdout = std.fs.File.stdout().deprecatedWriter();
19
+ var stdin_io: stdio.Input = .{};
20
+ stdin_io.init(.stdin());
21
+ const stdin = stdin_io.reader();
22
+
23
+ var stdout_io: stdio.Output = .{};
24
+ stdout_io.init(.stdout());
25
+ defer stdout_io.deinit();
26
+ const stdout = stdout_io.writer();
18
27
 
19
28
  while (true) {
20
- const line = stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 16 * 1024 * 1024) catch |err| {
29
+ const line = stdio.readLineAlloc(stdin, allocator, 16 * 1024 * 1024) catch |err| {
21
30
  try protocol.writeError(stdout, null, -32700, @errorName(err));
31
+ try stdout_io.flush();
22
32
  continue;
23
33
  };
24
34
  const owned_line = line orelse break;
@@ -26,6 +36,7 @@ pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_t
26
36
  const trimmed = std.mem.trim(u8, owned_line, " \t\r\n");
27
37
  if (trimmed.len == 0) continue;
28
38
  try dispatchLineWithTrace(allocator, device, trimmed, stdout, live_trace);
39
+ try stdout_io.flush();
29
40
  }
30
41
  }
31
42
 
@@ -34,45 +45,33 @@ pub fn serveTcp(allocator: std.mem.Allocator, device: anytype, port: u16) !void
34
45
  }
35
46
 
36
47
  pub fn serveTcpWithTrace(allocator: std.mem.Allocator, device: anytype, port: u16, live_trace: ?*trace.TraceWriter) !void {
37
- const address = try std.net.Address.parseIp("127.0.0.1", port);
38
- var server = try address.listen(.{ .reuse_address = true });
39
- defer server.deinit();
48
+ const address = try net.IpAddress.parse("127.0.0.1", port);
49
+ var server = try address.listen(stdio.io(), .{ .reuse_address = true });
50
+ defer server.deinit(stdio.io());
40
51
 
41
52
  while (true) {
42
- var connection = try server.accept();
43
- defer connection.stream.close();
44
- try serveTcpConnection(allocator, device, connection.stream, live_trace);
53
+ const connection = try server.accept(stdio.io());
54
+ defer connection.close(stdio.io());
55
+ try serveTcpConnection(allocator, device, connection, live_trace);
45
56
  }
46
57
  }
47
58
 
48
- fn serveTcpConnection(allocator: std.mem.Allocator, device: anytype, stream: std.net.Stream, live_trace: ?*trace.TraceWriter) !void {
59
+ fn serveTcpConnection(allocator: std.mem.Allocator, device: anytype, stream: net.Stream, live_trace: ?*trace.TraceWriter) !void {
49
60
  var write_buffer: [8192]u8 = undefined;
50
- var stream_writer = stream.writer(&write_buffer);
61
+ var stream_writer = stream.writer(stdio.io(), &write_buffer);
51
62
  const writer = &stream_writer.interface;
52
63
 
53
- var line = std.ArrayList(u8).empty;
54
- defer line.deinit(allocator);
55
-
56
64
  var read_buffer: [4096]u8 = undefined;
65
+ var stream_reader = stream.reader(stdio.io(), &read_buffer);
57
66
  while (true) {
58
- const n = try stream.read(&read_buffer);
59
- if (n == 0) break;
60
- for (read_buffer[0..n]) |ch| {
61
- if (ch == '\n') {
62
- const trimmed = std.mem.trim(u8, line.items, " \t\r\n");
63
- if (trimmed.len != 0) {
64
- try dispatchLineWithTrace(allocator, device, trimmed, writer, live_trace);
65
- try writer.flush();
66
- }
67
- line.clearRetainingCapacity();
68
- } else {
69
- try line.append(allocator, ch);
70
- }
71
- }
72
- }
73
-
74
- if (line.items.len != 0) {
75
- const trimmed = std.mem.trim(u8, line.items, " \t\r\n");
67
+ const line = stdio.readLineAlloc(&stream_reader.interface, allocator, 16 * 1024 * 1024) catch |err| {
68
+ try protocol.writeError(writer, null, -32700, @errorName(err));
69
+ try writer.flush();
70
+ continue;
71
+ };
72
+ const owned_line = line orelse break;
73
+ defer allocator.free(owned_line);
74
+ const trimmed = std.mem.trim(u8, owned_line, " \t\r\n");
76
75
  if (trimmed.len != 0) {
77
76
  try dispatchLineWithTrace(allocator, device, trimmed, writer, live_trace);
78
77
  try writer.flush();
@@ -124,21 +123,21 @@ pub fn dispatchLineWithTrace(
124
123
  }
125
124
 
126
125
  fn recordRpcEvent(tw: *trace.TraceWriter, kind: []const u8, method: []const u8, id: ?std.json.Value) !void {
127
- var payload = std.ArrayList(u8).empty;
128
- defer payload.deinit(tw.allocator);
129
- const writer = payload.writer(tw.allocator);
126
+ var payload: std.Io.Writer.Allocating = .init(tw.allocator);
127
+ defer payload.deinit();
128
+ const writer = &payload.writer;
130
129
  try writer.writeAll("{\"method\":");
131
130
  try trace.writeJsonString(writer, method);
132
131
  try writer.writeAll(",\"id\":");
133
132
  try protocol.writeId(writer, id);
134
133
  try writer.writeAll("}");
135
- try tw.recordEvent(kind, payload.items);
134
+ try tw.recordEvent(kind, writer.buffered());
136
135
  }
137
136
 
138
137
  fn recordRpcErrorEvent(tw: *trace.TraceWriter, method: []const u8, id: ?std.json.Value, err: anyerror) !void {
139
- var payload = std.ArrayList(u8).empty;
140
- defer payload.deinit(tw.allocator);
141
- const writer = payload.writer(tw.allocator);
138
+ var payload: std.Io.Writer.Allocating = .init(tw.allocator);
139
+ defer payload.deinit();
140
+ const writer = &payload.writer;
142
141
  try writer.writeAll("{\"method\":");
143
142
  try trace.writeJsonString(writer, method);
144
143
  try writer.writeAll(",\"id\":");
@@ -146,5 +145,5 @@ fn recordRpcErrorEvent(tw: *trace.TraceWriter, method: []const u8, id: ?std.json
146
145
  try writer.writeAll(",\"error\":");
147
146
  try trace.writeJsonString(writer, @errorName(err));
148
147
  try writer.writeAll("}");
149
- try tw.recordEvent("rpc.error", payload.items);
148
+ try tw.recordEvent("rpc.error", writer.buffered());
150
149
  }
@@ -107,13 +107,13 @@ fn dispatchAppMethod(
107
107
  }
108
108
 
109
109
  fn recordAppOpenLink(tw: *trace.TraceWriter, url: []const u8) !void {
110
- var payload = std.ArrayList(u8).empty;
111
- defer payload.deinit(tw.allocator);
112
- const writer = payload.writer(tw.allocator);
110
+ var payload: std.Io.Writer.Allocating = .init(tw.allocator);
111
+ defer payload.deinit();
112
+ const writer = &payload.writer;
113
113
  try writer.writeAll("{\"status\":\"ok\",\"url\":");
114
114
  try trace.writeJsonString(writer, url);
115
115
  try writer.writeAll("}");
116
- try tw.recordEvent("app.openLink", payload.items);
116
+ try tw.recordEvent("app.openLink", writer.buffered());
117
117
  }
118
118
 
119
119
  fn dispatchObserveMethod(
@@ -312,10 +312,10 @@ fn dispatchScenarioMethod(
312
312
  const path = try params_parser.requiredString(params, "path");
313
313
  var result = try validation.validateFile(allocator, path);
314
314
  defer result.deinit(allocator);
315
- var payload = std.ArrayList(u8).empty;
316
- defer payload.deinit(allocator);
317
- try cli_output.writeValidationJson(payload.writer(allocator), path, result);
318
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
315
+ var payload: std.Io.Writer.Allocating = .init(allocator);
316
+ defer payload.deinit();
317
+ try cli_output.writeValidationJson(&payload.writer, path, result);
318
+ try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
319
319
  return true;
320
320
  }
321
321
  return false;
@@ -20,12 +20,13 @@ pub fn writeResult(writer: anytype, id: ?std.json.Value, snap: types.Observation
20
20
  pub fn recordArtifact(tw: *trace.TraceWriter, kind: []const u8, snap: types.ObservationSnapshot) !void {
21
21
  const path = try tw.writeSnapshot(snap);
22
22
  defer tw.allocator.free(path);
23
- var payload = std.ArrayList(u8).empty;
24
- defer payload.deinit(tw.allocator);
25
- try payload.writer(tw.allocator).writeAll("{\"path\":");
26
- try trace.writeJsonString(payload.writer(tw.allocator), path);
27
- try payload.writer(tw.allocator).writeAll(",\"snapshotId\":");
28
- try trace.writeJsonString(payload.writer(tw.allocator), snap.id);
29
- try payload.writer(tw.allocator).writeAll("}");
30
- try tw.recordEvent(kind, payload.items);
23
+ var payload: std.Io.Writer.Allocating = .init(tw.allocator);
24
+ defer payload.deinit();
25
+ const out = &payload.writer;
26
+ try out.writeAll("{\"path\":");
27
+ try trace.writeJsonString(out, path);
28
+ try out.writeAll(",\"snapshotId\":");
29
+ try trace.writeJsonString(out, snap.id);
30
+ try out.writeAll("}");
31
+ try tw.recordEvent(kind, out.buffered());
31
32
  }
@@ -56,5 +56,5 @@ pub fn optionalDirection(params: ?std.json.Value, key: []const u8, default_value
56
56
  if (value != .string) return error.ParamMustBeString;
57
57
  if (std.mem.eql(u8, value.string, "down")) return .down;
58
58
  if (std.mem.eql(u8, value.string, "up")) return .up;
59
- return error.UnknownScrollDirection;
59
+ return error.unknownScrollDirection;
60
60
  }
@@ -4,6 +4,7 @@ const cli_explore = @import("cli_explore.zig");
4
4
  const protocol = @import("json_rpc_protocol.zig");
5
5
  const report = @import("report.zig");
6
6
  const runner_events = @import("runner_events.zig");
7
+ const stdio = @import("stdio.zig");
7
8
  const trace = @import("trace.zig");
8
9
 
9
10
  pub fn writeEventsResult(
@@ -23,7 +24,7 @@ pub fn writeEventsResult(
23
24
 
24
25
  const events_path = try std.fs.path.join(allocator, &.{ tw.root_dir, "events.jsonl" });
25
26
  defer allocator.free(events_path);
26
- const content = std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
27
+ const content = stdio.readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
27
28
  error.FileNotFound => try allocator.dupe(u8, ""),
28
29
  else => return err,
29
30
  };
@@ -35,9 +36,9 @@ pub fn writeEventsResult(
35
36
  try trace.writeJsonString(writer, tw.root_dir);
36
37
  try writer.print(",\"afterSeq\":{d},\"nextSeq\":", .{after_seq});
37
38
 
38
- var events_json = std.ArrayList(u8).empty;
39
- defer events_json.deinit(allocator);
40
- var events_writer = events_json.writer(allocator);
39
+ var events_json: std.Io.Writer.Allocating = .init(allocator);
40
+ defer events_json.deinit();
41
+ const events_writer = &events_json.writer;
41
42
  var next_seq = after_seq;
42
43
  var emitted: u64 = 0;
43
44
 
@@ -60,20 +61,20 @@ pub fn writeEventsResult(
60
61
  }
61
62
 
62
63
  try writer.print("{d},\"latestSeq\":{d},\"events\":[", .{ next_seq, tw.event_count });
63
- try writer.writeAll(events_json.items);
64
+ try writer.writeAll(events_writer.buffered());
64
65
  try writer.writeAll("]}}\n");
65
66
  }
66
67
 
67
68
  pub fn recordSimplePayload(tw: *trace.TraceWriter, kind: []const u8, key: []const u8, value: []const u8) !void {
68
- var payload = std.ArrayList(u8).empty;
69
- defer payload.deinit(tw.allocator);
70
- const writer = payload.writer(tw.allocator);
69
+ var payload: std.Io.Writer.Allocating = .init(tw.allocator);
70
+ defer payload.deinit();
71
+ const writer = &payload.writer;
71
72
  try writer.writeAll("{");
72
73
  try trace.writeJsonString(writer, key);
73
74
  try writer.writeAll(":");
74
75
  try trace.writeJsonString(writer, value);
75
76
  try writer.writeAll("}");
76
- try tw.recordEvent(kind, payload.items);
77
+ try tw.recordEvent(kind, writer.buffered());
77
78
  }
78
79
 
79
80
  pub fn writeDiscoverResult(
@@ -113,10 +114,10 @@ pub fn writeDiscoverResult(
113
114
  discovered.summary.validated,
114
115
  );
115
116
 
116
- var payload = std.ArrayList(u8).empty;
117
- defer payload.deinit(allocator);
118
- try cli_discover.writeJson(payload.writer(allocator), discovered.summary, discovered.validation);
119
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
117
+ var payload: std.Io.Writer.Allocating = .init(allocator);
118
+ defer payload.deinit();
119
+ try cli_discover.writeJson(&payload.writer, discovered.summary, discovered.validation);
120
+ try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
120
121
  }
121
122
 
122
123
  pub fn writeExploreResult(
@@ -159,10 +160,10 @@ pub fn writeExploreResult(
159
160
  explored.discovered.summary.validated,
160
161
  );
161
162
 
162
- var payload = std.ArrayList(u8).empty;
163
- defer payload.deinit(allocator);
164
- try cli_explore.writeJson(payload.writer(allocator), explored.summary, explored.discovered.summary, explored.discovered.validation);
165
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
163
+ var payload: std.Io.Writer.Allocating = .init(allocator);
164
+ defer payload.deinit();
165
+ try cli_explore.writeJson(&payload.writer, explored.summary, explored.discovered.summary, explored.discovered.validation);
166
+ try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
166
167
  }
167
168
 
168
169
  pub fn writeExplainResult(
@@ -177,9 +178,9 @@ pub fn writeExplainResult(
177
178
  };
178
179
 
179
180
  try tw.flushManifest();
180
- var payload = std.ArrayList(u8).empty;
181
- defer payload.deinit(allocator);
182
- try report.writeTraceExplanationJson(allocator, tw.root_dir, payload.writer(allocator));
183
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
181
+ var payload: std.Io.Writer.Allocating = .init(allocator);
182
+ defer payload.deinit();
183
+ try report.writeTraceExplanationJson(allocator, tw.root_dir, &payload.writer);
184
+ try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
184
185
  try tw.recordEvent("trace.explain", "{\"status\":\"ok\"}");
185
186
  }