From 37d079c3b1fb2edf7a1bf629ecb0ceeb69d8407b Mon Sep 17 00:00:00 2001 From: CodeAlex52 <59381946+CodeAlex52@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:19:19 +0800 Subject: [PATCH] test: add zero-dep node:test suite + CI workflow (closes #1) --- .github/workflows/ci.yml | 22 +++++++++++ README.md | 4 +- package.json | 3 ++ test/trace.test.mjs | 83 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 test/trace.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c8f3ce2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + name: node ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Zero-dependency test suite + run: npm test diff --git a/README.md b/README.md index f89d9f3..c4fab78 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ `SKILL.md → JSON IR → validate 5项 → deliver 单文件HTML`,学 Archify 链路的最小复刻,切更窄的 Agent 调试痛点。 -[English](./README_EN.md) · MIT · `node >= 18` +[English](./README_EN.md) · MIT · `node >= 18` · [![CI](https://github.com/CodeAlex52/agent-loop-trace/actions/workflows/ci.yml/badge.svg)](https://github.com/CodeAlex52/agent-loop-trace/actions/workflows/ci.yml) ![hero](./docs/assets/hero.svg) @@ -26,6 +26,8 @@ node bin/trace.mjs deliver examples/strands-forcing.json /tmp/loop-trace/demo.ht # 用浏览器打开 /tmp/loop-trace/demo.html:T 切主题,E 导出 PNG ``` +零依赖回归测试:`npm test`(Node 内置 node:test,覆盖 doctor / validate / deliver / 坏样本拒绝)。 + 在线例子(本地打开):`examples/rendered/strands.html`、`examples/rendered/dspy.html` ## Agent Skill 用法 diff --git a/package.json b/package.json index b3921a8..9fbd1df 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "bin": { "trace": "./bin/trace.mjs" }, + "scripts": { + "test": "node --test" + }, "engines": { "node": ">=18" } diff --git a/test/trace.test.mjs b/test/trace.test.mjs new file mode 100644 index 0000000..6fda23e --- /dev/null +++ b/test/trace.test.mjs @@ -0,0 +1,83 @@ +// Zero-dependency smoke + regression suite for agent-loop-trace. +// Run with: npm test (Node >= 18, built-in node:test runner) +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const BIN = join(ROOT, "bin", "trace.mjs"); +const EXAMPLES = ["examples/strands-forcing.json", "examples/dspy-reward.json"]; + +function run(args) { + try { + const stdout = execFileSync(process.execPath, [BIN, ...args], { + encoding: "utf8", + cwd: ROOT, + }); + return { code: 0, stdout }; + } catch (error) { + return { code: error.status ?? 1, stdout: String(error.stdout ?? "") }; + } +} + +test("doctor reports a ready toolchain", () => { + const r = run(["doctor", "--json"]); + assert.equal(r.code, 0); + const out = JSON.parse(r.stdout); + assert.equal(out.ok, true); + assert.deepEqual(out.checks, ["schema present", "renderer present", "no deps"]); +}); + +for (const example of EXAMPLES) { + test(`validate passes for ${example}`, () => { + const r = run(["validate", example, "--json"]); + assert.equal(r.code, 0); + const receipt = JSON.parse(r.stdout); + assert.equal(receipt.ok, true); + assert.equal(receipt.composition.status, "pass"); + assert.equal(receipt.composition.summary.errors, 0); + }); + + test(`deliver renders a self-contained HTML for ${example}`, () => { + const out = join(mkdtempSync(join(tmpdir(), "loop-trace-")), "trace.html"); + const r = run(["deliver", example, out, "--json"]); + assert.equal(r.code, 0); + const receipt = JSON.parse(r.stdout); + assert.equal(receipt.ok, true); + assert.match(receipt.artifact.sha256, /^[0-9a-f]{64}$/); + const html = readFileSync(out, "utf8"); + assert.match(html, / { + const bad = join(mkdtempSync(join(tmpdir(), "loop-trace-bad-")), "bad.json"); + const spec = JSON.parse(readFileSync(join(ROOT, EXAMPLES[0]), "utf8")); + spec.participants = [spec.participants[0]]; // violates the 2..7 participants check + writeFileSync(bad, JSON.stringify(spec)); + + const r = run(["validate", bad, "--json"]); + assert.equal(r.code, 1); + const receipt = JSON.parse(r.stdout); + assert.equal(receipt.ok, false); + assert.ok(receipt.composition.summary.errors > 0); +}); + +test("deliver refuses to render an invalid spec", () => { + const dir = mkdtempSync(join(tmpdir(), "loop-trace-refuse-")); + const bad = join(dir, "bad.json"); + const spec = JSON.parse(readFileSync(join(ROOT, EXAMPLES[0]), "utf8")); + spec.events = spec.events.slice(0, 1); // violates the 2..12 events check + writeFileSync(bad, JSON.stringify(spec)); + + const r = run(["deliver", bad, join(dir, "out.html"), "--json"]); + assert.equal(r.code, 1); + const receipt = JSON.parse(r.stdout); + assert.equal(receipt.ok, false); +});