// No tool and no agent creates a repository in an organisation (novox/hq issue 1124, ADR 0428 ยง8): the forge makes // whoever creates an organisation's repository its administrator, and an administrator can take its protection off. // So gitea_create_repo creates only under the tools' own user, with no owner argument; the tools' team keeps // can_create_org_repo false; and the tools' token has no write:organization. The organisation's repositories are // made by the module's setup login from its config value `repositories` (cmd/repositories). import { test, after } from "node:test"; import assert from "node:assert/strict"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { GiteaClient } from "../dist/client.js"; import { ConfiguredToken, TOKEN_SCOPES } from "../dist/token.js"; import { getGiteaTools } from "../dist/tools/index.js"; import { TOOLS_TEAM_UNITS } from "../dist/tools-account.js"; const sent: { method: string; path: string; body: any }[] = []; const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { const chunks: Buffer[] = []; for await (const c of req) chunks.push(c as Buffer); const raw = Buffer.concat(chunks).toString(); const path = new URL(req.url ?? "", "http://x").pathname.replace(/^\/api\/v1/, ""); const body = raw ? JSON.parse(raw) : undefined; sent.push({ method: req.method ?? "GET", path, body }); res.setHeader("Content-Type", "application/json"); if (req.method === "POST" && path === "/user/repos") { res.statusCode = 201; res.end(JSON.stringify({ full_name: `mesh-tools/${body.name}`, name: body.name, owner: { login: "mesh-tools" }, private: body.private, html_url: "", clone_url: "", default_branch: "main" })); return; } res.statusCode = 404; res.end(JSON.stringify({ message: "not found" })); }); await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); after(() => new Promise((r) => server.close(() => r()))); const { port } = server.address() as { port: number }; const client = new GiteaClient(`http://127.0.0.1:${port}`, new ConfiguredToken("t")); const tool = getGiteaTools(client).find((t) => t.name === "gitea_create_repo")!; test("gitea_create_repo has no owner argument and posts only to the tools' own user's repositories (1124/AC8)", async () => { assert.equal("owner" in tool.input, false, "the tool takes no owner"); assert.equal(GiteaClient.prototype.createRepo.length, 1, "createRepo takes the repository alone, no owner"); // An owner given anyway is not an argument of the tool: the repository is the tools' user's all the same. await tool.run({ owner: "novox", name: "mesh-delivery" }); await client.createRepo({ name: "x" }); assert.deepEqual(sent.map((s) => `${s.method} ${s.path}`), ["POST /user/repos", "POST /user/repos"]); assert.ok(sent.every((s) => !s.path.startsWith("/orgs/")), "nothing is sent to an organisation"); assert.doesNotMatch(tool.description, /organisation/i, "the description offers no organisation"); }); test("the tools' team cannot create in the organisation and its token holds no write:organization (1124/AC8)", () => { assert.equal(TOOLS_TEAM_UNITS.can_create_org_repo, false); assert.ok(!TOKEN_SCOPES.includes("write:organization"), `scopes ${TOKEN_SCOPES.join(", ")}`); assert.ok(TOKEN_SCOPES.every((s) => !/organization|admin/.test(s)), `scopes ${TOKEN_SCOPES.join(", ")}`); });