From efde37eb36b748bfc9d2167852e0c4759d8c9049 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 10 Dec 2025 01:19:46 +0000 Subject: [PATCH] test: add gateway/runtime utility coverage --- .../GatewayEnvironmentTests.swift | 30 +++++++ .../ClawdisIPCTests/RuntimeLocatorTests.swift | 71 ++++++++++++++++ .../ClawdisIPCTests/UtilitiesTests.swift | 80 +++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 apps/macos/Tests/ClawdisIPCTests/GatewayEnvironmentTests.swift create mode 100644 apps/macos/Tests/ClawdisIPCTests/RuntimeLocatorTests.swift create mode 100644 apps/macos/Tests/ClawdisIPCTests/UtilitiesTests.swift diff --git a/apps/macos/Tests/ClawdisIPCTests/GatewayEnvironmentTests.swift b/apps/macos/Tests/ClawdisIPCTests/GatewayEnvironmentTests.swift new file mode 100644 index 000000000..684bf235d --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/GatewayEnvironmentTests.swift @@ -0,0 +1,30 @@ +import Foundation +import Testing +@testable import Clawdis + +@Suite struct GatewayEnvironmentTests { + @Test func semverParsesCommonForms() { + #expect(Semver.parse("1.2.3") == Semver(major: 1, minor: 2, patch: 3)) + #expect(Semver.parse("v2.0.0") == Semver(major: 2, minor: 0, patch: 0)) + #expect(Semver.parse("3.4.5-beta.1") == Semver(major: 3, minor: 4, patch: 0)) // patch drops trailing text + #expect(Semver.parse(nil) == nil) + #expect(Semver.parse("invalid") == nil) + } + + @Test func semverCompatibilityRequiresSameMajorAndNotOlder() { + let required = Semver(major: 2, minor: 1, patch: 0) + #expect(Semver(major: 2, minor: 1, patch: 0).compatible(with: required)) + #expect(Semver(major: 2, minor: 2, patch: 0).compatible(with: required)) + #expect(Semver(major: 3, minor: 0, patch: 0).compatible(with: required) == false) + #expect(Semver(major: 1, minor: 9, patch: 9).compatible(with: required) == false) + } + + @Test func gatewayPortDefaultsAndRespectsOverride() { + let defaultPort = GatewayEnvironment.gatewayPort() + #expect(defaultPort == 18789) + + UserDefaults.standard.set(19999, forKey: "gatewayPort") + defer { UserDefaults.standard.removeObject(forKey: "gatewayPort") } + #expect(GatewayEnvironment.gatewayPort() == 19999) + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/RuntimeLocatorTests.swift b/apps/macos/Tests/ClawdisIPCTests/RuntimeLocatorTests.swift new file mode 100644 index 000000000..8717f06a2 --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/RuntimeLocatorTests.swift @@ -0,0 +1,71 @@ +import Foundation +import Testing +@testable import Clawdis + +@Suite struct RuntimeLocatorTests { + private func makeTempExecutable(contents: String) throws -> URL { + let dir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + let path = dir.appendingPathComponent("node") + try contents.write(to: path, atomically: true, encoding: .utf8) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) + return path + } + + @Test func resolveSucceedsWithValidNode() throws { + let script = """ + #!/bin/sh + echo v22.5.0 + """ + let node = try self.makeTempExecutable(contents: script) + let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) + guard case let .success(res) = result else { + Issue.record("Expected success, got \(result)") + return + } + #expect(res.path == node.path) + #expect(res.version == RuntimeVersion(major: 22, minor: 5, patch: 0)) + } + + @Test func resolveFailsWhenTooOld() throws { + let script = """ + #!/bin/sh + echo v18.2.0 + """ + let node = try self.makeTempExecutable(contents: script) + let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) + guard case let .failure(.unsupported(_, found, _, path, _)) = result else { + Issue.record("Expected unsupported error, got \(result)") + return + } + #expect(found == RuntimeVersion(major: 18, minor: 2, patch: 0)) + #expect(path == node.path) + } + + @Test func resolveFailsWhenVersionUnparsable() throws { + let script = """ + #!/bin/sh + echo node-version:unknown + """ + let node = try self.makeTempExecutable(contents: script) + let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) + guard case let .failure(.versionParse(_, raw, path, _)) = result else { + Issue.record("Expected versionParse error, got \(result)") + return + } + #expect(raw.contains("unknown")) + #expect(path == node.path) + } + + @Test func describeFailureIncludesPaths() { + let msg = RuntimeLocator.describeFailure(.notFound(searchPaths: ["/tmp/a", "/tmp/b"])) + #expect(msg.contains("PATH searched: /tmp/a:/tmp/b")) + } + + @Test func runtimeVersionParsesWithLeadingVAndMetadata() { + #expect(RuntimeVersion.from(string: "v22.1.3") == RuntimeVersion(major: 22, minor: 1, patch: 3)) + #expect(RuntimeVersion.from(string: "node 22.3.0-alpha.1") == RuntimeVersion(major: 22, minor: 3, patch: 0)) + #expect(RuntimeVersion.from(string: "bogus") == nil) + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/UtilitiesTests.swift b/apps/macos/Tests/ClawdisIPCTests/UtilitiesTests.swift new file mode 100644 index 000000000..4cdb7bcfc --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/UtilitiesTests.swift @@ -0,0 +1,80 @@ +import Foundation +import Testing +@testable import Clawdis + +@Suite struct UtilitiesTests { + @Test func ageStringsCoverCommonWindows() { + let now = Date(timeIntervalSince1970: 1_000_000) + #expect(age(from: now, now: now) == "just now") + #expect(age(from: now.addingTimeInterval(-45), now: now) == "just now") + #expect(age(from: now.addingTimeInterval(-75), now: now) == "1 minute ago") + #expect(age(from: now.addingTimeInterval(-10 * 60), now: now) == "10m ago") + #expect(age(from: now.addingTimeInterval(-3_600), now: now) == "1 hour ago") + #expect(age(from: now.addingTimeInterval(-5 * 3_600), now: now) == "5h ago") + #expect(age(from: now.addingTimeInterval(-26 * 3_600), now: now) == "yesterday") + #expect(age(from: now.addingTimeInterval(-3 * 86_400), now: now) == "3d ago") + } + + @Test func parseSSHTargetSupportsUserPortAndDefaults() { + let parsed1 = CommandResolver.parseSSHTarget("alice@example.com:2222") + #expect(parsed1?.user == "alice") + #expect(parsed1?.host == "example.com") + #expect(parsed1?.port == 2222) + + let parsed2 = CommandResolver.parseSSHTarget("example.com") + #expect(parsed2?.user == nil) + #expect(parsed2?.host == "example.com") + #expect(parsed2?.port == 22) + + let parsed3 = CommandResolver.parseSSHTarget("bob@host") + #expect(parsed3?.user == "bob") + #expect(parsed3?.host == "host") + #expect(parsed3?.port == 22) + } + + @Test func sanitizedTargetStripsLeadingSSHPrefix() { + UserDefaults.standard.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey) + UserDefaults.standard.set("ssh alice@example.com", forKey: remoteTargetKey) + defer { + UserDefaults.standard.removeObject(forKey: connectionModeKey) + UserDefaults.standard.removeObject(forKey: remoteTargetKey) + } + + let settings = CommandResolver.connectionSettings() + #expect(settings.mode == .remote) + #expect(settings.target == "alice@example.com") + } + + @Test func gatewayEntrypointPrefersDistOverBin() throws { + let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let dist = tmp.appendingPathComponent("dist/index.js") + let bin = tmp.appendingPathComponent("bin/clawdis.js") + try FileManager.default.createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true) + FileManager.default.createFile(atPath: dist.path, contents: Data()) + FileManager.default.createFile(atPath: bin.path, contents: Data()) + + let entry = CommandResolver.gatewayEntrypoint(in: tmp) + #expect(entry == dist.path) + } + + @Test func logLocatorPicksNewestLogFile() throws { + let fm = FileManager.default + let dir = URL(fileURLWithPath: "/tmp/clawdis", isDirectory: true) + try? fm.createDirectory(at: dir, withIntermediateDirectories: true) + + let older = dir.appendingPathComponent("clawdis-old-\(UUID().uuidString).log") + let newer = dir.appendingPathComponent("clawdis-new-\(UUID().uuidString).log") + fm.createFile(atPath: older.path, contents: Data("old".utf8)) + fm.createFile(atPath: newer.path, contents: Data("new".utf8)) + try fm.setAttributes([.modificationDate: Date(timeIntervalSinceNow: -100)], ofItemAtPath: older.path) + try fm.setAttributes([.modificationDate: Date()], ofItemAtPath: newer.path) + + let best = LogLocator.bestLogFile() + #expect(best?.lastPathComponent == newer.lastPathComponent) + + try? fm.removeItem(at: older) + try? fm.removeItem(at: newer) + } +}