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> = {}): 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<string, unknown>): 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" });
});
});