// // Units and edge cases: remote URL parsing, diff arithmetic, time formatting, // preference precedence, HTTP error surfacing, pagination, and the unconfigured // and legacy-server paths. // const { EXTENSION_ROOT } = require("./nova-stub.js"); const { PR_DIFF } = require("./fake-gitea.js"); const { Checker } = require("./assert.js"); const path = require("node:path"); const script = (...parts) => path.join(EXTENSION_ROOT, "Scripts", ...parts); const t = new Checker(); /** Swaps in a fetch implementation for the duration of `body`. */ async function withResponder(responder, body) { const saved = global.fetch; global.fetch = responder; try { return await body(); } finally { global.fetch = saved; } } const ok = (payload) => ({ ok: true, status: 200, text: async () => JSON.stringify(payload) }); const fail = (status, message) => ({ ok: false, status, text: async () => JSON.stringify({ message }), }); (async () => { t.section("remote URL parsing"); const remotes = require(script("gitea", "remotes.js")); const parsed = (host, owner, name, port = null) => ({ host, port, owner, name }); t.equals("https", remotes.parse("https://gitea.com/acme/widget.git"), parsed("gitea.com", "acme", "widget")); t.equals("https without .git", remotes.parse("https://gitea.com/acme/widget"), parsed("gitea.com", "acme", "widget")); t.equals( "https with credentials", remotes.parse("https://user:pass@git.example.com/acme/widget.git"), parsed("git.example.com", "acme", "widget"), ); t.equals( "https with port", remotes.parse("https://git.example.com:3000/acme/widget.git"), parsed("git.example.com", "acme", "widget", 3000), ); t.equals("scp-style ssh", remotes.parse("git@gitea.com:acme/widget.git"), parsed("gitea.com", "acme", "widget")); t.equals( "ssh:// with port", remotes.parse("ssh://git@git.example.com:2222/acme/widget.git"), parsed("git.example.com", "acme", "widget", 2222), ); t.equals( "instance served from a sub-path", remotes.parse("https://example.com/gitea/acme/widget.git"), parsed("example.com", "acme", "widget"), ); t.equals("host lower-cased", remotes.parse("git@GITEA.COM:Acme/Widget.git").host, "gitea.com"); t.equals("garbage rejected", remotes.parse("not a url"), null); t.equals("empty rejected", remotes.parse(""), null); t.equals("hostOf drops the port", remotes.hostOf("https://git.example.com:3000"), "git.example.com"); t.equals("hostOf on a plain URL", remotes.hostOf("https://gitea.com"), "gitea.com"); t.section("diff hunk to file line"); const review = require(script("controllers", "review.js")); t.equals("additions only", review.lineFromDiffHunk("@@ -0,0 +1,2 @@\n+first\n+second", null), 2); t.equals("removed lines do not advance the new file", review.lineFromDiffHunk("@@ -10,4 +10,7 @@\n ctx\n-gone\n+a\n+b", null), 12); t.equals("single-line hunk header", review.lineFromDiffHunk("@@ -5 +5 @@\n+changed", null), 5); t.equals("no hunk falls back to position", review.lineFromDiffHunk(null, 42), 42); t.equals("unparseable hunk falls back", review.lineFromDiffHunk("no header here", 7), 7); t.section("per-file diff extraction"); const pulls = require(script("controllers", "pullRequests.js")); const first = pulls.extractFileDiff(PR_DIFF, "src/login.js"); t.check( "slices out only the first file", first.startsWith("diff --git a/src/login.js") && !first.includes("src/new.js"), JSON.stringify(first?.slice(0, 60)), ); const last = pulls.extractFileDiff(PR_DIFF, "src/new.js"); t.check("slices out the last file", last.startsWith("diff --git a/src/new.js") && last.includes("noop")); t.equals("unknown file yields null", pulls.extractFileDiff(PR_DIFF, "nope.js"), null); t.section("time formatting"); const time = require(script("util", "time.js")); t.equals("Go zero time renders blank", time.relative("0001-01-01T00:00:00Z"), ""); t.equals("null renders blank", time.relative(null), ""); t.check("a past time reads as ago", time.relative(new Date(Date.now() - 7200e3).toISOString()).endsWith("ago")); t.equals("minutes and seconds", time.duration("2026-08-28T10:00:00Z", "2026-08-28T10:01:23Z"), "1m 23s"); t.equals("hours and minutes", time.duration("2026-08-28T10:00:00Z", "2026-08-28T12:30:00Z"), "2h 30m"); t.check( "a zero end time still yields a duration", typeof time.duration("2026-08-28T10:00:00Z", "0001-01-01T00:00:00Z") === "string", ); t.section("preference precedence"); const config = require(script("util", "config.js")); nova.config.set("gitea.baseUrl", "https://global.example.com"); t.equals("global value used", config.get("gitea.baseUrl"), "https://global.example.com"); nova.workspace.config.set("gitea.baseUrl", "https://workspace.example.com"); t.equals("workspace overrides global", config.get("gitea.baseUrl"), "https://workspace.example.com"); nova.workspace.config.remove("gitea.baseUrl"); t.equals("falls back to global", config.get("gitea.baseUrl"), "https://global.example.com"); nova.config.remove("gitea.baseUrl"); t.equals("falls back to the built-in default", config.get("gitea.baseUrl"), "https://gitea.com"); t.equals("boolean default when unset", config.bool("gitea.reviewComments.enabled"), true); nova.config.set("gitea.reviewComments.enabled", false); t.equals("an explicit false is respected", config.bool("gitea.reviewComments.enabled"), false); nova.config.remove("gitea.reviewComments.enabled"); t.equals("trailing slashes stripped", config.normalizeBaseUrl("https://gitea.com///"), "https://gitea.com"); t.equals("missing scheme filled in", config.normalizeBaseUrl("gitea.example.com"), "https://gitea.example.com"); t.equals("blank rejected", config.normalizeBaseUrl(" "), null); nova.config.set("gitea.baseUrl", "https://a.example.com"); nova.config.set("gitea.instances", ["https://b.example.com/", "https://a.example.com", "https://b.example.com"]); t.equals("instances de-duplicated, default first", config.instances(), [ "https://a.example.com", "https://b.example.com", ]); t.section("HTTP error surfacing"); const { GiteaClient, GiteaError } = require(script("gitea", "client.js")); const { GiteaApi } = require(script("gitea", "api.js")); const api = new GiteaApi(new GiteaClient("https://gitea.test", () => "tok")); const repo = { owner: "acme", name: "widget" }; await withResponder(async () => fail(401, "token is invalid"), async () => { try { await api.currentUser(); t.check("401 throws", false); } catch (error) { t.check("401 raises a GiteaError", error instanceof GiteaError); t.check("401 flagged as an auth failure", error.isAuth === true); t.check("401 tells the user what to do", error.message.includes("Set a valid token"), error.message); } }); await withResponder(async () => fail(403, "missing scope"), async () => { try { await api.listSecrets(repo); t.check("403 throws", false); } catch (error) { t.check("403 names the cause", error.message.includes("scope"), error.message); t.check("403 keeps the server's own detail", error.message.includes("missing scope"), error.message); } }); t.section("older Gitea servers"); await withResponder( async (url) => { if (url.includes("/actions/runs")) return fail(404, "Not Found"); if (url.includes("/actions/tasks")) { return ok({ total_count: 1, workflow_runs: [{ id: 1, name: "legacy" }] }); } return fail(404, "Not Found"); }, async () => { const result = await api.listRuns(repo, { limit: 10 }); t.check( "runs fall back to /actions/tasks", result.legacy === true && result.runs.length === 1, JSON.stringify(result), ); }, ); await withResponder(async () => fail(404, "Not Found"), async () => { t.equals("a missing artifacts endpoint yields an empty list", await api.listArtifacts(repo, 1), []); }); t.section("query building and pagination"); const paged = []; await withResponder( async (url) => { paged.push(url); const page = Number(new URL(url).searchParams.get("page")); return ok( page === 1 ? Array.from({ length: 50 }, (_, i) => ({ number: i + 1 })) : Array.from({ length: 5 }, (_, i) => ({ number: 51 + i })), ); }, async () => { t.equals("collects across pages", (await api.listPullRequests(repo, { limit: 60 })).length, 55); t.check("page and limit sent", paged[0].includes("page=1") && paged[0].includes("limit=50"), paged[0]); t.check( "state and sort sent", paged[0].includes("state=open") && paged[0].includes("sort=recentupdate"), paged[0], ); t.check("stops after a short page", paged.length === 2, String(paged.length)); }, ); const built = []; await withResponder( async (url) => { built.push(url); return ok({ workflow_runs: [] }); }, async () => { await api.listRuns(repo, { limit: 20, branch: null, event: null }); t.check( "null query parameters omitted entirely", !built[0].includes("branch=") && !built[0].includes("event=") && built[0].includes("limit=20"), built[0], ); }, ); const escaped = []; await withResponder( async (url) => { escaped.push(url); return ok({}); }, async () => { await api.repository({ owner: "my org", name: "a+b" }); t.check("path segments percent-encoded", escaped[0].includes("/repos/my%20org/a%2Bb"), escaped[0]); }, ); t.section("nothing configured"); const { Store } = require(script("store.js")); nova.config.set("gitea.baseUrl", "https://untokened.example.com"); nova.config.set("gitea.instances", []); const store = new Store(); t.check("store reports no token", store.hasAnyToken === false); await store.refresh({ rediscover: true }); store.stop(); const status = store.instanceStatus.get("https://untokened.example.com"); t.check("instance marked as needing a token", status?.needsToken === true, JSON.stringify(status)); const { BranchProvider } = require(script("views", "branchView.js")); const rows = await new BranchProvider(store).getChildren(null); t.check( "the sidebar explains how to sign in", rows.length === 1 && rows[0].name.includes("Set Token"), rows.map((row) => row.name).join(" | "), ); store.dispose(); t.finish("UNIT CHECKS"); })().catch((error) => { console.error("HARNESS ERROR", error); process.exit(2); });