cc-haha/src/server/__tests__/oauth-refresh-log.test.ts
程序员阿江(Relakkes) 837337901d fix(diagnostics): stop logging expected states as errors and isolate tests
The diagnostics panel filled with red ERROR/WARN entries during normal use
because the capture layer treated "wrote to console" as "something failed",
with no severity gatekeeping. Much of the noise was also test runs leaking
into the user's real ~/.claude/cc-haha/diagnostics.

- diagnosticsService: default unclassified events to info (not error);
  drop writes under NODE_ENV=test when no CLAUDE_CONFIG_DIR is set; document
  the console-capture contract (expected states use console.debug/info).
- index: don't install console/process capture under bun test.
- api/diagnostics: an ingested event with missing severity defaults to info.
- oauthRefreshLog (new): token refresh failure is gracefully handled and is
  never an error — expected expiry (401/403/revoked) logs at debug, anything
  else at warn. Wired into both Haha OAuth services.
- conversationService: classify cli_runtime_exit severity by exit code, so
  clean/SIGTERM/SIGKILL exits are info and only abnormal codes stay error
  (real "chat died" crashes remain perceivable).
- ws/handler: streaming partial tool-input JSON is normal — debug, not warn.

Tests: oauth-refresh-log + cli-exit-severity unit tests; diagnostics-service
covers the info default and the test-isolation guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:12:45 +08:00

68 lines
2.2 KiB
TypeScript

import { afterEach, describe, expect, spyOn, test } from 'bun:test'
import {
isExpectedAuthRefreshFailure,
logTokenRefreshFailure,
} from '../services/oauthRefreshLog.js'
describe('isExpectedAuthRefreshFailure', () => {
test.each([
'refresh revoked',
'401 Unauthorized',
'Request failed with status code 403',
'OpenAI token refresh failed: 403: {"error":"forbidden"}',
'invalid_grant',
'Forbidden',
])('treats %p as an expected re-auth signal', (message) => {
expect(isExpectedAuthRefreshFailure(new Error(message))).toBe(true)
})
test.each([
'Failed to fetch',
'network timeout',
'Request failed with status code 500',
'ECONNREFUSED',
])('treats %p as an unexpected failure', (message) => {
expect(isExpectedAuthRefreshFailure(new Error(message))).toBe(false)
})
test('handles non-Error values without throwing', () => {
expect(isExpectedAuthRefreshFailure('401 Unauthorized')).toBe(true)
expect(isExpectedAuthRefreshFailure(null)).toBe(false)
expect(isExpectedAuthRefreshFailure(undefined)).toBe(false)
})
})
describe('logTokenRefreshFailure', () => {
let errorSpy: ReturnType<typeof spyOn>
let warnSpy: ReturnType<typeof spyOn>
let debugSpy: ReturnType<typeof spyOn>
function install() {
errorSpy = spyOn(console, 'error').mockImplementation(() => {})
warnSpy = spyOn(console, 'warn').mockImplementation(() => {})
debugSpy = spyOn(console, 'debug').mockImplementation(() => {})
}
afterEach(() => {
errorSpy?.mockRestore()
warnSpy?.mockRestore()
debugSpy?.mockRestore()
})
test('never logs an expected expiry as console.error (would become a red ERROR)', () => {
install()
logTokenRefreshFailure('[Svc]', new Error('refresh revoked'))
expect(errorSpy).not.toHaveBeenCalled()
expect(warnSpy).not.toHaveBeenCalled()
expect(debugSpy).toHaveBeenCalledTimes(1)
})
test('logs an unexpected failure as a warn, never an error', () => {
install()
logTokenRefreshFailure('[Svc]', new Error('Failed to fetch'))
expect(errorSpy).not.toHaveBeenCalled()
expect(warnSpy).toHaveBeenCalledTimes(1)
expect(debugSpy).not.toHaveBeenCalled()
})
})