import { describe, expect, it } from 'vitest' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { GOAWAY_REASON_TO_CODE } from 'relay-contracts' import { applyGoAway, applyGoAwayCode, classifyGoAway, createRevocationState, } from '../src/lifecycle/revocation.js' describe('classifyGoAway (T14, frozen 3-value union)', () => { it('maps operatorDrain and shutdown → drain, revoked → revoked', () => { expect(classifyGoAway('operatorDrain')).toBe('drain') expect(classifyGoAway('shutdown')).toBe('drain') expect(classifyGoAway('revoked')).toBe('revoked') }) }) describe('createRevocationState (T14, INV12)', () => { it('revoke tears down once and makes isRevoked() true (suppresses reconnect)', () => { let teardowns = 0 const state = createRevocationState(() => { teardowns += 1 }) expect(state.isRevoked()).toBe(false) state.markRevoked('goaway-revoked') expect(state.isRevoked()).toBe(true) state.markRevoked('operator') // idempotent expect(teardowns).toBe(1) }) }) describe('applyGoAway / applyGoAwayCode (T14)', () => { it('a revoked GOAWAY tears down; a drain GOAWAY does not', () => { const revokeState = createRevocationState(() => {}) expect(applyGoAway('revoked', revokeState)).toBe('revoked') expect(revokeState.isRevoked()).toBe(true) const drainState = createRevocationState(() => {}) expect(applyGoAway('operatorDrain', drainState)).toBe('drain') expect(applyGoAway('shutdown', drainState)).toBe('drain') expect(drainState.isRevoked()).toBe(false) }) it('decodes the frozen wire codes via decodeGoAwayReason', () => { const state = createRevocationState(() => {}) expect(applyGoAwayCode(GOAWAY_REASON_TO_CODE.operatorDrain, state)).toBe('drain') expect(applyGoAwayCode(GOAWAY_REASON_TO_CODE.revoked, state)).toBe('revoked') }) it('an unknown reason code FAILS CLOSED to revoked (INV12 safety)', () => { const state = createRevocationState(() => {}) expect(applyGoAwayCode(99, state)).toBe('revoked') expect(state.isRevoked()).toBe(true) }) it('cross-plan-drift guard: no bare integer reason literal in revocation.ts', () => { const src = readFileSync( join(import.meta.dirname, '..', 'src', 'lifecycle', 'revocation.ts'), 'utf8', ) // The reason mapping must come only from the frozen union/decoder — never a hardcoded // `=== 2` / `reason: 2` style integer. (Comments are allowed to mention numbers.) const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '') expect(code).not.toMatch(/reason\s*[=:]\s*\d/) expect(code).not.toMatch(/===\s*[123]\b/) }) })