Files
openchamber/packages/ui/src/stores/useGitHubPrStatusStore.test.ts
T
Cursor Agentandserkraser 13cbbc76d7 test(github): cover persist, hydrate, and complete open-list misses
Add store persist/hydrate regressions for terminal branch associations
and a server test that a complete empty open list does not query closed
PRs. Inline the open-only matcher state so the next repo target can win.

Co-authored-by: serkraser <serkraser@gmail.com>
2026-08-15 04:27:52 +00:00

544 lines
21 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
import type { GitHubPullRequestStatus, RuntimeAPIs } from "@/lib/api/types"
let runtimeKey = "runtime-a"
mock.module("@/lib/runtime-switch", () => ({ getRuntimeKey: () => runtimeKey }))
const { getGitHubPrStatusKey, useGitHubPrStatusStore } = await import("./useGitHubPrStatusStore")
const deferred = <T>() => {
let resolve!: (value: T) => void
const promise = new Promise<T>((res) => { resolve = res })
return { promise, resolve }
}
const params = (github: RuntimeAPIs["github"], branch = "main") => ({
directory: "/repo",
branch,
remoteName: "origin",
canShow: true,
github,
githubAuthChecked: true,
githubConnected: true,
})
describe("GitHub PR status cache ownership", () => {
beforeEach(() => {
runtimeKey = "runtime-a"
useGitHubPrStatusStore.setState({ entries: {}, activeRequestCount: 0, totalRequestCount: 0 })
useGitHubPrStatusStore.getState().resetForRuntimeSwitch()
})
test("keys colliding paths by runtime and requested remote", () => {
const originA = getGitHubPrStatusKey("/repo", "main", "origin")
const upstreamA = getGitHubPrStatusKey("/repo", "main", "upstream")
runtimeKey = "runtime-b"
const originB = getGitHubPrStatusKey("/repo", "main", "origin")
expect(new Set([originA, upstreamA, originB]).size).toBe(3)
})
test("rejects a response after params change", async () => {
const request = deferred<GitHubPullRequestStatus>()
const github = { prStatus: () => request.promise } as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "main", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github))
const loading = useGitHubPrStatusStore.getState().refresh(key, { force: true })
useGitHubPrStatusStore.getState().setParams(key, params(github, "next"))
request.resolve({ connected: true, pr: null })
await loading
expect(useGitHubPrStatusStore.getState().entries[key]?.status).toBe(null)
expect(useGitHubPrStatusStore.getState().entries[key]?.isLoading).toBe(false)
})
test("rejects an old runtime response after reset", async () => {
const request = deferred<GitHubPullRequestStatus>()
const github = { prStatus: () => request.promise } as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "main", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github))
const loading = useGitHubPrStatusStore.getState().refresh(key, { force: true })
runtimeKey = "runtime-b"
useGitHubPrStatusStore.getState().resetForRuntimeSwitch()
request.resolve({ connected: true, pr: null })
await loading
expect(useGitHubPrStatusStore.getState().entries[key]?.status).toBe(null)
expect(useGitHubPrStatusStore.getState().activeRequestCount).toBe(0)
})
test("throttles repeated non-forced refreshes after a failure", async () => {
let requestCount = 0
const github = {
prStatus: async () => {
requestCount += 1
throw new Error("GitHub rate limited")
},
} as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "main", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github))
await useGitHubPrStatusStore.getState().refresh(key)
await useGitHubPrStatusStore.getState().refresh(key)
expect(requestCount).toBe(1)
expect(useGitHubPrStatusStore.getState().entries[key]?.error).toBe("GitHub rate limited")
})
test("does not throttle replacement params when a queued request becomes stale", async () => {
const first = deferred<GitHubPullRequestStatus>()
const second = deferred<GitHubPullRequestStatus>()
let staleRequestCount = 0
let replacementRequestCount = 0
const firstGitHub = { prStatus: () => first.promise } as unknown as RuntimeAPIs["github"]
const secondGitHub = { prStatus: () => second.promise } as unknown as RuntimeAPIs["github"]
const staleGitHub = {
prStatus: async () => {
staleRequestCount += 1
return { connected: true, pr: null }
},
} as unknown as RuntimeAPIs["github"]
const replacementGitHub = {
prStatus: async () => {
replacementRequestCount += 1
return { connected: true, pr: null }
},
} as unknown as RuntimeAPIs["github"]
const firstKey = getGitHubPrStatusKey("/repo", "first", "origin")
const secondKey = getGitHubPrStatusKey("/repo", "second", "origin")
const queuedKey = getGitHubPrStatusKey("/repo", "queued", "origin")
for (const [key, github, branch] of [
[firstKey, firstGitHub, "first"],
[secondKey, secondGitHub, "second"],
[queuedKey, staleGitHub, "queued"],
] as const) {
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github, branch))
}
const firstRefresh = useGitHubPrStatusStore.getState().refresh(firstKey, { force: true })
const secondRefresh = useGitHubPrStatusStore.getState().refresh(secondKey, { force: true })
const staleRefresh = useGitHubPrStatusStore.getState().refresh(queuedKey, { force: true })
await Promise.resolve()
useGitHubPrStatusStore.getState().setParams(queuedKey, params(replacementGitHub, "queued"))
first.resolve({ connected: true, pr: null })
second.resolve({ connected: true, pr: null })
await Promise.all([firstRefresh, secondRefresh, staleRefresh])
await useGitHubPrStatusStore.getState().refresh(queuedKey)
expect(staleRequestCount).toBe(0)
expect(replacementRequestCount).toBe(1)
})
test("rejects a server-cached response older than the held status", async () => {
const newer: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 2_000,
pr: { number: 7, title: "t", url: "u", state: "open", draft: false, base: "main", head: "f" },
checks: { state: "pending", total: 3, success: 2, failure: 0, pending: 1 },
}
const older: GitHubPullRequestStatus = {
...newer,
fetchedAt: 1_000,
checks: { state: "success", total: 3, success: 3, failure: 0, pending: 0 },
}
const responses = [newer, older]
const github = { prStatus: async () => responses.shift()! } as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "main", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github))
await useGitHubPrStatusStore.getState().refresh(key, { force: true })
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.checks?.pending).toBe(1)
await useGitHubPrStatusStore.getState().refresh(key, { force: true })
const held = useGitHubPrStatusStore.getState().entries[key]?.status
expect(held?.fetchedAt).toBe(2_000)
expect(held?.checks?.pending).toBe(1)
expect(useGitHubPrStatusStore.getState().entries[key]?.isLoading).toBe(false)
})
})
describe("GitHub PR status stale terminal associations", () => {
const originalSetInterval = globalThis.setInterval
const originalSetTimeout = globalThis.setTimeout
const originalClearInterval = globalThis.clearInterval
const originalClearTimeout = globalThis.clearTimeout
let intervalCallbacks: Array<() => void> = []
beforeEach(() => {
runtimeKey = "runtime-a"
intervalCallbacks = []
const setIntervalStub = ((handler: TimerHandler) => {
if (typeof handler === "function") {
intervalCallbacks.push(handler as () => void)
}
return 1
}) as unknown as typeof setInterval
const setTimeoutStub = (() => 1) as unknown as typeof setTimeout
const clearIntervalStub = (() => undefined) as typeof clearInterval
const clearTimeoutStub = (() => undefined) as typeof clearTimeout
globalThis.setInterval = setIntervalStub
globalThis.setTimeout = setTimeoutStub
globalThis.clearInterval = clearIntervalStub
globalThis.clearTimeout = clearTimeoutStub
// bun:test has no DOM; the store uses window timers and optional document visibility.
Object.assign(globalThis, {
window: {
setInterval: setIntervalStub,
setTimeout: setTimeoutStub,
clearInterval: clearIntervalStub,
clearTimeout: clearTimeoutStub,
},
document: { visibilityState: "visible" },
})
useGitHubPrStatusStore.setState({ entries: {}, activeRequestCount: 0, totalRequestCount: 0 })
useGitHubPrStatusStore.getState().resetForRuntimeSwitch()
})
afterEach(() => {
useGitHubPrStatusStore.getState().resetForRuntimeSwitch()
globalThis.setInterval = originalSetInterval
globalThis.setTimeout = originalSetTimeout
globalThis.clearInterval = originalClearInterval
globalThis.clearTimeout = originalClearTimeout
delete (globalThis as { window?: unknown }).window
delete (globalThis as { document?: unknown }).document
})
test("forced refresh replaces a merged PR with a newer open PR", async () => {
const merged: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
}
const newerOpen: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 2_000,
pr: { number: 15, title: "new", url: "u15", state: "open", draft: false, base: "main", head: "feature" },
}
let requestCount = 0
const github = {
prStatus: async () => {
requestCount += 1
return requestCount === 1 ? merged : newerOpen
},
} as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github, "feature"))
await useGitHubPrStatusStore.getState().refresh(key, { force: true })
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number).toBe(12)
await useGitHubPrStatusStore.getState().refresh(key, { force: true })
expect(requestCount).toBe(2)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number).toBe(15)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.state).toBe("open")
})
test("forced refresh clears a merged PR when no open PR remains", async () => {
const merged: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
repo: { owner: "acme", repo: "app", url: "https://github.com/acme/app" },
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
}
const empty: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 2_000,
repo: { owner: "acme", repo: "app", url: "https://github.com/acme/app" },
pr: null,
}
const github = {
prStatus: async () => empty,
} as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.setState((state) => ({
entries: {
...state.entries,
[key]: {
...state.entries[key]!,
status: merged,
isInitialStatusResolved: true,
lastRefreshAt: Date.now(),
},
},
}))
useGitHubPrStatusStore.getState().setParams(key, params(github, "feature"))
await useGitHubPrStatusStore.getState().refresh(key, { force: true })
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr).toBeNull()
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.repo).toEqual({
owner: "acme",
repo: "app",
url: "https://github.com/acme/app",
})
})
test("watcher discovery revalidates a cached merged PR", async () => {
const merged: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
}
const newerOpen: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 2_000,
pr: { number: 15, title: "new", url: "u15", state: "open", draft: false, base: "main", head: "feature" },
}
const responses = [merged, newerOpen]
let requestCount = 0
const github = {
prStatus: async () => {
requestCount += 1
return responses.shift()!
},
} as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github, "feature"))
useGitHubPrStatusStore.getState().startWatching(key)
for (let i = 0; i < 50 && useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number !== 12; i += 1) {
await Promise.resolve()
}
expect(requestCount).toBe(1)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number).toBe(12)
expect(intervalCallbacks).toHaveLength(1)
// Discovery poll for terminal state (lastDiscoveryPollAt starts at 0).
intervalCallbacks[0]!()
for (let i = 0; i < 50 && useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number !== 15; i += 1) {
await Promise.resolve()
}
expect(requestCount).toBe(2)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number).toBe(15)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.state).toBe("open")
})
test("watcher discovery clears a cached merged PR when no open PR exists", async () => {
const merged: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
}
const empty: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 2_000,
pr: null,
}
const responses = [merged, empty]
let requestCount = 0
const github = {
prStatus: async () => {
requestCount += 1
return responses.shift()!
},
} as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params(github, "feature"))
useGitHubPrStatusStore.getState().startWatching(key)
for (let i = 0; i < 50 && useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number !== 12; i += 1) {
await Promise.resolve()
}
expect(requestCount).toBe(1)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number).toBe(12)
intervalCallbacks[0]!()
for (let i = 0; i < 50 && useGitHubPrStatusStore.getState().entries[key]?.status?.pr != null; i += 1) {
await Promise.resolve()
}
expect(requestCount).toBe(2)
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr).toBeNull()
})
test("does not seed sibling entries from a closed PR", () => {
const closed: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 9, title: "closed", url: "u9", state: "closed", draft: false, base: "main", head: "feature" },
}
const autoKey = getGitHubPrStatusKey("/repo", "feature", null)
const originKey = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.setState({
entries: {
[autoKey]: {
status: closed,
isLoading: false,
error: null,
isInitialStatusResolved: true,
lastRefreshAt: Date.now(),
lastDiscoveryPollAt: 0,
watchers: 0,
params: null,
identity: {
runtimeKey: "runtime-a",
directory: "/repo",
branch: "feature",
remoteName: null,
},
resolvedRemoteName: "origin",
paramsRevision: 0,
},
},
activeRequestCount: 0,
totalRequestCount: 0,
})
useGitHubPrStatusStore.getState().ensureEntry(originKey)
expect(useGitHubPrStatusStore.getState().entries[originKey]?.status).toBeNull()
expect(useGitHubPrStatusStore.getState().entries[originKey]?.isInitialStatusResolved).toBe(false)
})
test("keeps a cached PR when a forced refresh fails", async () => {
const merged: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
}
const github = {
prStatus: async () => {
throw new Error("GitHub unavailable")
},
} as unknown as RuntimeAPIs["github"]
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.setState((state) => ({
entries: {
...state.entries,
[key]: {
...state.entries[key]!,
status: merged,
isInitialStatusResolved: true,
lastRefreshAt: Date.now(),
},
},
}))
useGitHubPrStatusStore.getState().setParams(key, params(github, "feature"))
await useGitHubPrStatusStore.getState().refresh(key, { force: true })
expect(useGitHubPrStatusStore.getState().entries[key]?.status?.pr?.number).toBe(12)
expect(useGitHubPrStatusStore.getState().entries[key]?.error).toBe("GitHub unavailable")
})
test("does not persist a merged branch association", () => {
const merged: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
}
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params({} as RuntimeAPIs["github"], "feature"))
useGitHubPrStatusStore.setState((state) => ({
entries: {
...state.entries,
[key]: {
...state.entries[key]!,
status: merged,
isInitialStatusResolved: true,
lastRefreshAt: Date.now(),
},
},
}))
const persisted = useGitHubPrStatusStore.persist.getOptions().partialize?.(
useGitHubPrStatusStore.getState(),
) as { entries?: Record<string, unknown> } | undefined
expect(persisted?.entries?.[key]).toBeUndefined()
})
test("still persists an open branch association", () => {
const open: GitHubPullRequestStatus = {
connected: true,
fetchedAt: 1_000,
pr: { number: 15, title: "new", url: "u15", state: "open", draft: false, base: "main", head: "feature" },
}
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
useGitHubPrStatusStore.getState().ensureEntry(key)
useGitHubPrStatusStore.getState().setParams(key, params({} as RuntimeAPIs["github"], "feature"))
useGitHubPrStatusStore.setState((state) => ({
entries: {
...state.entries,
[key]: {
...state.entries[key]!,
status: open,
isInitialStatusResolved: true,
lastRefreshAt: Date.now(),
},
},
}))
const persisted = useGitHubPrStatusStore.persist.getOptions().partialize?.(
useGitHubPrStatusStore.getState(),
) as { entries?: Record<string, { status?: GitHubPullRequestStatus | null }> } | undefined
expect(persisted?.entries?.[key]?.status?.pr?.number).toBe(15)
})
test("hydrate strips a legacy persisted merged PR and marks it unresolved", () => {
const key = getGitHubPrStatusKey("/repo", "feature", "origin")
const hydrated = useGitHubPrStatusStore.persist.getOptions().merge?.(
{
entries: {
[key]: {
status: {
connected: true,
fetchedAt: 1_000,
repo: { owner: "acme", repo: "app", url: "https://github.com/acme/app" },
pr: { number: 12, title: "old", url: "u12", state: "merged", draft: false, base: "main", head: "feature" },
checks: { state: "success", total: 1, success: 1, failure: 0, pending: 0 },
canMerge: true,
},
isInitialStatusResolved: true,
lastRefreshAt: Date.now(),
lastDiscoveryPollAt: 0,
identity: {
runtimeKey: "runtime-a",
directory: "/repo",
branch: "feature",
remoteName: "origin",
},
resolvedRemoteName: "origin",
},
},
},
useGitHubPrStatusStore.getState(),
) as {
entries: Record<string, {
status: GitHubPullRequestStatus | null
isInitialStatusResolved: boolean
}>
}
expect(hydrated.entries[key]?.status?.pr).toBeNull()
expect(hydrated.entries[key]?.status?.repo).toEqual({
owner: "acme",
repo: "app",
url: "https://github.com/acme/app",
})
expect(hydrated.entries[key]?.status?.checks).toBeUndefined()
expect(hydrated.entries[key]?.status?.canMerge).toBeUndefined()
expect(hydrated.entries[key]?.isInitialStatusResolved).toBe(false)
})
})