文件
hyb-oa/server/tests/unit/scripts/trellis-runtime-contract.test.js
T

181 行
7.1 KiB
JavaScript

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const repoRoot = path.resolve(__dirname, "../../../..");
const scriptsRoot = path.join(repoRoot, ".trellis/scripts");
describe("Trellis runtime manifest contract", () => {
let root;
let taskDir;
beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), "trellis-runtime-contract-"));
taskDir = path.join(root, ".trellis/tasks/fixture");
fs.mkdirSync(taskDir, { recursive: true });
fs.mkdirSync(path.join(root, ".trellis/spec"));
fs.writeFileSync(path.join(root, ".trellis/spec/contract.md"), "# Contract\n");
fs.writeFileSync(path.join(taskDir, "task.json"), JSON.stringify({ status: "planning" }));
});
afterEach(() => {
fs.rmSync(root, { recursive: true, force: true });
});
function runCase(mode, content, command = "validate") {
fs.writeFileSync(path.join(root, ".trellis/config.yaml"), `session_auto_commit: false\ncodex:\n dispatch_mode: ${mode}\n`);
for (const name of ["implement", "check"]) {
fs.writeFileSync(path.join(taskDir, `${name}.jsonl`), content);
}
const runner = `
import argparse
import sys
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, sys.argv[1])
import task
from common.task_context import cmd_validate
root = Path(sys.argv[2])
args = argparse.Namespace(dir=".trellis/tasks/fixture", allow_empty_context=False)
if sys.argv[3] == "start":
with patch("task.get_repo_root", return_value=root), patch("task.resolve_context_key", return_value=None), patch("task._record_start_state"):
result = task.cmd_start(args)
else:
with patch("common.task_context.get_repo_root", return_value=root):
result = cmd_validate(args)
sys.exit(0 if result == 0 else 1)
`;
const result = spawnSync("python3", ["-c", runner, scriptsRoot, root, command], {
encoding: "utf8",
env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
timeout: 10000,
});
if (result.error) throw result.error;
expect(result.stderr).not.toContain("Traceback");
return result;
}
it.each(["validate", "start"])("permits empty inline manifests at %s", (command) => {
expect(runCase("inline", "", command).status).toBe(0);
});
it.each(["validate", "start"])("keeps the sub-agent empty gate at %s", (command) => {
expect(runCase("sub-agent", "", command).status).toBe(1);
});
it("accepts legacy seeds only in inline mode", () => {
const seed = JSON.stringify({ _example: "legacy" });
const result = runCase("inline", seed);
expect(result.status).toBe(0);
for (const name of ["implement", "check"]) {
expect(result.stdout).toContain(`${name}.jsonl: ✓ (0 entries)`);
}
for (const mode of ["sub-agent", "auto"]) {
expect(runCase(mode, seed).status).toBe(1);
}
});
describe.each(["inline", "sub-agent", "auto"])("mixed legacy markers in %s", (mode) => {
it.each([
{ file: "", reason: "" },
{ file: false, reason: "invalid" },
{ file: null, reason: "invalid" },
{ path: "", reason: "invalid" },
{ path: false, reason: "invalid" },
{ path: null, reason: "invalid" },
{ reason: "" },
{ type: "directory" },
{ unexpected: false },
{ file: ".trellis/spec/contract.md", reason: "runtime contract" },
])("rejects a marker combined with %j", (fields) => {
const result = runCase(mode, JSON.stringify({ _example: "legacy", ...fields }));
expect(result.status).toBe(1);
for (const name of ["implement", "check"]) {
expect(result.stdout).toContain(`${name}.jsonl:1: Placeholder \`_example\` row left by an older task.py create`);
}
});
});
it("does not let a pure seed hide an invalid following row", () => {
const content = [
{ _example: "legacy" },
{ file: "", reason: "" },
].map((entry) => JSON.stringify(entry)).join("\n");
const result = runCase("inline", content);
expect(result.status).toBe(1);
for (const name of ["implement", "check"]) {
expect(result.stdout).toContain(`${name}.jsonl:2: file must be a non-empty string path`);
}
});
it.each(["inline", "auto"])("accepts curated entries in %s", (mode) => {
const entry = JSON.stringify({ file: ".trellis/spec/contract.md", reason: "runtime contract" });
expect(runCase(mode, entry).status).toBe(0);
expect(runCase(mode, entry, "start").status).toBe(0);
});
it.each([
"{bad json",
"[]",
JSON.stringify({ file: ".trellis/spec/contract.md", reason: "" }),
JSON.stringify({ file: "", reason: "missing path" }),
JSON.stringify({ file: 42, reason: "wrong type" }),
JSON.stringify({ file: ".trellis/spec/missing.md", reason: "missing file" }),
])("rejects malformed inline input: %s", (content) => {
expect(runCase("inline", content).status).toBe(1);
});
it("rejects paths and symlinks outside the project", () => {
const outside = `${root}-outside.md`;
fs.writeFileSync(outside, "# Outside\n");
try {
fs.symlinkSync(outside, path.join(root, ".trellis/spec/outside.md"));
for (const file of [outside, `../${path.basename(outside)}`, ".trellis/spec/outside.md"]) {
expect(runCase("inline", JSON.stringify({ file, reason: "escape" })).status).toBe(1);
}
} finally {
fs.rmSync(outside, { force: true });
}
});
});
describe("Trellis workflow routing contract", () => {
it("loads consultation and approved-planning rules through the real hook parser", () => {
const runner = `
import importlib.util
from pathlib import Path
import sys
root = Path(sys.argv[1])
sys.path.insert(0, str(root / ".trellis/scripts"))
spec = importlib.util.spec_from_file_location("workflow_hook", root / ".codex/hooks/inject-workflow-state.py")
hook = importlib.util.module_from_spec(spec)
spec.loader.exec_module(hook)
templates = hook.load_breadcrumbs(root)
assert "without asking to create a task" in templates["no_task"]
key = hook.resolve_breadcrumb_key("planning", "codex", {"codex": {"dispatch_mode": "inline"}})
assert key == "planning-inline"
message = hook.build_breadcrumb("fixture", "planning", templates, breadcrumb_key=key)
assert "already recorded approval satisfies this gate" in message
assert "stay in planning" not in message
assert "task_error" in templates
assert "Do not create or activate another task" in templates["task_error"]
print("Routing contract passed")
`;
const result = spawnSync("python3", ["-c", runner, repoRoot], {
encoding: "utf8",
env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
timeout: 10000,
});
expect(result.stderr).not.toContain("Traceback");
expect(result.status).toBe(0);
});
it("preserves explicit commit and archival boundaries", () => {
const finish = fs.readFileSync(path.join(repoRoot, ".agents/skills/trellis-finish-work/SKILL.md"), "utf8");
expect(finish).toContain("Commit only when explicitly requested");
expect(finish).toContain("task is genuinely complete and archival is explicitly requested");
expect(finish).not.toContain("current active task is always archived");
});
});