From 660d1f69d835dfdf1ab65bca0c608cadf376eff3 Mon Sep 17 00:00:00 2001 From: umpireboulianne-design Date: Sat, 24 Oct 2026 12:15:12 -0400 Subject: [PATCH] ci(agent): add blocking agent test and typecheck runner and CI verification Fixes #514 --- docs/CI.md | 9 ++++++ package.json | 5 +-- scripts/agent-ci.test.mjs | 57 ++++++++++++++++++++++++++++++++ scripts/run-agent-ci.mjs | 68 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 scripts/agent-ci.test.mjs create mode 100644 scripts/run-agent-ci.mjs diff --git a/docs/CI.md b/docs/CI.md index 4a3af75..4b7e9be 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -83,3 +83,12 @@ print order are asserted by: ```bash node --test scripts/check-bindings-cli.test.mjs ``` + +## Agent checks + +Run the blocking test and typecheck suite for Agent locally and in CI: + +```bash +pnpm agent:ci +node --test scripts/agent-ci.test.mjs +``` diff --git a/package.json b/package.json index 66e5b7c..223b0cb 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "errors:check": "node scripts/check-round-errors.mjs", "errors:test": "node --test scripts/check-round-errors.test.mjs", "bindings:generate": "stellar contract build --out-dir artifacts && rm -rf tmp-bindings && stellar contract bindings typescript --wasm artifacts/sub_rosa_round.wasm --output-dir tmp-bindings --overwrite && cp tmp-bindings/src/index.ts packages/round-bindings/src/index.ts && rm -rf tmp-bindings", - "bindings:check": "stellar contract build --out-dir artifacts && rm -rf tmp-bindings && stellar contract bindings typescript --wasm artifacts/sub_rosa_round.wasm --output-dir tmp-bindings --overwrite && (diff tmp-bindings/src/index.ts packages/round-bindings/src/index.ts; diff_exit=$?; rm -rf tmp-bindings; if [ $diff_exit -ne 0 ]; then echo '❌ Round bindings are stale. Run pnpm bindings:generate to update them.'; exit 1; else echo '✅ Round bindings are up to date.'; fi)", + "bindings:check": "stellar contract build --out-dir artifacts && rm -rf tmp-bindings && stellar contract bindings typescript --wasm artifacts/sub_rosa_round.wasm --output-dir tmp-bindings --overwrite && (diff tmp-bindings/src/index.ts packages/round-bindings/src/index.ts; diff_exit=$?; rm -rf tmp-bindings; if [ $diff_exit -ne 0 ]; then echo '\u274c Round bindings are stale. Run pnpm bindings:generate to update them.'; exit 1; else echo '\u2705 Round bindings are up to date.'; fi)", "sdk:test": "pnpm --filter @sub-rosa/sdk test", "sdk:typecheck": "pnpm --filter @sub-rosa/sdk typecheck", "bindings:test": "pnpm --filter @sub-rosa/round-bindings test", @@ -87,6 +87,7 @@ "errors:normalize:check": "node scripts/check-error-normalization.mjs", "errors:normalize:test": "pnpm --filter @sub-rosa/logging test && node --test scripts/check-error-normalization.test.mjs", "line-lengths:check": "node scripts/check-line-lengths.mjs", - "line-lengths:test": "node --test scripts/check-line-lengths.test.mjs" + "line-lengths:test": "node --test scripts/check-line-lengths.test.mjs", + "agent:ci": "node --test scripts/agent-ci.test.mjs && pnpm agent:test && pnpm agent:typecheck" } } diff --git a/scripts/agent-ci.test.mjs b/scripts/agent-ci.test.mjs new file mode 100644 index 0000000..e23b346 --- /dev/null +++ b/scripts/agent-ci.test.mjs @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Sub Rosa contributors +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { + CI_STEPS, + TARGET_WORKSPACES, + runAgentCi, + verifyAgentCiConfig, +} from "./run-agent-ci.mjs"; + +const rootPkg = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +); + +test("Agent CI configuration defines blocking test and typecheck steps in order", () => { + const verified = verifyAgentCiConfig(); + assert.deepEqual(verified.steps, ["agent:test", "agent:typecheck"]); + assert.ok(TARGET_WORKSPACES.length >= 1); + + assert.ok( + rootPkg.scripts["agent:ci"], + "root script agent:ci must exist", + ); + + let previousIndex = -1; + for (const step of CI_STEPS) { + assert.ok(rootPkg.scripts[step], `root script ${step} must exist`); + const idx = rootPkg.scripts["agent:ci"].indexOf(`pnpm ${step}`); + assert.ok( + idx > previousIndex, + `${step} must run as a blocking step in agent:ci`, + ); + previousIndex = idx; + } +}); + +test("Agent CI runner executes each step sequentially and blocks on failure", () => { + const executed = []; + runAgentCi(undefined, (_bin, args) => { + executed.push(args.join(" ")); + }); + assert.deepEqual( + executed, + CI_STEPS.map((step) => `run ${step}`), + ); + + assert.throws( + () => + runAgentCi(undefined, (_bin, args) => { + if (args[1] === CI_STEPS[0]) { + throw new Error("simulated step failure"); + } + }), + /Blocking CI step failed/, + ); +}); diff --git a/scripts/run-agent-ci.mjs b/scripts/run-agent-ci.mjs new file mode 100644 index 0000000..e9e7611 --- /dev/null +++ b/scripts/run-agent-ci.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +// Copyright (c) 2026 Sub Rosa contributors +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createLogger } from "../packages/logging/src/index.cjs"; +import { normalizeError } from "../packages/logging/src/errors.cjs"; + +const diagnostics = createLogger("scripts.run-agent-ci"); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); + +export const CI_STEPS = Object.freeze(["agent:test", "agent:typecheck"]); +export const TARGET_WORKSPACES = Object.freeze([{"path": "services/agent", "name": "@sub-rosa/agent"}]); + +export function verifyAgentCiConfig(root = ROOT) { + const rootPkgPath = resolve(root, "package.json"); + const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf8")); + const scripts = rootPkg.scripts ?? {}; + + for (const step of CI_STEPS) { + if (!scripts[step]) { + throw new Error(`Missing required root script: ${step}`); + } + } + + for (const ws of TARGET_WORKSPACES) { + const wsPkgPath = resolve(root, ws.path, "package.json"); + if (!existsSync(wsPkgPath)) { + throw new Error(`Missing workspace package.json: ${ws.path}`); + } + const wsPkg = JSON.parse(readFileSync(wsPkgPath, "utf8")); + if (wsPkg.name !== ws.name) { + throw new Error(`Unexpected package name in ${ws.path}: ${wsPkg.name}`); + } + if (!wsPkg.scripts?.test) { + throw new Error(`Missing test script in ${ws.path}`); + } + if (!wsPkg.scripts?.typecheck) { + throw new Error(`Missing typecheck script in ${ws.path}`); + } + } + + return { steps: CI_STEPS, workspaces: TARGET_WORKSPACES }; +} + +export function runAgentCi(root = ROOT, runner = execFileSync) { + verifyAgentCiConfig(root); + const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; + for (const step of CI_STEPS) { + diagnostics.info("ci-step-start", `Running pnpm ${step}`); + try { + runner(pnpmBin, ["run", step], { + cwd: root, + stdio: "inherit", + env: { ...process.env }, + }); + } catch (error) { + diagnostics.error("ci-step-failed", normalizeError(error)); + throw new Error(`Blocking CI step failed: ${step}`); + } + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + runAgentCi(); +}