Loading files
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
import { mkdtemp, readFile, rm } from 'fs/promises'
import { tmpdir } from 'os'
const authDir = await mkdtemp(`${tmpdir()}/harness-auth-session-`)
process.env.HARNESS_AUTH_DIR = authDir
const {
AUTH_SESSION_FILE,
AuthSessionError,
AuthSessionManager,
clearAuthSession,
readAuthSession,
writeAuthSession,
} = await import('./authSession.js')
const baseSession = () => ({
version: 1 as const,
accessToken: 'old-access',
refreshToken: 'refresh-1',
expiresAt: Date.now() - 1,
autonomousEnv: 'prod' as const,
computerId: 'computer-1',
machineId: 'machine-1',
updatedAt: Date.now(),
})
afterEach(() => {
clearAuthSession()
vi.unstubAllGlobals()
})
afterAll(async () => {
delete process.env.HARNESS_AUTH_DIR
await rm(authDir, { recursive: true, force: true })
})
describe('AuthSessionManager', () => {
it('coalesces concurrent expired-token refreshes into one request and persists the rotated tokens', async () => {
writeAuthSession(baseSession())
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
success: true,
data: { token: 'new-access', refreshToken: 'refresh-2', expiresIn: 3600 },
})))
vi.stubGlobal('fetch', fetchMock)
const manager = new AuthSessionManager('https://api.example.test')
await expect(Promise.all([manager.accessToken(), manager.accessToken()])).resolves.toEqual(['new-access', 'new-access'])
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(readAuthSession()).toMatchObject({ accessToken: 'new-access', refreshToken: 'refresh-2' })
await expect(readFile(AUTH_SESSION_FILE, 'utf8')).resolves.toContain('new-access')
})
it('uses the newer persisted access token rather than refreshing a stale 401 again', async () => {
writeAuthSession({ ...baseSession(), accessToken: 'already-refreshed', expiresAt: Date.now() + 3_600_000 })
const fetchMock = vi.fn<typeof fetch>()
vi.stubGlobal('fetch', fetchMock)
const manager = new AuthSessionManager('https://api.example.test')
await expect(manager.accessToken({ force: true, failedToken: 'old-access' })).resolves.toBe('already-refreshed')
expect(fetchMock).not.toHaveBeenCalled()
})
it('names `harness login` when the refresh service answers 503, because that is what a dead token looks like here', async () => {
// Measured 2026-09-06 against the live backend: an unusable refresh token comes back as
// `503 {"error":{"code":"AUTH_SERVICE_UNAVAILABLE"}}`, NOT 401 and not REFRESH_TOKEN_INVALID.
// So this answer is indistinguishable from a real outage, and the message has to carry both
// readings — saying only the upstream's "Authentication service unavailable" sends somebody
// with a dead sign-in away to wait for a service that is fine.
writeAuthSession(baseSession())
// A fresh Response per call: a body can only be read once, so one shared Response would make
// every call after the first see an empty body and prove nothing about the message.
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockImplementation(async () => new Response(JSON.stringify({
success: false,
error: { code: 'AUTH_SERVICE_UNAVAILABLE', message: 'Authentication service unavailable' },
}), { status: 503 })))
const manager = new AuthSessionManager('https://api.example.test')
const err = await manager.accessToken().catch((e: unknown) => e) as InstanceType<typeof AuthSessionError>
expect(err).toMatchObject({ name: AuthSessionError.name, code: 'UNAVAILABLE' })
expect(err.message).toMatch(/harness login/) // the way out, which the upstream never names
expect(err.message).toMatch(/Authentication service unavailable/) // the upstream's own words, kept
})
it('keeps the session when the refresh service is merely unavailable', async () => {
// The other half of the same ambiguity, and the reason this must NOT be reclassified as
// INVALID_REFRESH: that branch deletes the session, and deleting it on a transient blip
// destroys a refresh token nothing can bring back. Ambiguous means keep, and say so.
writeAuthSession(baseSession())
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
success: false,
error: { code: 'AUTH_SERVICE_UNAVAILABLE', message: 'Authentication service unavailable' },
}), { status: 503 })))
const manager = new AuthSessionManager('https://api.example.test')
await expect(manager.accessToken()).rejects.toMatchObject({ code: 'UNAVAILABLE' })
expect(readAuthSession()).toMatchObject({ refreshToken: 'refresh-1' })
})
it('gives up on a refresh that never answers, keeping the session', async () => {
// The daemon's reconnect waits on this call, and the desktop never restarts a daemon that is
// alive — an unbounded fetch here was a machine "connected" in status and disconnected in fact.
writeAuthSession(baseSession())
vi.stubGlobal('fetch', vi.fn((_url: string, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => reject(new Error('aborted')))
})))
const manager = new AuthSessionManager('https://api.example.test', { refreshTimeoutMs: 50 })
await expect(manager.accessToken()).rejects.toMatchObject({ code: 'UNAVAILABLE' })
expect(readAuthSession()).toMatchObject({ refreshToken: 'refresh-1' })
})
it('still says `harness login` when the service gives no message of its own', async () => {
writeAuthSession(baseSession())
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockRejectedValue(new Error('socket hang up')))
const manager = new AuthSessionManager('https://api.example.test')
await expect(manager.accessToken()).rejects.toThrow(/harness login/)
expect(readAuthSession()).toMatchObject({ refreshToken: 'refresh-1' })
})
it('clears an invalid refresh session instead of retrying it forever', async () => {
writeAuthSession(baseSession())
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify({
success: false,
error: { code: 'REFRESH_TOKEN_INVALID', message: 'expired' },
}), { status: 401 })))
const manager = new AuthSessionManager('https://api.example.test')
await expect(manager.accessToken()).rejects.toMatchObject({ name: AuthSessionError.name, code: 'INVALID_REFRESH' })
expect(readAuthSession()).toBeNull()
})
})
59e2a9d01This file is pinned to the selected release. Download the original to open it in your design tool.