Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 用法
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"bin": {
"trace": "./bin/trace.mjs"
},
"scripts": {
"test": "node --test"
},
"engines": {
"node": ">=18"
}
Expand Down
83 changes: 83 additions & 0 deletions test/trace.test.mjs
Original file line number Diff line number Diff line change
@@ -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, /<svg id="diagram"/);
assert.match(html, /data-theme="dark"/);
assert.match(html, /static artifact, no runtime claim/);
});
}

test("validate rejects a spec that breaks a check", () => {
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);
});
Loading