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