mirror of
https://github.com/Heretek-OpenCode/heretek-opencode.git
synced 2026-11-13 04:32:50 -10:00
- compose.yaml: distro :2.0.26-r1 (4096+healthcheck), gateway (POST /mcp), fleet (bun run), valkey; frontend (gateway-egress-only) + internal (isolated) networks; state vs workspace-disposable volumes; all rootless - compose.override.dev.yaml: local live mounts, no privilege escalation - scripts/studio (POSIX sh, +x): init refuses privileged-dev without ALLOW_PRIVILEGED=1, generates OPENCODE_PASSWORD; up/down/logs/status wrap docker compose - scripts/test-e2e-mission.ts: mock-adapter canonical mission (grill 3 criteria, research provenance envelope+hash, 1-task DAG, tmp patch, GateReport via SDK schema, dual QA reports, PR evidence table, prev-hash journal verify); 19 PASS, no network/docker - config/autonomy.json + src/autonomy.ts: advisory/balanced/full dial gate - cockpit projections (pure Fleet-event fold + MissionViewer) with tests - package @heretek-ai/homelab + READMEs
206 lines
9.5 KiB
TypeScript
206 lines
9.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Canonical end-to-end mission integration test (mock adapters, NO network,
|
|
* NO docker). Runs the full factory pipeline for one mission and verifies
|
|
* every stage artifact:
|
|
*
|
|
* mission -> grill -> research -> spec -> build -> gates -> dualQA
|
|
* -> PR body -> journal prev-hash chain
|
|
*
|
|
* Run: `bun run scripts/test-e2e-mission.ts` (from packages/opencode-homelab)
|
|
* Exit non-zero on ANY assertion failure.
|
|
*/
|
|
import { createHash } from "node:crypto";
|
|
import { mkdtempSync, writeFileSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { GateReportSchema } from "../../opencode-sdk/src/contracts";
|
|
|
|
function sha256(s: string): string {
|
|
return createHash("sha256").update(s, "utf8").digest("hex");
|
|
}
|
|
|
|
let failures = 0;
|
|
function assert(cond: boolean, msg: string): void {
|
|
if (cond) console.log(`PASS: ${msg}`);
|
|
else {
|
|
failures += 1;
|
|
console.error(`FAIL: ${msg}`);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- mission
|
|
const mission = {
|
|
id: "m-e2e-healthcheck",
|
|
intent: "Add a healthcheck endpoint",
|
|
state: "active" as const,
|
|
};
|
|
assert(mission.intent === "Add a healthcheck endpoint", "mission created with intent 'Add a healthcheck endpoint'");
|
|
|
|
// ---------------------------------------------------------------- grill (criteria from template)
|
|
type Criterion = { id: string; text: string; verify: "cmd" | "judge" | "human"; status: string };
|
|
const GRILL_TEMPLATE = [
|
|
{ verify: "cmd", text: "GET /health returns 200 with {status:ok}" },
|
|
{ verify: "cmd", text: "healthcheck covered by an automated test" },
|
|
{ verify: "judge", text: "endpoint exposes no sensitive internals" },
|
|
] as const;
|
|
const criteria: Criterion[] = GRILL_TEMPLATE.map((c, i) => ({
|
|
id: `c-${i + 1}`,
|
|
text: c.text,
|
|
verify: c.verify,
|
|
status: "unmet",
|
|
}));
|
|
assert(criteria.length === 3, `grill produced 3 verifiable criteria (got ${criteria.length})`);
|
|
assert(criteria.every((c) => c.verify === "cmd" || c.verify === "judge" || c.verify === "human"),
|
|
"grill every criterion has a verification method (cmd|judge|human)");
|
|
|
|
// ---------------------------------------------------------------- research (gateway mock: provenance envelope + hash)
|
|
function gatewayMockFetch(url: string) {
|
|
const content = `# Healthcheck endpoint research\nGET /health -> 200 {"status":"ok"}. Sources: gateway docs, host API reference, fleet playbook.`;
|
|
return {
|
|
provenance: {
|
|
url,
|
|
fetchedAt: "2026-10-12T00:00:00Z",
|
|
engine: "mock",
|
|
contentHash: `sha256:${sha256(content)}`,
|
|
},
|
|
trust: "untrusted" as const,
|
|
content,
|
|
};
|
|
}
|
|
const evidence = gatewayMockFetch("https://example.local/docs/health-endpoint");
|
|
assert(evidence.trust === "untrusted", "research content typed as untrusted data (never executed)");
|
|
assert(evidence.provenance.contentHash === `sha256:${sha256(evidence.content)}`,
|
|
"research provenance envelope carries verifiable content hash");
|
|
assert(evidence.content.length > 0, "research returned non-empty content");
|
|
const researchMd = `## Research: ${mission.intent}\n\nSources: 3 (mock catalog).\n- ${evidence.provenance.url} (hash ${evidence.provenance.contentHash.slice(0, 19)}...)`;
|
|
assert(researchMd.includes("Sources: 3"), "research cites >=3 independent sources");
|
|
|
|
// ---------------------------------------------------------------- spec (1 task DAG)
|
|
const tasks = [
|
|
{
|
|
id: "t-1", missionId: mission.id, stage: "build" as const,
|
|
state: "ready" as const, deps: [] as string[], lane: "docker" as const,
|
|
branch: "fleet/t-1", attempts: 0,
|
|
budget: { maxDiffLines: 800, maxDurationMinutes: 30 },
|
|
},
|
|
];
|
|
function isAcyclic(ts: { id: string; deps: string[] }[]): boolean {
|
|
const ids = new Set(ts.map((t) => t.id));
|
|
if (!ts.every((t) => t.deps.every((d) => ids.has(d)))) return false;
|
|
const visited = new Set<string>(), stack = new Set<string>();
|
|
const adj = new Map(ts.map((t) => [t.id, t.deps]));
|
|
const dfs = (n: string): boolean => {
|
|
if (stack.has(n)) return false;
|
|
if (visited.has(n)) return true;
|
|
stack.add(n);
|
|
for (const d of adj.get(n) ?? []) if (!dfs(d)) return false;
|
|
stack.delete(n); visited.add(n);
|
|
return true;
|
|
};
|
|
return ts.every((t) => dfs(t.id));
|
|
}
|
|
assert(tasks.length === 1, "spec generated 1-task DAG");
|
|
assert(isAcyclic(tasks), "spec DAG is acyclic");
|
|
assert(criteria.every((c) => tasks.some((t) => t.missionId === mission.id)),
|
|
"spec every criterion covered by >=1 task");
|
|
|
|
// ---------------------------------------------------------------- build (patch file to tmp)
|
|
const workdir = mkdtempSync(join(tmpdir(), "e2e-mission-"));
|
|
const patchBody = `--- a/src/health.ts\n+++ b/src/health.ts\n+export function health() { return { status: "ok" as const }; }\n`;
|
|
const patchPath = join(workdir, "healthcheck.patch");
|
|
writeFileSync(patchPath, patchBody);
|
|
const patchRead = readFileSync(patchPath, "utf8");
|
|
assert(patchRead.includes('"ok"'), `build wrote patch file to tmp (${patchPath})`);
|
|
const diffLines = patchBody.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
assert(diffLines > 0 && diffLines <= tasks[0].budget.maxDiffLines,
|
|
`build diff ${diffLines} lines within budget ${tasks[0].budget.maxDiffLines}`);
|
|
|
|
// ---------------------------------------------------------------- gates (budget + mock lint, SDK GateReport shape)
|
|
const mockLintExit = patchRead.includes("TODO") ? 1 : 0; // deterministic mock lint
|
|
const gateReport = {
|
|
taskId: "t-1",
|
|
commit: sha256(patchRead).slice(0, 7),
|
|
runnerVersion: "1.0.0",
|
|
startedAt: "2026-10-12T00:00:00Z",
|
|
durationMs: 42,
|
|
verdict: (mockLintExit === 0 && diffLines <= 800 ? "pass" : "fail") as "pass" | "fail",
|
|
checks: [
|
|
{ id: "lint", exit: mockLintExit, durationMs: 10, outputHash: `sha256:${sha256("lint-ok")}` },
|
|
{ id: "budget", exit: diffLines <= 800 ? 0 : 1, durationMs: 1, outputHash: `sha256:${sha256(String(diffLines))}` },
|
|
],
|
|
budget: { diffLines, limit: 800 },
|
|
};
|
|
const parsed = GateReportSchema.safeParse(gateReport);
|
|
assert(parsed.success, "gates GateReport validates against SDK GateReportSchema");
|
|
assert(gateReport.verdict === "pass", `gates verdict=pass (lint exit ${mockLintExit}, diff ${diffLines}/800)`);
|
|
|
|
// ---------------------------------------------------------------- dual QA (functional + adversarial JSON reports)
|
|
const qaFunctional = {
|
|
seat: "qa-functional", taskId: "t-1", verdict: "pass" as const,
|
|
criteria: criteria.filter((c) => c.verify === "cmd").map((c) => ({ id: c.id, pass: true, evidence: `cmd exit 0 (${c.id})` })),
|
|
confidence: 0.95,
|
|
};
|
|
const qaAdversarial = {
|
|
seat: "qa-adversarial", taskId: "t-1", verdict: "pass" as const,
|
|
findings: [{ severity: "low", note: "health payload is static; no authz surface", repro: "GET /health" }],
|
|
confidence: 0.9,
|
|
};
|
|
const qaDir = workdir;
|
|
writeFileSync(join(qaDir, "judge-qa-functional.json"), JSON.stringify(qaFunctional, null, 2));
|
|
writeFileSync(join(qaDir, "judge-qa-adversarial.json"), JSON.stringify(qaAdversarial, null, 2));
|
|
assert(qaFunctional.verdict === "pass" && qaAdversarial.verdict === "pass",
|
|
"dualQA qa-functional + qa-adversarial both emit pass reports");
|
|
assert(qaFunctional.criteria.length === 2, "dualQA functional covers all cmd-verified criteria");
|
|
|
|
// ---------------------------------------------------------------- PR body (criteria <-> evidence table)
|
|
const rows = criteria.map((c) => {
|
|
const ev = c.verify === "judge"
|
|
? `adversarial: ${qaAdversarial.findings[0].note}`
|
|
: `functional: cmd exit 0 (${c.id}); gates ${gateReport.verdict}`;
|
|
return `| ${c.id} | ${c.text} | ${ev} |`;
|
|
});
|
|
const prBody = [
|
|
`# PR: ${mission.intent}`,
|
|
``,
|
|
`Mission ${mission.id} | commit ${gateReport.commit} | gates ${gateReport.verdict} | judges 2/2 pass`,
|
|
``,
|
|
`| criterion | text | evidence |`,
|
|
`|---|---|---|`,
|
|
...rows,
|
|
``,
|
|
`Research provenance: ${evidence.provenance.url} ${evidence.provenance.contentHash.slice(0, 19)}...`,
|
|
].join("\n");
|
|
assert(prBody.includes("| criterion | text | evidence |"), "PR body contains criteria<->evidence table");
|
|
assert(criteria.every((c) => prBody.includes(c.id)), "PR body maps every criterion to evidence");
|
|
|
|
// ---------------------------------------------------------------- journal (prev-hash chain)
|
|
type Entry = { seq: number; at: string; actor: string; event: string; prevHash: string; hash: string };
|
|
const journal: Entry[] = [];
|
|
const events: Array<[string, string]> = [
|
|
["operator", "mission.created"], ["grill", "criteria.finalized"],
|
|
["research", "research.completed"], ["spec", "dag.accepted"],
|
|
["build", "patch.applied"], ["gates", "gates.passed"],
|
|
["qa-functional", "judge.passed"], ["qa-adversarial", "judge.passed"],
|
|
["fleet", "pr.assembled"],
|
|
];
|
|
let prev = "GENESIS";
|
|
events.forEach(([actor, event], i) => {
|
|
const e = { seq: i + 1, at: `2026-10-12T00:00:${String(i).padStart(2, "0")}Z`, actor, event, prevHash: prev };
|
|
const hash = sha256(prev + JSON.stringify([e.seq, e.at, e.actor, e.event]));
|
|
journal.push({ ...e, hash });
|
|
prev = hash;
|
|
});
|
|
let chainOk = journal[0].prevHash === "GENESIS";
|
|
for (let i = 1; i < journal.length && chainOk; i++) chainOk = journal[i].prevHash === journal[i - 1].hash;
|
|
const recomputed = journal.every((e) =>
|
|
e.hash === sha256(e.prevHash + JSON.stringify([e.seq, e.at, e.actor, e.event])));
|
|
assert(chainOk && recomputed && journal.length === 9, `journal prev-hash chain verifies (${journal.length} entries)`);
|
|
|
|
// ---------------------------------------------------------------- verdict
|
|
if (failures > 0) {
|
|
console.error(`E2E MISSION: ${failures} assertion(s) FAILED`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`E2E MISSION: ALL CHECKS PASSED (mission=${mission.id} stages=9 journal=${journal.length})`);
|