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
JavaScript
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
};