UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

310 lines (304 loc) 8.69 kB
import { getPromoteRequiredSuites, init_test_manifest, loadTestManifest, requireTestManifest, resolveSuite, suiteTimeoutMs } from "./chunk-PBH357QR.js"; import { init_types, testRunEntityId } from "./chunk-E2CFJKLU.js"; import { createVcsOp, init_ops } from "./chunk-GRWQPKYK.js"; import { __esm } from "./chunk-2ESYSVXG.js"; // src/desk/browser-types.ts var DEFAULT_BROWSER_RELAY_PORT, DEFAULT_BROWSER_RELAY_URL, DEFAULT_BROWSER_SMOKE_STEPS; var init_browser_types = __esm({ "src/desk/browser-types.ts"() { "use strict"; DEFAULT_BROWSER_RELAY_PORT = 7420; DEFAULT_BROWSER_RELAY_URL = `http://127.0.0.1:${DEFAULT_BROWSER_RELAY_PORT}`; DEFAULT_BROWSER_SMOKE_STEPS = [ { type: "visible", selector: "body" }, { type: "noConsoleErrors" } ]; } }); // src/desk/browser-steps.ts import { existsSync, readFileSync } from "fs"; import { join } from "path"; function browserSuitePath(rootPath, suiteId) { return join(rootPath, ".trellis", "browser-suites", `${suiteId}.json`); } function loadBrowserSteps(rootPath, suiteId, stepsFile) { const path = stepsFile ? join(rootPath, stepsFile) : browserSuitePath(rootPath, suiteId); if (!existsSync(path)) { if (suiteId === "browser-smoke") return DEFAULT_BROWSER_SMOKE_STEPS; throw new Error( `Browser suite steps not found at ${path}. Add steps or use suite id browser-smoke.` ); } const raw = JSON.parse(readFileSync(path, "utf-8")); if (!Array.isArray(raw.steps) || raw.steps.length === 0) { throw new Error(`Invalid browser steps file ${path}: missing "steps" array`); } return raw.steps; } var init_browser_steps = __esm({ "src/desk/browser-steps.ts"() { "use strict"; init_browser_types(); } }); // src/desk/browser-verify-client.ts async function relayHealth(relayUrl = DEFAULT_BROWSER_RELAY_URL) { try { const res = await fetch(`${relayUrl.replace(/\/$/, "")}/health`); if (!res.ok) return { ok: false }; return await res.json(); } catch { return { ok: false }; } } async function runBrowserVerifyViaRelay(opts) { const relayUrl = (opts.relayUrl ?? DEFAULT_BROWSER_RELAY_URL).replace(/\/$/, ""); const body = { suiteId: opts.suiteId, steps: opts.steps, timeoutMs: opts.timeoutMs }; let res; try { res = await fetch(`${relayUrl}/browser/verify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); } catch (err) { const message = err instanceof Error ? err.message : "Could not reach browser relay"; return { ok: false, suiteId: opts.suiteId, steps: [], durationMs: 0, error: `${message}. Start relay: trellis browser relay`, exitCode: 1, output: message }; } const result = await res.json(); const output = formatVerifyOutput(result); const exitCode = result.ok ? 0 : 1; if (!res.ok && !result.error) { result.error = `HTTP ${res.status}`; } return { ...result, exitCode, output }; } function formatVerifyOutput(result) { const lines = []; if (result.tabUrl) lines.push(`tab: ${result.tabUrl}`); if (result.error) lines.push(`error: ${result.error}`); for (const step of result.steps) { const label = step.step.type + ("selector" in step.step ? ` ${step.step.selector}` : ""); lines.push(`${step.ok ? "\u2713" : "\u2717"} ${label}${step.message ? ` \u2014 ${step.message}` : ""}`); } lines.push(`duration: ${result.durationMs.toFixed(0)}ms`); return lines.join("\n"); } var init_browser_verify_client = __esm({ "src/desk/browser-verify-client.ts"() { "use strict"; init_browser_types(); } }); // src/vcs/test-runner.ts import { exec } from "child_process"; import { randomUUID } from "crypto"; import { promisify } from "util"; async function executeTestCommand(opts) { const started = Date.now(); try { const result = await execAsync(opts.command, { cwd: opts.cwd, timeout: opts.timeoutMs ?? 12e4, maxBuffer: 1024 * 1024 }); const output = (result.stdout + "\n" + result.stderr).trim(); return { status: "passed", output, exitCode: 0, durationMs: Date.now() - started }; } catch (err) { const output = ((err.stdout ?? "") + "\n" + (err.stderr ?? err.message ?? "")).trim(); return { status: "failed", output, exitCode: err.code ?? 1, durationMs: Date.now() - started }; } } async function emitTestRunOp(ctx, params) { const testRunId = testRunEntityId(randomUUID()); const op = await createVcsOp("vcs:testRun", { agentId: ctx.agentId, previousHash: ctx.getLastOp()?.hash, vcs: { testRunId, testRunSuite: params.suiteId, testRunCommand: params.command, testRunStatus: params.status, testRunOutput: params.output?.slice(0, 4096), testRunExitCode: params.exitCode, testRunDurationMs: params.durationMs, testRunTrigger: params.trigger, laneId: params.laneId, issueId: params.issueId } }); await ctx.applyOp(op); return { testRunId, suite: params.suiteId, command: params.command, status: params.status, output: params.output, exitCode: params.exitCode, durationMs: params.durationMs, opHash: op.hash }; } async function runTestSuite(ctx, params) { const manifestRoot = params.manifestRoot ?? params.cwd; const manifest = params.manifest ?? requireTestManifest(manifestRoot); const suite = resolveSuite(manifest, params.suiteId); if (suite.kind === "browser") { const steps = loadBrowserSteps( manifestRoot, params.suiteId, suite.stepsFile ); const executed2 = await runBrowserVerifyViaRelay({ suiteId: params.suiteId, steps, timeoutMs: suiteTimeoutMs(suite) }); return emitTestRunOp(ctx, { suiteId: params.suiteId, command: suite.command, status: executed2.ok ? "passed" : "failed", output: executed2.output, exitCode: executed2.exitCode, durationMs: executed2.durationMs, laneId: params.laneId, issueId: params.issueId, trigger: params.trigger ?? "manual" }); } const executed = await executeTestCommand({ command: suite.command, cwd: params.cwd, timeoutMs: suiteTimeoutMs(suite) }); return emitTestRunOp(ctx, { suiteId: params.suiteId, command: suite.command, status: executed.status, output: executed.output, exitCode: executed.exitCode, durationMs: executed.durationMs, laneId: params.laneId, issueId: params.issueId, trigger: params.trigger ?? "manual" }); } async function runTestSuites(ctx, params) { const manifestRoot = params.manifestRoot ?? params.cwd; const manifest = params.manifest ?? requireTestManifest(manifestRoot); const results = []; for (const suiteId of params.suiteIds) { results.push( await runTestSuite(ctx, { cwd: params.cwd, manifestRoot, suiteId, manifest, laneId: params.laneId, issueId: params.issueId, trigger: params.trigger }) ); } return results; } async function runPromoteRequiredTests(ctx, cwd, laneId, manifestRoot) { const root = manifestRoot ?? cwd; const manifest = requireTestManifest(root); const suiteIds = getPromoteRequiredSuites(manifest); if (suiteIds.length === 0) { throw new Error( "No promote test suites configured. Set promote.require or defaultSuite in .trellis/tests.json" ); } return runTestSuites(ctx, { cwd, manifestRoot: root, suiteIds, manifest, laneId, trigger: "pre-promote" }); } function allTestRunsPassed(results) { return results.length > 0 && results.every((r) => r.status === "passed"); } function tryLoadTestManifest(rootPath) { try { return loadTestManifest(rootPath); } catch { return null; } } function describeSuite(suiteId, suite) { const label = suite.description ?? suiteId; return `${suiteId}: ${label}`; } var execAsync; var init_test_runner = __esm({ "src/vcs/test-runner.ts"() { init_ops(); init_test_manifest(); init_types(); init_browser_steps(); init_browser_verify_client(); execAsync = promisify(exec); } }); export { DEFAULT_BROWSER_RELAY_PORT, DEFAULT_BROWSER_RELAY_URL, DEFAULT_BROWSER_SMOKE_STEPS, init_browser_types, loadBrowserSteps, init_browser_steps, relayHealth, runBrowserVerifyViaRelay, init_browser_verify_client, executeTestCommand, emitTestRunOp, runTestSuite, runTestSuites, runPromoteRequiredTests, allTestRunsPassed, tryLoadTestManifest, describeSuite, init_test_runner };