paperclip/server/src/__tests__/server-info.test.ts

204 lines
6.1 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import {
createServerInfoSnapshot,
getServerInfoSnapshot,
resetServerInfoCacheForTests,
} from "../server-info.js";
function gitCommandFor(shortSha: string, subject: string): () => string {
return () =>
[shortSha.padEnd(40, "0"), shortSha, subject, "2026-06-25T17:00:00-07:00"].join("\n");
}
describe("server info snapshot", () => {
it("captures process start time and git metadata", () => {
const snapshot = createServerInfoSnapshot({
now: new Date("2026-06-26T00:00:00.000Z"),
gitCommand: () =>
[
"0123456789abcdef0123456789abcdef01234567",
"0123456",
"Add server info debug view",
"2026-06-25T17:00:00-07:00",
].join("\n"),
gitBranchCommand: () => "feature/server-info\n",
gitStatusCommand: () => "",
});
expect(snapshot).toEqual({
processStartedAt: "2026-06-26T00:00:00.000Z",
git: {
available: true,
fullSha: "0123456789abcdef0123456789abcdef01234567",
shortSha: "0123456",
branchName: "feature/server-info",
subject: "Add server info debug view",
committedAt: "2026-06-26T00:00:00.000Z",
localChanges: {
available: true,
hasLocalChanges: false,
stagedFileCount: 0,
unstagedFileCount: 0,
untrackedFileCount: 0,
},
},
});
});
it("summarizes local checkout changes without exposing file paths", () => {
const snapshot = createServerInfoSnapshot({
now: new Date("2026-06-26T00:00:00.000Z"),
gitCommand: () =>
[
"0123456789abcdef0123456789abcdef01234567",
"0123456",
"Add server info debug view",
"2026-06-25T17:00:00-07:00",
].join("\n"),
gitStatusCommand: () =>
[
"M packages/shared/src/types/server-info.ts",
" M ui/src/components/SidebarServerInfo.tsx",
"MM server/src/server-info.ts",
"?? server/src/__tests__/server-info.test.ts",
].join("\n"),
});
expect(snapshot.git).toMatchObject({
available: true,
localChanges: {
available: true,
hasLocalChanges: true,
stagedFileCount: 2,
unstagedFileCount: 2,
untrackedFileCount: 1,
},
});
});
it("keeps commit metadata available when git status is unavailable", () => {
const snapshot = createServerInfoSnapshot({
now: new Date("2026-06-26T00:00:00.000Z"),
gitCommand: () =>
[
"0123456789abcdef0123456789abcdef01234567",
"0123456",
"Add server info debug view",
"2026-06-25T17:00:00-07:00",
].join("\n"),
gitStatusCommand: () => {
throw new Error("status unavailable");
},
});
expect(snapshot.git).toMatchObject({
available: true,
localChanges: {
available: false,
unavailableReason: "git_status_unavailable",
},
});
});
it("keeps commit metadata available when HEAD is detached", () => {
const snapshot = createServerInfoSnapshot({
now: new Date("2026-06-26T00:00:00.000Z"),
gitCommand: () =>
[
"0123456789abcdef0123456789abcdef01234567",
"0123456",
"Add server info debug view",
"2026-06-25T17:00:00-07:00",
].join("\n"),
gitBranchCommand: () => {
throw new Error("detached HEAD");
},
gitStatusCommand: () => "",
});
expect(snapshot.git).toMatchObject({
available: true,
branchName: null,
shortSha: "0123456",
});
});
it("uses sanitized fallback metadata when git is unavailable", () => {
const snapshot = createServerInfoSnapshot({
now: new Date("2026-06-26T00:00:00.000Z"),
gitCommand: () => {
throw new Error("fatal: not a git repository");
},
buildCommitCommand: () => null,
});
expect(snapshot).toEqual({
processStartedAt: "2026-06-26T00:00:00.000Z",
git: {
available: false,
unavailableReason: "git_unavailable",
},
});
});
it("uses deployment commit metadata when the runtime has no git directory", () => {
const snapshot = createServerInfoSnapshot({
now: new Date("2026-06-26T00:00:00.000Z"),
gitCommand: () => {
throw new Error("fatal: not a git repository");
},
buildCommitCommand: () => "0123456789abcdef0123456789abcdef01234567",
});
expect(snapshot).toEqual({
processStartedAt: "2026-06-26T00:00:00.000Z",
git: {
available: true,
fullSha: "0123456789abcdef0123456789abcdef01234567",
shortSha: "0123456",
branchName: null,
subject: "Source build",
committedAt: null,
localChanges: {
available: false,
unavailableReason: "git_status_unavailable",
},
},
});
});
});
describe("getServerInfoSnapshot", () => {
beforeEach(() => {
resetServerInfoCacheForTests();
});
it("re-reads the running commit after the cache TTL expires", () => {
const first = getServerInfoSnapshot({
now: 0,
gitCommand: gitCommandFor("aaaaaaa", "First boot"),
});
expect(first.git).toMatchObject({ shortSha: "aaaaaaa", subject: "First boot" });
// Within the TTL window the cached commit is reused.
const cached = getServerInfoSnapshot({
now: 1000,
gitCommand: gitCommandFor("bbbbbbb", "After restart"),
});
expect(cached.git).toMatchObject({ shortSha: "aaaaaaa", subject: "First boot" });
// Past the TTL the new HEAD is picked up without a process restart.
const refreshed = getServerInfoSnapshot({
now: 3000,
gitCommand: gitCommandFor("bbbbbbb", "After restart"),
});
expect(refreshed.git).toMatchObject({ shortSha: "bbbbbbb", subject: "After restart" });
});
it("keeps processStartedAt stable across refreshes", () => {
const first = getServerInfoSnapshot({ now: 0, gitCommand: gitCommandFor("aaaaaaa", "a") });
const second = getServerInfoSnapshot({ now: 5000, gitCommand: gitCommandFor("bbbbbbb", "b") });
expect(second.processStartedAt).toBe(first.processStartedAt);
});
});