Files
mesh-catalog/modules/gitea/test/commit-trees.test.ts
T
jschoubben 75957fc3c0
mesh/repo-check head 75957fc3c006 merged into trunk e8c3448797c7 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/merge-gate head 75957fc3c006 merged into trunk e8c3448797c7 — pass: group feat/delivery-parts composed: every machine composes with the change as it d…
mesh/delivery covered after its failure: a later walk that contains it was delivered: plan-1792433168676870614 (novox/mesh-catalog at eda55ceb into main,…
mesh/delivery-group group feat/delivery-parts delivered: every member is delivered
Hash the root tree from the root's entries, since the forge names the commit (issue 1234)
The listing's top sha is the commit's, not the root tree's (seen live, novox/mesh-delivery at 7f61817), so
gitea_commit_trees now computes the root tree as git does, from the entries without a slash. The query keys
recursive and per_page stay in the allow-list: read flags only.
2026-10-19 14:52:29 +02:00

174 lines
8.2 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { createServer } from "node:http";
// Fixtures follow Gitea's GitTreeResponse as its API documents it (swagger: sha, url, tree[{path, mode, type, size,
// sha, url}], truncated, page, total_count) and as the forge answers GET /repos/{o}/{r}/git/trees/{sha}; the entry
// fields are those, with sha values written out. The live capture (1234/AC6): GET
// /repos/novox/mesh-delivery/git/trees/7f61817 (non-recursive) answered the COMMIT's sha at the top, not the root
// tree's; the root tree is computed from the root's own entries, as git does.
import { createHash } from "node:crypto";
const COMMIT = "c".repeat(40);
const h = (c: string) => c.repeat(40);
const entry = (path: string, type: "tree" | "blob", sha: string) => ({
path, mode: type === "tree" ? "040000" : "100644", type, size: type === "tree" ? 0 : 12, sha, url: `u/${sha}`,
});
const LISTING = [
entry("README.md", "blob", h("1")),
entry("module.json", "blob", h("2")),
entry("modules", "tree", h("3")),
entry("modules/a", "tree", h("4")),
entry("modules/a/module.json", "blob", h("5")),
entry("modules/a/main.go", "blob", h("6")),
entry("modules/b", "tree", h("7")),
entry("modules/b/c", "tree", h("8")),
entry("modules/b/c/module.json", "blob", h("9")),
entry("modules/d", "tree", h("a")),
entry("modules/d/notes.md", "blob", h("b")),
];
/** git's own hash of a tree from its entries (an independent restatement for the fixtures): mode without leading zero. */
function gitTree(es: { path: string; type: string; sha: string; mode: string }[]): string {
const ordered = es.map((e) => ({ ...e, key: e.path + (e.type === "tree" ? "/" : "") })).sort((a, b) => Buffer.compare(Buffer.from(a.key), Buffer.from(b.key)));
const body = Buffer.concat(ordered.map((e) => Buffer.concat([Buffer.from(`${e.mode.replace(/^0+/, "")} ${e.path}\0`), Buffer.from(e.sha, "hex")])));
return createHash("sha1").update(Buffer.concat([Buffer.from(`tree ${body.length}\0`), body])).digest("hex");
}
const ROOT = gitTree(LISTING.filter((e) => !e.path.includes("/")));
async function serve(pages: any[]) {
const { GiteaClient } = await import("../client.ts");
const asked: string[] = [];
const server = createServer((req, res) => {
asked.push(req.url ?? "");
const url = new URL(req.url ?? "", "http://x");
if (url.pathname !== `/api/v1/repos/novox/hq/git/trees/${COMMIT}` || url.searchParams.get("recursive") !== "true") {
res.statusCode = 404;
return res.end("{}");
}
const page = Number(url.searchParams.get("page") ?? 1);
res.end(JSON.stringify(pages[page - 1] ?? { sha: COMMIT, tree: [], truncated: false, page, total_count: 0 }));
});
await new Promise<void>((r) => server.listen(0, r));
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
return { client, server, asked };
}
test("a commit's trees: the root and the tree of each directory holding a module.json", async () => {
// 1234/AC1 and 1234/AC4
const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
try {
const got = await client.commitTrees("novox", "hq", COMMIT);
assert.deepEqual(got, { root: ROOT, trees: { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") } });
} finally {
server.close();
}
});
test("a listing the forge pages is read page by page", async () => {
// 1234/AC1
const { client, server, asked } = await serve([
{ sha: COMMIT, tree: LISTING.slice(0, 5), truncated: true, page: 1, total_count: LISTING.length },
{ sha: COMMIT, tree: LISTING.slice(5, 9), truncated: true, page: 2, total_count: LISTING.length },
{ sha: COMMIT, tree: LISTING.slice(9), truncated: false, page: 3, total_count: LISTING.length },
]);
try {
const got = await client.commitTrees("novox", "hq", COMMIT);
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
assert.equal(asked.length, 3, `asked ${asked.join(" ")}`);
} finally {
server.close();
}
});
test("a listing the forge says is cut fails, naming the repository and the commit, and answers no trees", async () => {
// 1234/AC2
const { client, server } = await serve([{ sha: COMMIT, tree: LISTING, truncated: true, page: 1, total_count: LISTING.length }]);
try {
await assert.rejects(client.commitTrees("novox", "hq", COMMIT), (err: Error) => {
assert.match(err.message, /novox\/hq/);
assert.ok(err.message.includes(COMMIT), err.message);
assert.match(err.message, /cut/);
return true;
});
} finally {
server.close();
}
});
test("a sha that is not a full commit, or no owner or repo, is refused before the forge is asked", async () => {
// 1234/AC3
const { getGiteaTools } = await import("../tools/index.ts");
const { client, server, asked } = await serve([]);
try {
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
assert.ok(tool, "the tool is listed");
for (const args of [
{ owner: "novox", repo: "hq", sha: "abc1234" },
{ owner: "novox", repo: "hq", sha: "main" },
{ owner: "novox", repo: "hq", sha: "" },
{ owner: "novox", repo: "hq", sha: "c".repeat(41) },
{ owner: "", repo: "hq", sha: COMMIT },
{ owner: "novox", repo: " ", sha: COMMIT },
]) await assert.rejects(tool.run(args as any), /./, JSON.stringify(args));
assert.equal(asked.length, 0, `the forge was asked ${asked.join(" ")}`);
} finally {
server.close();
}
});
test("gitea_open_pulls says each pull request's head branch beside its head sha", async () => {
// 1234/AC5
const { GiteaClient } = await import("../client.ts");
const server = createServer((req, res) => {
const url = req.url ?? "";
const send = (v: unknown) => res.end(JSON.stringify(v));
if (url.startsWith("/api/v1/repos/issues/search")) {
return send(url.includes("page=1") ? [{ number: 7, repository: { owner: "novox", name: "hq", full_name: "novox/hq" } }] : []);
}
if (url.startsWith("/api/v1/repos/novox/hq/pulls/7")) {
return send({ number: 7, state: "open", base: { ref: "main" }, head: { ref: "feat/x", sha: "aaa1111" }, html_url: "h" });
}
if (url.includes("/branch_protections")) { res.statusCode = 403; return res.end("{}"); }
if (url.startsWith("/api/v1/repos/novox/hq/branches/main")) return send({ name: "main" });
if (url.includes("/commits/aaa1111/status")) return send({ statuses: [] });
res.statusCode = 404;
res.end("{}");
});
await new Promise<void>((r) => server.listen(0, r));
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
try {
const [p] = await client.openPulls();
assert.deepEqual(p, { owner: "novox", repo: "hq", number: 7, base: "main", head: "feat/x", head_sha: "aaa1111", html_url: "h",
required: [], statuses: [], checks: {}, labels: [] });
} finally {
server.close();
}
});
test("the root tree is git's hash of the root's own entries, never the listing's top sha (the commit's)", async () => {
// 1234/AC6, from the live capture named in the header: novox/mesh-delivery at 7f61817.
const commit = "7f618177d6773d8d2d2b9e0b10e13b85ec700c59";
const listing = [
entry("README.md", "blob", "a087d22cbcbecce42256315671ff7e4d6ca85a7c"),
entry("merge-check.sh", "blob", "c4ef9a3dd6390b0edde7e4137095553897cecfb0"),
entry("modules", "tree", "31c0abb312ee6ce4693a7aacdd8b3fba871d6cf3"),
entry("modules/x", "tree", h("d")),
entry("modules/x/module.json", "blob", h("e")),
];
const { GiteaClient } = await import("../client.ts");
const server = createServer((req, res) => {
res.end(JSON.stringify(new URL(req.url ?? "", "http://x").pathname.endsWith(`/git/trees/${commit}`)
? { sha: commit, tree: listing, truncated: false, page: 1, total_count: listing.length } : {}));
});
await new Promise<void>((r) => server.listen(0, r));
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
try {
const got = await client.commitTrees("novox", "mesh-delivery", commit);
assert.equal(got.root, "659bcaaac6c16972ec380267a4eba69d15558c26");
assert.deepEqual(got.trees, { "modules/x": h("d") });
} finally {
server.close();
}
});