g1t/apps/web/app/lib/audit.test.ts
| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { AuditEntry } from "@g1t/contracts"; |
| 5 | |
| 6 | import { |
| 7 | actorLabel, |
| 8 | exportName, |
| 9 | filterHref, |
| 10 | parseFilters, |
| 11 | retainedSince, |
| 12 | ruleLabel, |
| 13 | targetLabel, |
| 14 | toCsv, |
| 15 | toQuery, |
| 16 | visibilityFor, |
| 17 | } from "./audit.ts"; |
| 18 | |
| 19 | const entry: AuditEntry = { |
| 20 | id: "aud_1", |
| 21 | time: "2026-10-04T12:00:00.000Z", |
| 22 | actorKind: "agent", |
| 23 | actor: "g1t-agent", |
| 24 | actorId: "usr_g1t_agent", |
| 25 | agent: "g1t-agent", |
| 26 | onBehalfOf: "syntaqx", |
| 27 | runId: "run_1", |
| 28 | runKind: "implement", |
| 29 | credentialId: "tok_1", |
| 30 | action: "merge_pull_request", |
| 31 | surface: "mcp", |
| 32 | workspace: "acme", |
| 33 | repo: "acme/rocket", |
| 34 | number: 12, |
| 35 | gitRef: null, |
| 36 | path: null, |
| 37 | outcome: "denied", |
| 38 | rule: "never", |
| 39 | result: "forbidden", |
| 40 | message: 'A g1t agent\'s token can never use merge_pull_request: "merging" is for people, too.', |
| 41 | requestId: "8c1f", |
| 42 | }; |
| 43 | |
| 44 | test("owners see everything, members their projects, others nothing", () => { |
| 45 | assert.deepEqual(visibilityFor("owner", "ana"), { kind: "all" }); |
| 46 | assert.deepEqual(visibilityFor("member", "ana"), { kind: "projects", username: "ana" }); |
| 47 | assert.equal(visibilityFor(null, "ana"), null); |
| 48 | }); |
| 49 | |
| 50 | test("filters are read from the address, and nonsense is dropped", () => { |
| 51 | const filters = parseFilters( |
| 52 | new URLSearchParams("actor=syntaqx&outcome=maybe&kind=agent&from=2026-10-01&to=yesterday&project=rocket"), |
| 53 | ); |
| 54 | assert.equal(filters.actor, "syntaqx"); |
| 55 | assert.equal(filters.outcome, ""); |
| 56 | assert.equal(filters.kind, "agent"); |
| 57 | assert.equal(filters.from, "2026-10-01"); |
| 58 | assert.equal(filters.to, ""); |
| 59 | const query = toQuery("acme", { kind: "all" }, { ...filters, to: "2026-10-04" }, 100); |
| 60 | assert.equal(query.repo, "acme/rocket"); |
| 61 | assert.equal(query.since, "2026-10-01T00:00:00.000Z"); |
| 62 | // The end day is inclusive. |
| 63 | assert.equal(query.until, "2026-10-05T00:00:00.000Z"); |
| 64 | assert.equal(query.actorKind, "agent"); |
| 65 | assert.equal(query.outcome, null); |
| 66 | }); |
| 67 | |
| 68 | test("links keep the other filters", () => { |
| 69 | const filters = parseFilters(new URLSearchParams("actor=ana&outcome=denied")); |
| 70 | assert.equal(filterHref("/acme/-/audit", filters, { before: "aud_9" }), "/acme/-/audit?actor=ana&outcome=denied&before=aud_9"); |
| 71 | assert.equal(filterHref("/acme/-/audit", parseFilters(new URLSearchParams())), "/acme/-/audit"); |
| 72 | }); |
| 73 | |
| 74 | test("an agent is shown with whom it acted for", () => { |
| 75 | assert.equal(actorLabel(entry), "g1t-agent on behalf of syntaqx"); |
| 76 | assert.equal(actorLabel({ actor: "ana", agent: null, onBehalfOf: null }), "ana"); |
| 77 | assert.equal(targetLabel(entry), "acme/rocket#12"); |
| 78 | assert.equal(targetLabel({ ...entry, number: null, gitRef: "refs/heads/fix" }), "acme/rocket refs/heads/fix"); |
| 79 | assert.equal(ruleLabel("never"), "never allowed for agents"); |
| 80 | assert.equal(ruleLabel("run:implement/tools"), "implement run tools"); |
| 81 | assert.equal(ruleLabel("run:update/runner:push"), "update run runner (push)"); |
| 82 | }); |
| 83 | |
| 84 | test("the CSV quotes what it must and keeps formulas as text", () => { |
| 85 | const csv = toCsv([entry, { ...entry, id: "aud_2", path: "=HYPERLINK(1)", message: null }]); |
| 86 | const lines = csv.trimEnd().split("\r\n"); |
| 87 | assert.equal(lines.length, 3); |
| 88 | assert.ok(lines[0].startsWith("id,time,workspace,actorKind,actor")); |
| 89 | assert.ok(lines[1].includes('"A g1t agent\'s token can never use merge_pull_request: ""merging"" is for people, too."')); |
| 90 | assert.ok(lines[2].includes("'=HYPERLINK(1)")); |
| 91 | assert.equal(exportName("acme", "csv", new Date("2026-10-04T23:00:00Z")), "acme-audit-2026-10-04.csv"); |
| 92 | }); |
| 93 | |
| 94 | test("the log reads back only as far as the plan keeps it", () => { |
| 95 | const now = Date.parse("2026-10-31T00:00:00.000Z"); |
| 96 | // 90 days, on every plan. |
| 97 | assert.equal(retainedSince(null, 90, now), "2026-08-02T00:00:00.000Z"); |
| 98 | assert.equal(retainedSince(null, 30, now), "2026-10-01T00:00:00.000Z"); |
| 99 | assert.equal(retainedSince("2026-01-01T00:00:00.000Z", 30, now), "2026-10-01T00:00:00.000Z"); |
| 100 | // A later start than the window is kept. |
| 101 | assert.equal(retainedSince("2026-10-20T00:00:00.000Z", 30, now), "2026-10-20T00:00:00.000Z"); |
| 102 | // A longer window, where one is set. |
| 103 | assert.equal(retainedSince("2026-01-01T00:00:00.000Z", 365, now), "2026-01-01T00:00:00.000Z"); |
| 104 | }); |