atagen

roundabout

public
Star0
Watch0
7770b8fcfa8b72ae913db787fd5ef641f31bd302
1
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git

Blame: tests/omp.test.mjs

4e5ca2e20401atagen2026-09-28 11:42 UTC1
import assert from "node:assert/strict";
4e5ca2e20401atagen2026-09-28 11:42 UTC2
import { spawnSync } from "node:child_process";
4e5ca2e20401atagen2026-09-28 11:42 UTC3
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4e5ca2e20401atagen2026-09-28 11:42 UTC4
import { tmpdir } from "node:os";
4e5ca2e20401atagen2026-09-28 11:42 UTC5
import { join } from "node:path";
4e5ca2e20401atagen2026-09-28 11:42 UTC6
import { after, beforeEach, test } from "node:test";
4e5ca2e20401atagen2026-09-28 11:42 UTC7
import roundabout from "../plugin/omp/index.js";
4e5ca2e20401atagen2026-09-28 11:42 UTC8
4e5ca2e20401atagen2026-09-28 11:42 UTC9
const temporary = mkdtempSync(join(tmpdir(), "roundabout-omp-"));
4e5ca2e20401atagen2026-09-28 11:42 UTC10
const captured = join(temporary, "input.json");
4e5ca2e20401atagen2026-09-28 11:42 UTC11
const mockCli = process.env.ROUNDABOUT_TEST_CLI;
4e5ca2e20401atagen2026-09-28 11:42 UTC12
const handlers = new Map();
4e5ca2e20401atagen2026-09-28 11:42 UTC13
const notifications = [];
4e5ca2e20401atagen2026-09-28 11:42 UTC14
const context = {
4e5ca2e20401atagen2026-09-28 11:42 UTC15
  cwd: temporary,
4e5ca2e20401atagen2026-09-28 11:42 UTC16
  hasUI: true,
4e5ca2e20401atagen2026-09-28 11:42 UTC17
  agent: { kind: "main" },
4e5ca2e20401atagen2026-09-28 11:42 UTC18
  sessionManager: { getSessionId: () => "omp-session" },
4e5ca2e20401atagen2026-09-28 11:42 UTC19
  ui: { notify: (...args) => notifications.push(args) },
4e5ca2e20401atagen2026-09-28 11:42 UTC20
};
4e5ca2e20401atagen2026-09-28 11:42 UTC21
const bash = { toolName: "bash", input: { command: "cargo build", env: { KEEP: "yes" }, async: true } };
4e5ca2e20401atagen2026-09-28 11:42 UTC22
4e5ca2e20401atagen2026-09-28 11:42 UTC23
roundabout({ on: (name, handler) => handlers.set(name, handler) });
4e5ca2e20401atagen2026-09-28 11:42 UTC24
4e5ca2e20401atagen2026-09-28 11:42 UTC25
beforeEach(() => {
4e5ca2e20401atagen2026-09-28 11:42 UTC26
  process.env.ROUNDABOUT_CLI = mockCli;
4e5ca2e20401atagen2026-09-28 11:42 UTC27
  process.env.ROUNDABOUT_TEST_INPUT = captured;
4e5ca2e20401atagen2026-09-28 11:42 UTC28
  delete process.env.ROUNDABOUT_TEST_OUTPUT;
4e5ca2e20401atagen2026-09-28 11:42 UTC29
  delete process.env.ROUNDABOUT_TEST_EXIT;
4e5ca2e20401atagen2026-09-28 11:42 UTC30
  notifications.length = 0;
4e5ca2e20401atagen2026-09-28 11:42 UTC31
  rmSync(captured, { force: true });
4e5ca2e20401atagen2026-09-28 11:42 UTC32
});
4e5ca2e20401atagen2026-09-28 11:42 UTC33
4e5ca2e20401atagen2026-09-28 11:42 UTC34
after(() => rmSync(temporary, { recursive: true, force: true }));
4e5ca2e20401atagen2026-09-28 11:42 UTC35
4e5ca2e20401atagen2026-09-28 11:42 UTC36
function output(value) {
4e5ca2e20401atagen2026-09-28 11:42 UTC37
  process.env.ROUNDABOUT_TEST_OUTPUT = JSON.stringify(value);
4e5ca2e20401atagen2026-09-28 11:42 UTC38
}
4e5ca2e20401atagen2026-09-28 11:42 UTC39
4e5ca2e20401atagen2026-09-28 11:42 UTC40
function input() {
4e5ca2e20401atagen2026-09-28 11:42 UTC41
  return JSON.parse(readFileSync(captured, "utf8"));
4e5ca2e20401atagen2026-09-28 11:42 UTC42
}
4e5ca2e20401atagen2026-09-28 11:42 UTC43
4e5ca2e20401atagen2026-09-28 11:42 UTC44
test("maps build guards and preserves wrapped command arguments", async () => {
4e5ca2e20401atagen2026-09-28 11:42 UTC45
  output({ hookSpecificOutput: { permissionDecision: "deny", permissionDecisionReason: "use rabt run" } });
4e5ca2e20401atagen2026-09-28 11:42 UTC46
  assert.deepEqual(await handlers.get("tool_call")(bash, context), { block: true, reason: "use rabt run" });
4e5ca2e20401atagen2026-09-28 11:42 UTC47
  assert.equal(input().event, "omp-pre-tool-use");
4e5ca2e20401atagen2026-09-28 11:42 UTC48
  assert.equal(input().input.tool_name, "Bash");
4e5ca2e20401atagen2026-09-28 11:42 UTC49
  output({ hookSpecificOutput: { updatedInput: { command: "rabt run -s m -- cargo build" } } });
4e5ca2e20401atagen2026-09-28 11:42 UTC50
  const result = await handlers.get("tool_call")({ ...bash, input: { ...bash.input, cwd: "/project" } }, context);
4e5ca2e20401atagen2026-09-28 11:42 UTC51
  assert.equal(input().input.cwd, "/project");
4e5ca2e20401atagen2026-09-28 11:42 UTC52
  assert.equal(result.input.command, "rabt run -s m -- cargo build");
4e5ca2e20401atagen2026-09-28 11:42 UTC53
  assert.equal(result.input.async, true);
4e5ca2e20401atagen2026-09-28 11:42 UTC54
  assert.deepEqual(result.input.env, { KEEP: "yes", ROUNDABOUT_SESSION_ID: "omp-session" });
4e5ca2e20401atagen2026-09-28 11:42 UTC55
});
4e5ca2e20401atagen2026-09-28 11:42 UTC56
4e5ca2e20401atagen2026-09-28 11:42 UTC57
test("fails open for malformed output, nonzero exits and missing CLI", async () => {
4e5ca2e20401atagen2026-09-28 11:42 UTC58
  process.env.ROUNDABOUT_TEST_OUTPUT = "not JSON";
4e5ca2e20401atagen2026-09-28 11:42 UTC59
  assert.equal((await handlers.get("tool_call")(bash, context)).input.command, "cargo build");
4e5ca2e20401atagen2026-09-28 11:42 UTC60
  output({ hookSpecificOutput: { permissionDecision: "deny" } });
4e5ca2e20401atagen2026-09-28 11:42 UTC61
  process.env.ROUNDABOUT_TEST_EXIT = "1";
4e5ca2e20401atagen2026-09-28 11:42 UTC62
  assert.equal((await handlers.get("tool_call")(bash, context)).block, undefined);
4e5ca2e20401atagen2026-09-28 11:42 UTC63
  process.env.ROUNDABOUT_CLI = join(temporary, "missing-rabt");
4e5ca2e20401atagen2026-09-28 11:42 UTC64
  assert.equal((await handlers.get("tool_call")(bash, context)).block, undefined);
4e5ca2e20401atagen2026-09-28 11:42 UTC65
});
4e5ca2e20401atagen2026-09-28 11:42 UTC66
4e5ca2e20401atagen2026-09-28 11:42 UTC67
test("maps artifact policies and bounds advisory continuations", async () => {
4e5ca2e20401atagen2026-09-28 11:42 UTC68
  output({ decision: "block", reason: "clean artifacts" });
4e5ca2e20401atagen2026-09-28 11:42 UTC69
  assert.deepEqual(await handlers.get("session_stop")({ stop_hook_active: true }, context), {
4e5ca2e20401atagen2026-09-28 11:42 UTC70
    decision: "block", reason: "clean artifacts",
4e5ca2e20401atagen2026-09-28 11:42 UTC71
  });
4e5ca2e20401atagen2026-09-28 11:42 UTC72
  output({ hookSpecificOutput: { additionalContext: "clean artifacts" } });
4e5ca2e20401atagen2026-09-28 11:42 UTC73
  assert.deepEqual(await handlers.get("session_stop")({ stop_hook_active: false }, context), {
4e5ca2e20401atagen2026-09-28 11:42 UTC74
    continue: true, additionalContext: "clean artifacts",
4e5ca2e20401atagen2026-09-28 11:42 UTC75
  });
4e5ca2e20401atagen2026-09-28 11:42 UTC76
  assert.equal(await handlers.get("session_stop")({ stop_hook_active: true }, context), undefined);
4e5ca2e20401atagen2026-09-28 11:42 UTC77
  output({ systemMessage: "over cap" });
4e5ca2e20401atagen2026-09-28 11:42 UTC78
  await handlers.get("session_stop")({ stop_hook_active: false }, context);
4e5ca2e20401atagen2026-09-28 11:42 UTC79
  assert.deepEqual(notifications, [["over cap", "warning"]]);
4e5ca2e20401atagen2026-09-28 11:42 UTC80
});
4e5ca2e20401atagen2026-09-28 11:42 UTC81
4e5ca2e20401atagen2026-09-28 11:42 UTC82
test("the CLI tracks OMP sessions ahead of inherited agent identities", () => {
4e5ca2e20401atagen2026-09-28 11:42 UTC83
  const rabt = process.env.ROUNDABOUT_TEST_RABT;
4e5ca2e20401atagen2026-09-28 11:42 UTC84
  const config = join(temporary, "config.toml");
4e5ca2e20401atagen2026-09-28 11:42 UTC85
  writeFileSync(config, '[queue]\nlinger = "1s"\n');
4e5ca2e20401atagen2026-09-28 11:42 UTC86
  const env = {
4e5ca2e20401atagen2026-09-28 11:42 UTC87
    ...process.env,
4e5ca2e20401atagen2026-09-28 11:42 UTC88
    ROUNDABOUT_CONFIG: config,
4e5ca2e20401atagen2026-09-28 11:42 UTC89
    ROUNDABOUT_STATE_DIR: join(temporary, "state"),
4e5ca2e20401atagen2026-09-28 11:42 UTC90
    ROUNDABOUT_RUNTIME_DIR: join(temporary, "run"),
4e5ca2e20401atagen2026-09-28 11:42 UTC91
    ROUNDABOUT_SESSION_ID: "omp-session",
4e5ca2e20401atagen2026-09-28 11:42 UTC92
    CLAUDE_CODE_SESSION_ID: "inherited-claude",
4e5ca2e20401atagen2026-09-28 11:42 UTC93
    CODEX_THREAD_ID: "inherited-codex",
4e5ca2e20401atagen2026-09-28 11:42 UTC94
  };
4e5ca2e20401atagen2026-09-28 11:42 UTC95
  function run(args, payload) {
4e5ca2e20401atagen2026-09-28 11:42 UTC96
    const result = spawnSync(rabt, args, { env, input: payload, encoding: "utf8", timeout: 10000 });
4e5ca2e20401atagen2026-09-28 11:42 UTC97
    assert.equal(result.status, 0, result.stderr);
4e5ca2e20401atagen2026-09-28 11:42 UTC98
    return result.stdout;
4e5ca2e20401atagen2026-09-28 11:42 UTC99
  }
4e5ca2e20401atagen2026-09-28 11:42 UTC100
  run(["expect", "-s", "s"]);
4e5ca2e20401atagen2026-09-28 11:42 UTC101
  assert.equal(JSON.parse(run(["status", "--json"])).intents[0].session, "omp-session");
4e5ca2e20401atagen2026-09-28 11:42 UTC102
  run(["hook", "omp-session-end"], JSON.stringify({ session_id: "omp-session" }));
4e5ca2e20401atagen2026-09-28 11:42 UTC103
  assert.equal(JSON.parse(run(["status", "--json"])).intents.length, 0);
4e5ca2e20401atagen2026-09-28 11:42 UTC104
  run(["expect", "--session", "explicit", "-s", "s"]);
4e5ca2e20401atagen2026-09-28 11:42 UTC105
  assert.equal(JSON.parse(run(["status", "--json"])).intents[0].session, "explicit");
4e5ca2e20401atagen2026-09-28 11:42 UTC106
  run(["expect", "--session", "explicit", "--done"]);
4e5ca2e20401atagen2026-09-28 11:42 UTC107
});