SPB Git

spb/zyquo-agent Public MIT

The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.

Swift 94.7% Shell 4.1% Python 0.7% Makefile 0.5%
5.4 KB · 121 lines swift
Raw Blame History
1//2//  UISmoke.swift3//  Zyquo Agent4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Hidden `--run-ui-smoke` mode: drives the RunController (the UI's view9//  model) with the scripted MockProviderClient — no network, no keys, no10//  window — and asserts the published state transitions the command-center11//  UI renders from: step entries appear and stream, the plan lands, tokens12//  accumulate, the run completes, and the task record gains the persisted13//  agent-run message with its embedded steps. Everything runs against14//  temp-rooted persistence so the user's real tasks and policy rules are15//  never touched.16//1718import Foundation1920enum UISmoke {21    /// Returns the process exit status (0 = all checks passed).22    @MainActor23    static func run() async -> Int32 {24        var failures: [String] = []25        func check(_ condition: Bool, _ label: String) {26            if condition {27                print("  ✔ \(label)")28            } else {29                failures.append(label)30                print("  ✘ \(label)")31            }32        }3334        print("Zyquo Agent — UI smoke (RunController + MockProviderClient)")3536        // Temp-rooted persistence: no real tasks/rules touched.37        let temporaryRoot = FileManager.default.temporaryDirectory38            .appendingPathComponent("ZyquoAgent-uismoke-\(UUID().uuidString.prefix(8))")39        let persistence = PersistenceService(rootDirectory: temporaryRoot)40        defer { try? FileManager.default.removeItem(at: temporaryRoot) }4142        let store = TaskStore(persistence: persistence)43        let task = store.newTask(model: MockProviderClient.model)44        let controller = RunController(taskID: task.id, store: store, policyPersistence: persistence)4546        var sawEntries = false47        var sawPlan = false48        var sawRunningStatus = false49        var sawApproval = false5051        controller.start(52            prompt: "Create a demo folder in the workspace with a shell command and verify it exists.",53            model: MockProviderClient.model,54            client: MockProviderClient(),55            apiKey: "mock"56        )57        check(controller.isRunning, "run starts (isRunning)")5859        // Poll the published state until the run finishes (mock finishes in60        // well under a second; 15s is a generous CI ceiling).61        let deadline = Date().addingTimeInterval(15)62        while controller.outcome == nil && Date() < deadline {63            sawEntries = sawEntries || !controller.entries.isEmpty64            sawPlan = sawPlan || controller.plan != nil65            sawRunningStatus = sawRunningStatus || (store.task(id: task.id)?.status.isActive ?? false)66            // Guarded mode holds the mock's `mkdir` at the gate — resolve the67            // approval card the way the UI's Approve button does.68            if controller.pendingApproval != nil {69                sawApproval = true70                controller.resolveApproval(.approve)71            }72            try? await Task.sleep(nanoseconds: 20_000_000)73        }7475        check(sawEntries, "live step entries were published during the run")76        check(sawPlan, "plan was published (update_plan intercepted)")77        check(sawRunningStatus, "task status went active while running")78        check(sawApproval, "approval card surfaced and resolved (guarded mode)")7980        guard let outcome = controller.outcome else {81            print("  ✘ run did not finish within 15s")82            return 183        }84        if case .completed(let answer) = outcome {85            check(!answer.isEmpty, "run completed with a final answer")86        } else {87            check(false, "run completed (got \(outcome))")88        }8990        check(!controller.isRunning, "isRunning cleared after the run")91        check(controller.entries.isEmpty, "live timeline folded into history after the run")92        check(controller.tokensUsed > 0, "token usage accumulated")93        check(controller.pendingApproval == nil, "no dangling approval")9495        let finished = store.task(id: task.id)96        check(finished?.status == .done, "task status is Done")97        check(finished?.messages.count == 2, "history has prompt + run (got \(finished?.messages.count ?? 0))")98        let runMessage = finished?.messages.last99        check(runMessage?.kind == .agentRun, "last history entry is the agent run")100        check((runMessage?.steps?.count ?? 0) >= 3, "run message embeds the steps (got \(runMessage?.steps?.count ?? 0))")101        check(runMessage?.plan?.doneCount == 2, "run message carries the finished plan")102        check(finished?.workspacePath != nil, "task bound to a workspace")103        check(!controller.terminalLines.isEmpty, "terminal feed captured output")104105        // Reopen path: a fresh controller re-attaches the workspace.106        if let workspacePath = finished?.workspacePath {107            let reopened = RunController(taskID: task.id, store: store, policyPersistence: persistence)108            check(reopened.workspaceRoot?.path == workspacePath, "reopened controller reattaches the workspace")109            check(reopened.plan != nil, "reopened controller restores the last plan")110            try? FileManager.default.removeItem(at: URL(fileURLWithPath: workspacePath))111        }112113        if failures.isEmpty {114            print("UI smoke: all checks passed.")115            return 0116        }117        print("UI smoke: \(failures.count) check(s) FAILED.")118        return 1119    }120}121