Files
openchamber/packages/ui/src/stores/useLinearAuthStore.test.ts
T
Bohdan Triapitsyn 6fa2cb66d0 fix: scope instance-served state to the connected instance
Linear and GitHub logins, quotas, MCP status, skills and agent memory are
served by whichever instance is connected, but each was cached globally or
by directory alone — which two instances can share. Switching instances left
the previous instance's answers on screen and its Linear login usable against
a runtime that has no Linear.

Reset them all through runtimeEndpointReset, each store guarding its in-flight
requests with a generation so a response for the previous instance cannot land
in the new one. The Linear team filter is now persisted per instance: a team
belongs to one workspace, so carrying it across filtered the new instance's
issue list down to nothing.

Usage also waits for the instance to report itself initialised before loading.
Providers report themselves as configured only once the instance can read their
credentials, so a fetch fired at mount answered "nothing configured" for every
provider and cached it — which is why Usage stayed missing from the work-status
panel until Settings -> Usage forced a fresh fetch.
2026-09-03 11:49:34 +03:00

80 lines
2.7 KiB
TypeScript

import { beforeEach, describe, expect, mock, test } from "bun:test"
import type { LinearAPI, LinearAuthStatus } from "@/lib/api/types"
mock.module("@/lib/runtime-fetch", () => ({ runtimeFetch: async () => new Response("{}") }))
const { useLinearAuthStore } = await import("./useLinearAuthStore")
const deferred = <T>() => {
let resolve!: (value: T) => void
const promise = new Promise<T>((res) => { resolve = res })
return { promise, resolve }
}
// Only `authStatus` is exercised here; the rest of the surface is present so
// the stub is a real `LinearAPI` rather than an assertion over a fragment.
const unreachable = () => Promise.reject(new Error("not used in this test"))
const linearApi = (authStatus: LinearAPI["authStatus"]): LinearAPI => ({
authStatus,
authStart: unreachable,
authDisconnect: unreachable,
authActivate: unreachable,
issuesList: unreachable,
issueGet: unreachable,
issueStates: unreachable,
issueUpdate: unreachable,
mappingGet: unreachable,
mappingSet: unreachable,
sessionStatusPost: unreachable,
preferencesGet: unreachable,
preferencesSet: unreachable,
})
describe("Linear auth is scoped to the connected instance", () => {
beforeEach(() => {
useLinearAuthStore.getState().resetForRuntimeSwitch()
})
test("a switch drops the previous instance's login", async () => {
await useLinearAuthStore.getState().refreshStatus(
linearApi(async () => ({ connected: true })),
{ force: true },
)
expect(useLinearAuthStore.getState().status?.connected).toBe(true)
useLinearAuthStore.getState().resetForRuntimeSwitch()
expect(useLinearAuthStore.getState().status).toBeNull()
expect(useLinearAuthStore.getState().hasChecked).toBe(false)
})
test("a status still in flight for the previous instance cannot land in the new one", async () => {
const pending = deferred<LinearAuthStatus>()
const refresh = useLinearAuthStore.getState().refreshStatus(
linearApi(() => pending.promise),
{ force: true },
)
useLinearAuthStore.getState().resetForRuntimeSwitch()
pending.resolve({ connected: true })
await refresh
expect(useLinearAuthStore.getState().status).toBeNull()
expect(useLinearAuthStore.getState().hasChecked).toBe(false)
})
test("a failed check is not an authoritative disconnect", async () => {
await useLinearAuthStore.getState().refreshStatus(
linearApi(async () => ({ connected: true })),
{ force: true },
)
await useLinearAuthStore.getState().refreshStatus(
linearApi(async () => { throw new Error("offline") }),
{ force: true },
)
expect(useLinearAuthStore.getState().status?.connected).toBe(true)
expect(useLinearAuthStore.getState().status?.error).toBe("offline")
})
})