import { describe, expect, it } from "vitest"; import { decodeLogFrame, pipelineLogsUrl } from "./spindleLogs"; // captured off the spindle's own serializer: CiSubscribePipelineLogs_Event // .Serialize in api/tangled/cisubscribePipelineLogs_ext.go, hex-dumped const frames = { controlStart: "a261746823636f6e74726f6c626f7001a7646b696e6464757365726473746570016474696d6574323032362d30382d30365431323a30303a30305a6673746174757365737461727467636f6d6d616e64676563686f20686967636f6e74656e746952756e20746573747368776f726b666c6f77656275696c64", controlMinimal: "a261746823636f6e74726f6c626f7001a46473746570016474696d6574323032362d30382d30365431323a30303a30395a67636f6e74656e746952756e20746573747368776f726b666c6f77656275696c64", data: "a26174652364617461626f7001a56473746570016474696d6574323032362d30382d30365431323a30303a30315a6673747265616d6673746465727267636f6e74656e746e1b5b33316d626f6f6d1b5b306d0a68776f726b666c6f77656275696c64", error: "a1626f7020a2656572726f7270576f726b666c6f774e6f74466f756e64676d657373616765706e6f207375636820776f726b666c6f77" } as const; const bytes = (hex: string): Uint8Array => Uint8Array.from(hex.match(/../g)!.map((pair) => parseInt(pair, 16))); describe("decodeLogFrame", () => { it("reads a step start with its command and kind", () => { expect(decodeLogFrame(bytes(frames.controlStart))).toEqual({ kind: "control", control: { time: "2026-08-06T12:00:00Z", workflow: "build", step: 1, content: "Run tests", command: "echo hi", status: "start", kind: "user" } }); }); it("leaves the optional control fields off when the spindle omits them", () => { const frame = decodeLogFrame(bytes(frames.controlMinimal)); expect(frame).toEqual({ kind: "control", control: { time: "2026-08-06T12:00:09Z", workflow: "build", step: 1, content: "Run tests" } }); }); it("keeps a data line's ansi bytes and trailing newline intact", () => { expect(decodeLogFrame(bytes(frames.data))).toEqual({ kind: "data", data: { time: "2026-08-06T12:00:01Z", workflow: "build", step: 1, content: "\u001b[31mboom\u001b[0m\n", stream: "stderr" } }); }); it("surfaces the stream's error frame", () => { expect(decodeLogFrame(bytes(frames.error))).toEqual({ kind: "error", error: "WorkflowNotFound", message: "no such workflow" }); }); it("ignores a message type it doesn't know", () => { // same header shape, unknown t const unknown = "a2617464236e6577626f7001a0"; expect(decodeLogFrame(bytes(unknown))).toBeNull(); }); }); describe("pipelineLogsUrl", () => { it("upgrades the spindle's scheme and repeats the workflow param", () => { expect(pipelineLogsUrl("spindle.tangled.org", "3lz", ["build", "test"])).toBe( "wss://spindle.tangled.org/xrpc/sh.tangled.ci.subscribePipelineLogs?pipeline=3lz&workflows=build&workflows=test" ); }); it("stays plaintext for a local spindle", () => { expect(pipelineLogsUrl("localhost:6555", "3lz", ["build"])).toBe( "ws://localhost:6555/xrpc/sh.tangled.ci.subscribePipelineLogs?pipeline=3lz&workflows=build" ); }); });