From b9a72687f47e79bdd336bc8a6c6a06a75a94f2ad Mon Sep 17 00:00:00 2001 From: Vernon Stinebaker Date: Tue, 5 May 2026 14:49:03 +0800 Subject: [PATCH] test(fixtures): add reusable temp paths helper --- src/installer/orchestrator.zig | 19 +++++------ src/supervisor/manager.zig | 28 ++++++---------- src/supervisor/runtime_state.zig | 17 ++++------ src/test_helpers.zig | 56 ++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 39 deletions(-) create mode 100644 src/test_helpers.zig diff --git a/src/installer/orchestrator.zig b/src/installer/orchestrator.zig index ac1b190..1060f4f 100644 --- a/src/installer/orchestrator.zig +++ b/src/installer/orchestrator.zig @@ -15,6 +15,7 @@ const nullclaw_web_channel = @import("../core/nullclaw_web_channel.zig"); const manager_mod = @import("../supervisor/manager.zig"); const ui_modules_mod = @import("ui_modules.zig"); const managed_skills = @import("../managed_skills.zig"); +const test_helpers = @import("../test_helpers.zig"); const MAX_CONFIG_BYTES = 4 * 1024 * 1024; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -898,32 +899,28 @@ test "writeFile creates file with correct content" { test "directory creation succeeds in temp directory" { const allocator = std.testing.allocator; - const tmp_root = "/tmp/test-orchestrator-dirs"; - std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; - defer std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; - - var p = try paths_mod.Paths.init(allocator, tmp_root); - defer p.deinit(allocator); + var fixture = try test_helpers.TempPaths.init(allocator); + defer fixture.deinit(); // Create top-level dirs - try p.ensureDirs(); + try fixture.paths.ensureDirs(); // Create component dir - const comp_dir = try std.fs.path.join(allocator, &.{ p.root, "instances", "testcomp" }); + const comp_dir = try std.fs.path.join(allocator, &.{ fixture.paths.root, "instances", "testcomp" }); defer allocator.free(comp_dir); try std_compat.fs.makeDirAbsolute(comp_dir); // Create instance dir - const inst_dir = try p.instanceDir(allocator, "testcomp", "myinst"); + const inst_dir = try fixture.paths.instanceDir(allocator, "testcomp", "myinst"); defer allocator.free(inst_dir); try std_compat.fs.makeDirAbsolute(inst_dir); // Create data and logs subdirs - const data_dir = try p.instanceData(allocator, "testcomp", "myinst"); + const data_dir = try fixture.paths.instanceData(allocator, "testcomp", "myinst"); defer allocator.free(data_dir); try std_compat.fs.makeDirAbsolute(data_dir); - const logs_dir = try p.instanceLogs(allocator, "testcomp", "myinst"); + const logs_dir = try fixture.paths.instanceLogs(allocator, "testcomp", "myinst"); defer allocator.free(logs_dir); try std_compat.fs.makeDirAbsolute(logs_dir); diff --git a/src/supervisor/manager.zig b/src/supervisor/manager.zig index 0e65947..23421b1 100644 --- a/src/supervisor/manager.zig +++ b/src/supervisor/manager.zig @@ -5,6 +5,7 @@ const health = @import("health.zig"); const runtime_state = @import("runtime_state.zig"); const paths_mod = @import("../core/paths.zig"); const component_cli = @import("../core/component_cli.zig"); +const test_helpers = @import("../test_helpers.zig"); pub const Status = enum { stopped, @@ -866,20 +867,16 @@ test "getStatus returns null for unknown instance" { test "logSupervisor appends diagnostics to nullhub.log" { const allocator = std.testing.allocator; - const tmp_root = "/tmp/test-nullhub-mgr-log-supervisor"; - std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; - defer std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; + var fixture = try test_helpers.TempPaths.init(allocator); + defer fixture.deinit(); - var p = try paths_mod.Paths.init(allocator, tmp_root); - defer p.deinit(allocator); - - var mgr = Manager.init(allocator, p); + var mgr = Manager.init(allocator, fixture.paths); defer mgr.deinit(); mgr.logSupervisor("nullclaw", "diag", "first diagnostic {d}", .{@as(u8, 1)}); mgr.logSupervisor("nullclaw", "diag", "second diagnostic", .{}); - const logs_dir = try p.instanceLogs(allocator, "nullclaw", "diag"); + const logs_dir = try fixture.paths.instanceLogs(allocator, "nullclaw", "diag"); defer allocator.free(logs_dir); const log_path = try std.fs.path.join(allocator, &.{ logs_dir, "nullhub.log" }); defer allocator.free(log_path); @@ -935,14 +932,12 @@ test "restart preserves launch args with spaces" { if (comptime builtin.os.tag == .windows) return error.SkipZigTest; const allocator = std.testing.allocator; - const tmp_root = "/tmp/test-nullhub-mgr-restart-argv"; - std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; - defer std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; - try std_compat.fs.makeDirAbsolute(tmp_root); + var fixture = try test_helpers.TempPaths.init(allocator); + defer fixture.deinit(); - const script_path = try std.fs.path.join(allocator, &.{ tmp_root, "capture-arg.sh" }); + const script_path = try fixture.path(allocator, "capture-arg.sh"); defer allocator.free(script_path); - const output_path = try std.fs.path.join(allocator, &.{ tmp_root, "captured.txt" }); + const output_path = try fixture.path(allocator, "captured.txt"); defer allocator.free(output_path); const script = @@ -954,10 +949,7 @@ test "restart preserves launch args with spaces" { defer script_file.close(); try script_file.writeAll(script); - var p = try paths_mod.Paths.init(allocator, tmp_root); - defer p.deinit(allocator); - - var mgr = Manager.init(allocator, p); + var mgr = Manager.init(allocator, fixture.paths); defer mgr.deinit(); const launch_args = [_][]const u8{ script_path, "hello world", output_path }; diff --git a/src/supervisor/runtime_state.zig b/src/supervisor/runtime_state.zig index a5cfce4..ed9a8cc 100644 --- a/src/supervisor/runtime_state.zig +++ b/src/supervisor/runtime_state.zig @@ -2,6 +2,7 @@ const std = @import("std"); const std_compat = @import("compat"); const fs_compat = @import("../fs_compat.zig"); const paths_mod = @import("../core/paths.zig"); +const test_helpers = @import("../test_helpers.zig"); pub const PersistedRuntimeView = struct { pid: u64, @@ -127,14 +128,10 @@ pub fn delete( test "runtime state round-trips through instance.json" { const allocator = std.testing.allocator; - const tmp_root = "/tmp/nullhub-test-runtime-state"; - std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; - defer std_compat.fs.deleteTreeAbsolute(tmp_root) catch {}; + var fixture = try test_helpers.TempPaths.init(allocator); + defer fixture.deinit(); - var paths = try paths_mod.Paths.init(allocator, tmp_root); - defer paths.deinit(allocator); - - try write(allocator, paths, "nullclaw", "demo", .{ + try write(allocator, fixture.paths, "nullclaw", "demo", .{ .pid = 42, .port = 8080, .health_endpoint = "/health", @@ -147,7 +144,7 @@ test "runtime state round-trips through instance.json" { .starting_since = 1000, }); - var loaded = (try load(allocator, paths, "nullclaw", "demo")).?; + var loaded = (try load(allocator, fixture.paths, "nullclaw", "demo")).?; defer loaded.deinit(allocator); try std.testing.expectEqual(@as(u64, 42), loaded.pid); @@ -158,6 +155,6 @@ test "runtime state round-trips through instance.json" { try std.testing.expectEqual(@as(usize, 2), loaded.launch_args.len); try std.testing.expectEqualStrings("--verbose", loaded.launch_args[1]); - delete(allocator, paths, "nullclaw", "demo"); - try std.testing.expect((try load(allocator, paths, "nullclaw", "demo")) == null); + delete(allocator, fixture.paths, "nullclaw", "demo"); + try std.testing.expect((try load(allocator, fixture.paths, "nullclaw", "demo")) == null); } diff --git a/src/test_helpers.zig b/src/test_helpers.zig new file mode 100644 index 0000000..16a4cb4 --- /dev/null +++ b/src/test_helpers.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const std_compat = @import("compat"); +const paths_mod = @import("core/paths.zig"); + +pub const TempPaths = struct { + allocator: std.mem.Allocator, + tmp: std.testing.TmpDir, + root: []u8, + paths: paths_mod.Paths, + + pub fn init(allocator: std.mem.Allocator) !TempPaths { + const tmp = std.testing.tmpDir(.{}); + const root = try std_compat.fs.Dir.wrap(tmp.dir).realpathAlloc(allocator, "."); + errdefer allocator.free(root); + + const paths = try paths_mod.Paths.init(allocator, root); + errdefer { + var owned_paths = paths; + owned_paths.deinit(allocator); + } + + return .{ + .allocator = allocator, + .tmp = tmp, + .root = root, + .paths = paths, + }; + } + + pub fn deinit(self: *TempPaths) void { + self.paths.deinit(self.allocator); + self.allocator.free(self.root); + self.tmp.cleanup(); + self.* = undefined; + } + + pub fn path(self: TempPaths, allocator: std.mem.Allocator, sub_path: []const u8) ![]const u8 { + return std.fs.path.join(allocator, &.{ self.root, sub_path }); + } +}; + +test "TempPaths creates isolated nullhub root" { + const allocator = std.testing.allocator; + + var fixture = try TempPaths.init(allocator); + defer fixture.deinit(); + + try std.testing.expect(std.fs.path.isAbsolute(fixture.root)); + try std.testing.expectEqualStrings(fixture.root, fixture.paths.root); + + try fixture.paths.ensureDirs(); + + const state_path = try fixture.path(allocator, "state.json"); + defer allocator.free(state_path); + try std.testing.expect(std.mem.startsWith(u8, state_path, fixture.root)); +}