atagen

roundabout

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git
tests/omp.test.mjsJavascript107 lines4.8 KB
1
import assert from "node:assert/strict";
2
import { spawnSync } from "node:child_process";
3
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
import { tmpdir } from "node:os";
5
import { join } from "node:path";
6
import { after, beforeEach, test } from "node:test";
7
import roundabout from "../plugin/omp/index.js";
8
9
const temporary = mkdtempSync(join(tmpdir(), "roundabout-omp-"));
10
const captured = join(temporary, "input.json");
11
const mockCli = process.env.ROUNDABOUT_TEST_CLI;
12
const handlers = new Map();
13
const notifications = [];
14
const context = {
15
  cwd: temporary,
16
  hasUI: true,
17
  agent: { kind: "main" },
18
  sessionManager: { getSessionId: () => "omp-session" },
19
  ui: { notify: (...args) => notifications.push(args) },
20
};
21
const bash = { toolName: "bash", input: { command: "cargo build", env: { KEEP: "yes" }, async: true } };
22
23
roundabout({ on: (name, handler) => handlers.set(name, handler) });
24
25
beforeEach(() => {
26
  process.env.ROUNDABOUT_CLI = mockCli;
27
  process.env.ROUNDABOUT_TEST_INPUT = captured;
28
  delete process.env.ROUNDABOUT_TEST_OUTPUT;
29
  delete process.env.ROUNDABOUT_TEST_EXIT;
30
  notifications.length = 0;
31
  rmSync(captured, { force: true });
32
});
33
34
after(() => rmSync(temporary, { recursive: true, force: true }));
35
36
function output(value) {
37
  process.env.ROUNDABOUT_TEST_OUTPUT = JSON.stringify(value);
38
}
39
40
function input() {
41
  return JSON.parse(readFileSync(captured, "utf8"));
42
}
43
44
test("maps build guards and preserves wrapped command arguments", async () => {
45
  output({ hookSpecificOutput: { permissionDecision: "deny", permissionDecisionReason: "use roundabout run" } });
46
  assert.deepEqual(await handlers.get("tool_call")(bash, context), { block: true, reason: "use roundabout run" });
47
  assert.equal(input().event, "omp-pre-tool-use");
48
  assert.equal(input().input.tool_name, "Bash");
49
  output({ hookSpecificOutput: { updatedInput: { command: "roundabout run -s m -- cargo build" } } });
50
  const result = await handlers.get("tool_call")({ ...bash, input: { ...bash.input, cwd: "/project" } }, context);
51
  assert.equal(input().input.cwd, "/project");
52
  assert.equal(result.input.command, "roundabout run -s m -- cargo build");
53
  assert.equal(result.input.async, true);
54
  assert.deepEqual(result.input.env, { KEEP: "yes", ROUNDABOUT_SESSION_ID: "omp-session" });
55
});
56
57
test("fails open for malformed output, nonzero exits and missing CLI", async () => {
58
  process.env.ROUNDABOUT_TEST_OUTPUT = "not JSON";
59
  assert.equal((await handlers.get("tool_call")(bash, context)).input.command, "cargo build");
60
  output({ hookSpecificOutput: { permissionDecision: "deny" } });
61
  process.env.ROUNDABOUT_TEST_EXIT = "1";
62
  assert.equal((await handlers.get("tool_call")(bash, context)).block, undefined);
63
  process.env.ROUNDABOUT_CLI = join(temporary, "missing-roundabout");
64
  assert.equal((await handlers.get("tool_call")(bash, context)).block, undefined);
65
});
66
67
test("maps artifact policies and bounds advisory continuations", async () => {
68
  output({ decision: "block", reason: "clean artifacts" });
69
  assert.deepEqual(await handlers.get("session_stop")({ stop_hook_active: true }, context), {
70
    decision: "block", reason: "clean artifacts",
71
  });
72
  output({ hookSpecificOutput: { additionalContext: "clean artifacts" } });
73
  assert.deepEqual(await handlers.get("session_stop")({ stop_hook_active: false }, context), {
74
    continue: true, additionalContext: "clean artifacts",
75
  });
76
  assert.equal(await handlers.get("session_stop")({ stop_hook_active: true }, context), undefined);
77
  output({ systemMessage: "over cap" });
78
  await handlers.get("session_stop")({ stop_hook_active: false }, context);
79
  assert.deepEqual(notifications, [["over cap", "warning"]]);
80
});
81
82
test("the CLI tracks OMP sessions ahead of inherited agent identities", () => {
83
  const roundabout = process.env.ROUNDABOUT_TEST_RABT;
84
  const config = join(temporary, "config.toml");
85
  writeFileSync(config, '[queue]\nlinger = "1s"\n');
86
  const env = {
87
    ...process.env,
88
    ROUNDABOUT_CONFIG: config,
89
    ROUNDABOUT_STATE_DIR: join(temporary, "state"),
90
    ROUNDABOUT_RUNTIME_DIR: join(temporary, "run"),
91
    ROUNDABOUT_SESSION_ID: "omp-session",
92
    CLAUDE_CODE_SESSION_ID: "inherited-claude",
93
    CODEX_THREAD_ID: "inherited-codex",
94
  };
95
  function run(args, payload) {
96
    const result = spawnSync(roundabout, args, { env, input: payload, encoding: "utf8", timeout: 10000 });
97
    assert.equal(result.status, 0, result.stderr);
98
    return result.stdout;
99
  }
100
  run(["expect", "-s", "s"]);
101
  assert.equal(JSON.parse(run(["status", "--json"])).intents[0].session, "omp-session");
102
  run(["hook", "omp-session-end"], JSON.stringify({ session_id: "omp-session" }));
103
  assert.equal(JSON.parse(run(["status", "--json"])).intents.length, 0);
104
  run(["expect", "--session", "explicit", "-s", "s"]);
105
  assert.equal(JSON.parse(run(["status", "--json"])).intents[0].session, "explicit");
106
  run(["expect", "--session", "explicit", "--done"]);
107
});