// node --test modules/cicd-workload/test.mjs (after `cargo wasm`; ../atlibs: its JS host) // The workload composed with process-mcp, the machine's MCP shell a stub iroh: fetcher: checkout, then // each step as `sh -s` with its env on stdin; output as results; the first failing step ends the job. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { loadComposition, runCompose } from "../../../atlibs/javascript/src/compose/index.js"; const lib = (name) => ({ ...JSON.parse(readFileSync(new URL(`../${name}/module.json`, import.meta.url), "utf8")), module: readFileSync(new URL(`../../target/wasm32-unknown-unknown/release/${name.replaceAll("-", "_")}.wasm`, import.meta.url)) }); const ctx = { wasm: { run: async ({ module, entry, input }) => { const x = new WebAssembly.Instance(new WebAssembly.Module(module), {}).exports; const data = new TextEncoder().encode(JSON.stringify(input)); const ptr = x.alloc(data.length); new Uint8Array(x.memory.buffer, ptr, data.length).set(data); const packed = BigInt.asUintN(64, x[entry](ptr, data.length)); return JSON.parse(new TextDecoder().decode(new Uint8Array(x.memory.buffer, Number(packed >> 32n), Number(packed & 0xffffffffn)))); } } }; const P = "computer.socialweb.cicd.process."; const manifest = { $type: "com.fedfork.exec.composition", name: "workload", instances: [{ name: "workload", module: "cicd-workload" }, { name: "process", module: "process-mcp" }], edges: ["start", "write", "read"].map((e) => ({ from: "workload", nsid: P + e, to: "process", reason: `the job's ${e}` })), exports: [{ nsid: "computer.socialweb.cicd.workload.run", instance: "workload", entry: "run" }], }; // The guest agent's MCP shell: each job's output is "ran ", in two reads; a script // with `exit 3` in it exits 3. function shell() { const jobs = []; const calls = []; const tools = { start: ({ command, stdin }) => (assert.equal(stdin, undefined), jobs.push({ command, stdin: "", reads: 0 }), { id: String(jobs.length) }), write: ({ id, data, close }) => (assert.equal(close, true), (jobs[id - 1].stdin = data), {}), read: ({ id, cursor }) => { const job = jobs[id - 1]; const out = `ran ${job.stdin.trim().split("\n").pop()}\n`; if (job.reads++ === 0) return { output: out.slice(0, 4), cursor: 4, done: false }; return { output: out.slice(cursor), cursor: out.length, done: true, exitCode: job.stdin.includes("exit 3") ? 3 : 0 }; }, }; const fetch = async ({ method, url, body }) => { assert.equal(method, "POST"); assert.equal(url, "iroh:///e30/shell/mcp"); const { params } = JSON.parse(body); calls.push(params.name); const out = tools[params.name](params.arguments); return { status: 200, headers: {}, body: JSON.stringify({ jsonrpc: "2.0", id: 1, result: { content: [{ type: "text", text: JSON.stringify(out) }], structuredContent: out } }) }; }; return { jobs, calls, fetchers: { "iroh:": fetch } }; } const job = (steps) => ({ checkout: { git: "https://knotless.dev/alice.test/proj", commit: "abc123" }, env: { CI: "1" }, steps }); for (const [name, steps, ok, codes] of [ ["every step runs", [{ name: "build", run: "make" }, { name: "test", run: "make test", env: { X: "it's" } }], true, [0, 0, 0]], ["the first failing step ends the job", [{ name: "bad", run: "exit 3" }, { name: "never", run: "true" }], false, [0, 3]], ]) { test(`cicd-workload: ${name}`, async () => { const m = shell(); const comp = await loadComposition(ctx, manifest, { load: async (key) => lib(key) }); const said = []; const onEvent = (event, out) => out?.result?.output && said.push(out.result.output); const ran = await runCompose(ctx, comp, "workload", "run", { url: "iroh:///e30/shell/mcp", job: job(steps) }, { fetchers: m.fetchers, http: { "iroh://*": {} }, onEvent, budget: { calls: Infinity, wallMs: Infinity } }); assert.equal(ran.error, undefined, ran.error); assert.equal(ran.result.ok, ok); assert.ok(said.join("").includes("ran "), JSON.stringify(said)); assert.deepEqual(ran.result.steps.map((s) => s.exitCode), codes); assert.equal(ran.result.steps[0].name, "checkout"); assert.match(m.jobs[0].stdin, /git clone -q 'https:\/\/knotless.dev\/alice.test\/proj' \/work\ncd \/work\ngit checkout -q 'abc123'/); assert.match(m.jobs[1].stdin, /^cd \/work\nexport KNOTLESS_RUN_OUTPUTS=\/outputs\nexport CI='1'\n/); if (ok) assert.match(m.jobs[2].stdin, /export X='it'\\''s'\nexec sh -ec 'make test'\n$/); assert.equal(m.jobs.length, codes.length); assert.ok(m.calls.every((c) => ["start", "write", "read"].includes(c))); }); } test("cicd-workload: a runner checks out once, then runs a step a call; nothing to run is ok", async () => { const comp = await loadComposition(ctx, manifest, { load: async (key) => lib(key) }); const run = async (j) => { const m = shell(); const ran = await runCompose(ctx, comp, "workload", "run", { url: "iroh:///e30/shell/mcp", job: j }, { fetchers: m.fetchers, http: { "iroh://*": {} }, budget: { calls: Infinity, wallMs: Infinity } }); return { ran, m }; }; const one = await run({ steps: [{ name: "test", run: "make test" }] }); assert.deepEqual(one.ran.result.steps, [{ name: "test", exitCode: 0 }]); assert.equal(one.m.jobs.length, 1); const none = await run({}); assert.deepEqual([none.ran.result, none.m.jobs.length], [{ steps: [], ok: true }, 0]); });