import { afterEach, describe, expect, it, vi } from "vitest"; import { spindleUrl, toPipelineSummary } from "#lib/api/spindle.ts"; import type * as CiDefs from "#lib/api/lexicons/types/org/tangled/ci/defs.ts"; describe("spindleUrl", () => { afterEach(() => vi.unstubAllEnvs()); it("refuses hosts that name this machine or its network outside dev", () => { vi.stubEnv("DEV", false); for (const host of [ "localhost:6555", "127.0.0.1", "169.254.169.254", "[::1]", "2130706433" ]) { expect(() => spindleUrl(host), host).toThrow(); } }); it("gives bare hosts https and localhost plain http", () => { expect(spindleUrl("spindle.tangled.org")).toBe("https://spindle.tangled.org"); expect(spindleUrl("localhost:6555")).toBe("http://localhost:6555"); }); it("keeps an explicit scheme and drops the trailing slash", () => { expect(spindleUrl("http://spindle.test/")).toBe("http://spindle.test"); }); }); const workflow = (over: Partial = {}): CiDefs.WorkflowRun => ({ id: "build", name: "build", status: "success", ...over }) as CiDefs.WorkflowRun; // event.push and pullRequest are still empty in the lexicons, so triggers are loose const pipeline = (over: Record): CiDefs.Pipeline => ({ id: "3lz", commit: "9f2c1ab4d8e7350f6b1c2d9e4a5b6c7d8e9f0a1b", trigger: { $type: "org.tangled.ci.trigger.manual", repo: "did:plc:thisrepo", commit: "9f2c1ab4d8e7350f6b1c2d9e4a5b6c7d8e9f0a1b" }, workflows: [workflow()], ...over }) as unknown as CiDefs.Pipeline; describe("toPipelineSummary", () => { it("strips the ref prefix off a push trigger", () => { const summary = toPipelineSummary( pipeline({ trigger: { $type: "org.tangled.event.push", ref: "refs/heads/master", newSha: "a".repeat(40), oldSha: "b".repeat(40) } }) ); expect(summary.trigger).toEqual({ kind: "push", targetRef: "master" }); }); it("reads a pull request's target and source branches", () => { const summary = toPipelineSummary( pipeline({ trigger: { $type: "org.tangled.event.pullRequest", targetBranch: "master", sourceBranch: "fix-timeout", sourceSha: "c".repeat(40) } }) ); expect(summary.trigger).toEqual({ kind: "pull_request", targetRef: "master", sourceLabel: "fix-timeout" }); }); it("links a pull request run to its pull", () => { const summary = toPipelineSummary( pipeline({ trigger: { $type: "org.tangled.event.pullRequest", targetBranch: "main", pull: "at://did:plc:thisrepo/org.tangled.track.ticket/3mpull" } }), "did:plc:thisrepo" ); expect(summary.trigger).toEqual({ kind: "pull_request", targetRef: "main", sourceLabel: "", pullPath: "/did:plc:thisrepo/pulls/3mpull" }); }); it("doesn't link a pull that lives in another repo", () => { const summary = toPipelineSummary( pipeline({ trigger: { $type: "org.tangled.event.pullRequest", targetBranch: "main", pull: "at://did:plc:someoneelse/org.tangled.track.ticket/3mpull" } }), "did:plc:thisrepo" ); expect(summary.trigger).not.toHaveProperty("pullPath"); }); it("keeps a fork's repo did so the page can upgrade the label", () => { const summary = toPipelineSummary( pipeline({ trigger: { $type: "org.tangled.event.pullRequest", targetBranch: "master", sourceBranch: "fix-timeout", sourceSha: "c".repeat(40), sourceRepo: "did:plc:forkowner" } }), "did:plc:thisrepo" ); expect(summary.trigger).toEqual({ kind: "pull_request", targetRef: "master", sourceLabel: "fix-timeout", sourceRepo: "did:plc:forkowner" }); }); it("drops the did when the pull came from the same repo", () => { const summary = toPipelineSummary( pipeline({ trigger: { $type: "org.tangled.event.pullRequest", targetBranch: "master", sourceBranch: "fix-timeout", sourceSha: "c".repeat(40), sourceRepo: "did:plc:thisrepo" } }), "did:plc:thisrepo" ); expect(summary.trigger).toEqual({ kind: "pull_request", targetRef: "master", sourceLabel: "fix-timeout" }); }); it("times a workflow from its own start and finish stamps", () => { const summary = toPipelineSummary( pipeline({ workflows: [ workflow({ startedAt: "2026-08-04T09:00:00.000Z", finishedAt: "2026-08-04T09:01:12.000Z" }), workflow({ name: "test", status: "running", startedAt: "2026-08-04T09:00:00.000Z" }) ] }) ); expect(summary.workflows.map((w) => w.duration)).toEqual([72_000, 0]); }); }); describe("createSpindleClient", () => { const load = async () => { vi.resetModules(); vi.doMock("$app/env", () => ({ browser: false, dev: false })); return import("#lib/api/spindle.ts"); }; it("never sends a request to a spindle in the private network", async () => { const { createSpindleClient, queryPipelines } = await load(); const fetch = vi.fn(async () => Response.json({ pipelines: [] })); for (const spindle of ["http://169.254.169.254", "http://10.0.0.5:9000", "localhost:6555"]) await expect( queryPipelines(createSpindleClient(spindle, fetch), { repo: "did:plc:alice" }) ).rejects.toThrow(); expect(fetch).not.toHaveBeenCalled(); }); it("reaches a public spindle without following redirects", async () => { const { createSpindleClient, queryPipelines } = await load(); const fetch = vi.fn(async () => Response.json({ pipelines: [] })); await queryPipelines(createSpindleClient("did:web:spindle.example.com", fetch), { repo: "did:plc:alice" }); expect(fetch).toHaveBeenCalledTimes(1); const [url, init] = fetch.mock.calls[0] as unknown as [URL, RequestInit]; expect(String(url)).toMatch(/^https:\/\/spindle\.example\.com\/xrpc\//); expect(init).toMatchObject({ redirect: "error" }); }); });