Files
Piik/tests/release-update.test.ts
T
AAA 5781d6bbfa feat(client): complete native source and release boundary
Complete the Native Client source/audio boundary, strict v5 loopback contract, Browser media lifecycle handling, and non-blocking release checks. Keep unverified platform/physical gates explicit; do not rewrite the Go Server.
2026-09-04 03:42:54 +08:00

110 lines
3.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
checkReleaseUpdate,
DEFAULT_RELEASE_API_URL,
parseReleaseMetadata,
} from "../src/client/lib/release-update";
// The Node checker and the Browser checker deliberately share the same release
// contract while keeping their runtime boundaries independent.
// @ts-expect-error The executable helper has no generated TypeScript declaration.
import * as nodeRelease from "../scripts/release-update.mjs";
const currentRevision = "a".repeat(40);
const latestRevision = "b".repeat(40);
const release = {
tag_name: latestRevision,
html_url: `https://github.com/TNTcraftHIM/Screener/releases/tag/${latestRevision}`,
};
describe("Client release update notice", () => {
it("compares a strict release identity without sending the current revision", async () => {
const fetchImpl = vi.fn<typeof fetch>(async (input, init) => {
expect(String(input)).toBe(DEFAULT_RELEASE_API_URL);
expect(init?.credentials).toBe("omit");
expect(JSON.stringify(init)).not.toContain(currentRevision);
expect(JSON.stringify(init)).not.toContain("Authorization");
return new Response(JSON.stringify(release), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
await expect(
checkReleaseUpdate(currentRevision, { fetchImpl }),
).resolves.toEqual({
revision: latestRevision,
url: release.html_url,
});
expect(fetchImpl).toHaveBeenCalledTimes(1);
});
it("fails closed for unavailable, malformed, and current releases", async () => {
expect(parseReleaseMetadata({ ...release, tag_name: "short" })).toBeNull();
expect(parseReleaseMetadata({ ...release, html_url: "https://evil.example" })).toBeNull();
expect(
parseReleaseMetadata({
...release,
html_url: `https://user@github.com/TNTcraftHIM/Screener/releases/tag/${latestRevision}`,
}),
).toBeNull();
expect(parseReleaseMetadata({ ...release, draft: true })).toBeNull();
await expect(checkReleaseUpdate("development")).resolves.toBeNull();
await expect(
checkReleaseUpdate(latestRevision, {
fetchImpl: vi.fn<typeof fetch>(async () =>
new Response(JSON.stringify(release), { status: 200 }),
),
}),
).resolves.toBeNull();
await expect(
checkReleaseUpdate(currentRevision, {
fetchImpl: vi.fn<typeof fetch>(async () =>
new Response("offline", { status: 503 }),
),
}),
).resolves.toBeNull();
});
});
describe("operator release checker", () => {
it("uses the same full-SHA release contract and status values", async () => {
const fetchImpl = async () => ({
ok: true,
json: async () => release,
});
await expect(
nodeRelease.checkRelease({ currentRevision, fetchImpl }),
).resolves.toEqual({
status: "update-available",
currentRevision,
latestRevision,
releaseUrl: release.html_url,
});
await expect(
nodeRelease.checkRelease({ currentRevision: latestRevision, fetchImpl }),
).resolves.toMatchObject({ status: "up-to-date" });
expect(nodeRelease.UPDATE_EXIT_CODES).toEqual({
upToDate: 0,
available: 10,
unavailable: 20,
});
});
it("never forwards an operator token to a non-GitHub fixture endpoint", async () => {
let receivedHeaders: Record<string, string> | undefined;
const fetchImpl = async (_input: string, init?: RequestInit) => {
receivedHeaders = Object.fromEntries(new Headers(init?.headers));
return { ok: true, json: async () => release };
};
await nodeRelease.checkRelease({
currentRevision,
apiURL: "http://127.0.0.1:8787/releases/latest",
token: "fixture-secret",
fetchImpl,
});
expect(receivedHeaders?.authorization).toBeUndefined();
});
});