| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | ABILITY_LEVELS, |
| 6 | COMPUTER_ABILITIES, |
| 7 | G1T_ABILITIES, |
| 8 | abilitiesSummary, |
| 9 | allAbilities, |
| 10 | askedFor, |
| 11 | autonomyOfLevel, |
| 12 | checkMcpName, |
| 13 | checkMcpUrl, |
| 14 | connectorsWithAbilities, |
| 15 | defaultLevel, |
| 16 | findAbility, |
| 17 | integrationAbilities, |
| 18 | levelOfAutonomy, |
| 19 | maxLevel, |
| 20 | mcpAbility, |
| 21 | mcpToolName, |
| 22 | resolveAbilities, |
| 23 | withSetting, |
| 24 | withinLevel, |
| 25 | } from "./abilities.ts"; |
| 26 | import { CONNECTORS } from "./connectors.ts"; |
| 27 | |
| 28 | const AUTONOMY = { open_pull_requests: "alone", merge: "approval", deploy_production: "approval", edit_docs: "suggest" } as const; |
| 29 | const resolve = (over: Partial<Parameters<typeof resolveAbilities>[0]> = {}) => resolveAbilities({ connectors: CONNECTORS, abilities: null, autonomy: AUTONOMY, connected: [], ...over }); |
| 30 | |
| 31 | test("defaults by kind: reads alone, writes inside g1t when asked for, sending outside asks first, restricted never and no freer than asking", () => { |
| 32 | assert.equal(defaultLevel("read"), "alone"); |
| 33 | assert.equal(defaultLevel("write"), "asked"); |
| 34 | assert.equal(defaultLevel("send"), "ask"); |
| 35 | assert.equal(defaultLevel("restricted"), "never"); |
| 36 | assert.equal(maxLevel("restricted"), "ask"); |
| 37 | assert.equal(maxLevel("send"), "alone"); |
| 38 | assert.ok(withinLevel("never", "ask")); |
| 39 | assert.ok(!withinLevel("alone", "ask")); |
| 40 | assert.deepEqual(ABILITY_LEVELS, ["alone", "asked", "ask", "never"]); |
| 41 | }); |
| 42 | |
| 43 | test("every integration ability belongs to an available workspace connector with a provider, and its kind fits the connector's capabilities", () => { |
| 44 | const withAbilities = connectorsWithAbilities(CONNECTORS); |
| 45 | assert.ok(withAbilities.length >= 3, "Linear, Jira and Sentry at least"); |
| 46 | for (const connector of withAbilities) { |
| 47 | assert.equal(connector.status, "available", `${connector.id} is connectable today`); |
| 48 | assert.ok(connector.scopes.includes("workspace"), `${connector.id} connects for a workspace`); |
| 49 | assert.ok(connector.provider, `${connector.id} has a provider the integrations service knows`); |
| 50 | const defs = integrationAbilities(connector.id); |
| 51 | const caps = connector.capabilities ?? []; |
| 52 | if (defs.some((d) => d.kind === "read")) assert.ok(caps.includes("Agents can read"), `${connector.id} says agents can read`); |
| 53 | if (defs.some((d) => d.kind === "send")) assert.ok(caps.includes("Writes back"), `${connector.id} says it writes back`); |
| 54 | for (const def of defs) { |
| 55 | assert.equal(def.id, `integration:${connector.id}:${def.id.split(":")[2]}`); |
| 56 | assert.equal(def.tools.length, 1, "one tool per integration ability"); |
| 57 | } |
| 58 | } |
| 59 | assert.deepEqual(integrationAbilities("datadog"), [], "Datadog opens issues by itself: nothing for an agent to call"); |
| 60 | }); |
| 61 | |
| 62 | test("g1t's own are always on and the four autonomy ones keep their choices as levels", () => { |
| 63 | const sections = resolve(); |
| 64 | const g1t = sections.find((s) => s.group === "g1t")!; |
| 65 | const artifacts = g1t.sources[0]!.abilities.find((a) => a.id === "g1t:artifacts")!; |
| 66 | assert.equal(artifacts.level, "alone"); |
| 67 | assert.equal(artifacts.can_change, false); |
| 68 | const merge = g1t.sources[0]!.abilities.find((a) => a.id === "g1t:merge")!; |
| 69 | assert.equal(merge.level, "ask", "approval reads as Ask first"); |
| 70 | assert.deepEqual(merge.choices, ["alone", "ask", "never"]); |
| 71 | const deploy = g1t.sources[0]!.abilities.find((a) => a.id === "g1t:deploy")!; |
| 72 | assert.equal(deploy.kind, "restricted"); |
| 73 | assert.ok(!deploy.choices.includes("alone"), "production deploys never go alone"); |
| 74 | assert.equal(levelOfAutonomy("suggest"), "ask"); |
| 75 | assert.equal(autonomyOfLevel("edit_docs", "ask"), "suggest"); |
| 76 | assert.equal(autonomyOfLevel("merge", "ask"), "approval"); |
| 77 | assert.equal(autonomyOfLevel("merge", "never"), "never"); |
| 78 | assert.equal(G1T_ABILITIES.filter((d) => d.autonomy).length, 4); |
| 79 | const computer = sections.find((s) => s.group === "computer")!; |
| 80 | const shell = computer.sources[0]!.abilities.find((a) => a.id === "computer:shell")!; |
| 81 | assert.equal(shell.status, "ready"); |
| 82 | assert.equal(shell.level, "asked", "commands run when asked for, until the person says otherwise"); |
| 83 | assert.deepEqual(shell.choices, ["alone", "asked", "ask", "never"]); |
| 84 | const files = computer.sources[0]!.abilities.find((a) => a.id === "computer:files")!; |
| 85 | assert.equal(files.level, "alone"); |
| 86 | assert.ok(files.can_change); |
| 87 | const coming = computer.sources[0]!.abilities.filter((a) => a.id === "computer:browser" || a.id === "computer:web"); |
| 88 | assert.equal(coming.length, 2); |
| 89 | assert.ok(coming.every((a) => a.status === "coming" && a.choices.length === 0 && !a.can_change), "the browser and web search are still coming"); |
| 90 | assert.equal(COMPUTER_ABILITIES.length, 4); |
| 91 | }); |
| 92 | |
| 93 | test("a connected integration lists its rows at their defaults; one that isn't is listed, but nothing there can be changed", () => { |
| 94 | const sections = resolve({ connected: ["linear"] }); |
| 95 | const integrations = sections.find((s) => s.group === "integration")!; |
| 96 | const linear = integrations.sources.find((s) => s.id === "linear")!; |
| 97 | assert.equal(linear.connected, true); |
| 98 | assert.deepEqual( |
| 99 | linear.abilities.map((a) => [a.id, a.level, a.credentials, a.can_change]), |
| 100 | [ |
| 101 | ["integration:linear:read", "alone", "workspace", true], |
| 102 | ["integration:linear:import", "asked", "workspace", true], |
| 103 | ["integration:linear:comment", "ask", "workspace", true], |
| 104 | ], |
| 105 | ); |
| 106 | assert.equal(linear.abilities[0]!.personal_available, false, "Linear's personal side is still coming"); |
| 107 | const jira = integrations.sources.find((s) => s.id === "jira")!; |
| 108 | assert.equal(jira.connected, false); |
| 109 | assert.ok(jira.abilities.every((a) => !a.can_change)); |
| 110 | assert.equal(integrations.sources[0]!.id, "linear", "connected first"); |
| 111 | assert.ok(!integrations.sources.some((s) => s.id === "webhooks" || s.id === "anthropic"), "nothing to call, not connected: not listed"); |
| 112 | const withDatadog = resolve({ connected: ["datadog"] }).find((s) => s.group === "integration")!; |
| 113 | const datadog = withDatadog.sources.find((s) => s.id === "datadog")!; |
| 114 | assert.equal(datadog.abilities.length, 0); |
| 115 | assert.match(datadog.note ?? "", /Opens issues by itself/); |
| 116 | }); |
| 117 | |
| 118 | test("a setting moves a level within its kind's limit, and is dropped when it is the default again", () => { |
| 119 | let abilities = withSetting(null, "integration:linear:comment", { level: "alone" }); |
| 120 | assert.deepEqual(abilities.settings, { "integration:linear:comment": { level: "alone" } }); |
| 121 | const sections = resolve({ connected: ["linear"], abilities }); |
| 122 | assert.equal(findAbility(sections, "integration:linear:comment")!.ability.level, "alone"); |
| 123 | abilities = withSetting(abilities, "integration:linear:comment", { credentials: "asker" }); |
| 124 | assert.deepEqual(abilities.settings["integration:linear:comment"], { level: "alone", credentials: "asker" }); |
| 125 | abilities = withSetting(abilities, "integration:linear:comment", { level: null, credentials: null }); |
| 126 | assert.deepEqual(abilities.settings, {}, "nothing kept once everything is the default"); |
| 127 | // A restricted ability set freer than Ask is held at Ask. |
| 128 | const held = resolveAbilities({ connectors: CONNECTORS, abilities: null, autonomy: { ...AUTONOMY, deploy_production: "approval" }, connected: [] }); |
| 129 | assert.equal(findAbility(held, "g1t:deploy")!.ability.max, "ask"); |
| 130 | }); |
| 131 | |
| 132 | test("a personal agent runs on the asker's connections unless an owner chose the workspace's", () => { |
| 133 | const sections = resolve({ connected: ["linear"], personal: true }); |
| 134 | assert.equal(findAbility(sections, "integration:linear:read")!.ability.credentials, "asker"); |
| 135 | const chosen = resolve({ connected: ["linear"], personal: true, abilities: withSetting(null, "integration:linear:read", { credentials: "workspace" }) }); |
| 136 | assert.equal(findAbility(chosen, "integration:linear:read")!.ability.credentials, "workspace"); |
| 137 | }); |
| 138 | |
| 139 | test("MCP servers: each listed tool is a row, a write unless the server says it reads, offered under <server>__<tool>", () => { |
| 140 | const server = { |
| 141 | id: "mcp_1", |
| 142 | name: "weather", |
| 143 | url: "https://mcp.example.com/", |
| 144 | tools: [ |
| 145 | { name: "get_forecast", description: "Today's forecast", kind: "read" as const, input_schema: { type: "object", properties: {} } }, |
| 146 | { name: "set_alert", description: "", kind: "write" as const, input_schema: { type: "object", properties: {} } }, |
| 147 | ], |
| 148 | added_by: "ana", |
| 149 | added_at: "2026-10-10T00:00:00.000Z", |
| 150 | checked_at: null, |
| 151 | problem: null, |
| 152 | }; |
| 153 | const sections = resolve({ abilities: { settings: {}, mcp_servers: [server] } }); |
| 154 | const mcp = sections.find((s) => s.group === "mcp")!; |
| 155 | assert.equal(mcp.sources.length, 1); |
| 156 | const [read, write] = mcp.sources[0]!.abilities; |
| 157 | assert.equal(read!.level, "alone"); |
| 158 | assert.equal(write!.level, "ask", "a write outside g1t asks first"); |
| 159 | assert.equal(write!.kind, "send"); |
| 160 | assert.deepEqual(read!.tools, ["weather__get_forecast"]); |
| 161 | assert.equal(mcpToolName("My Server", "do.thing"), "my_server__do_thing"); |
| 162 | assert.equal(mcpAbility(server, server.tools[1]!).id, "mcp:mcp_1:set_alert"); |
| 163 | }); |
| 164 | |
| 165 | test("an MCP server's address is HTTPS on a public host, never local, an address or g1t's own", () => { |
| 166 | assert.equal(checkMcpUrl("https://mcp.example.com/sse").ok, true); |
| 167 | assert.equal(checkMcpUrl("http://mcp.example.com/").ok, false); |
| 168 | assert.equal(checkMcpUrl("https://localhost:3000/").ok, false); |
| 169 | assert.equal(checkMcpUrl("https://10.0.0.5/").ok, false); |
| 170 | assert.equal(checkMcpUrl("https://[::1]/").ok, false); |
| 171 | assert.equal(checkMcpUrl("https://mcp.internal/").ok, false); |
| 172 | assert.equal(checkMcpUrl("https://api.g1t.sh/mcp").ok, false); |
| 173 | assert.equal(checkMcpUrl("https://user:pw@mcp.example.com/").ok, false); |
| 174 | assert.equal(checkMcpUrl("not a url").ok, false); |
| 175 | assert.equal(checkMcpName("Weather").ok, true); |
| 176 | assert.equal(checkMcpName("a").ok, false); |
| 177 | assert.equal(checkMcpName("bad name").ok, false); |
| 178 | }); |
| 179 | |
| 180 | test("alone when asked for it: the item's key or the ability's name in what the person said", () => { |
| 181 | assert.ok(askedFor("Can you comment on ENG-42 with the test plan?", ["ENG-42", "comment"])); |
| 182 | assert.ok(askedFor("what's eng-42 about", ["ENG-42"]), "any case"); |
| 183 | assert.ok(!askedFor("Summarise the thread", ["ENG-42", "import"])); |
| 184 | assert.ok(!askedFor("", ["ENG-42"])); |
| 185 | }); |
| 186 | |
| 187 | test("the summary says what it does alone, when asked, after asking, and never", () => { |
| 188 | const abilities = withSetting(withSetting(null, "integration:linear:comment", { level: "never" }), "integration:sentry:resolve", { level: "alone" }); |
| 189 | const sections = resolve({ connected: ["linear", "sentry"], abilities }); |
| 190 | const summary = abilitiesSummary(sections); |
| 191 | assert.equal( |
| 192 | summary, |
| 193 | "Can open pull requests, change files on its computer, read issues in Linear, read issues in Sentry and resolve issues in Sentry on its own; runs commands on its computer, imports issues in Linear and imports issues in Sentry when asked for it; asks before merging, deploying to production, editing docs and commenting in Sentry; never comments in Linear.", |
| 194 | ); |
| 195 | assert.equal(allAbilities(sections).filter((a) => a.group === "integration" && a.can_change).length, 7); |
| 196 | assert.match(abilitiesSummary(resolve()), /open pull requests and change files on its computer on its own/); |
| 197 | }); |