Files
oh-my-opencode/src/hooks/preemptive-compaction-degradation-monitor.regression.test.ts
T
YeonGyu-Kim 61083d499d fix(oauth+errors): OAuth silent refresh, quota STOP patterns, compaction loop cap
Bug fixes:
1. OAuth token refresh (#3149): buildHttpRequestInit() now attempts silent refresh
   via refresh_token before triggering full browser re-auth. Added refresh() method
   to McpOAuthProvider. Includes test isolation fix for discovery mock.

2. Quota error STOP (#3126): Added STOP_MESSAGE_PATTERNS in model-error-classifier
   that take precedence over RETRYABLE_MESSAGE_PATTERNS. Message-only quota errors
   now non-retryable. Runtime-fallback: quota_exceeded with 'retrying in' signal
   still triggers fallback (provider-managed auto-retry). Restored removed patterns.

3. Compaction loop (#3127): MAX_RECOVERY_ATTEMPTS=3 cap + additional suppression
   guard from opencode session in degradation monitor.

Also: refactored extractAutoRetrySignal to auto-retry-signal.ts, new regression
tests for quota classifier and compaction degradation monitor.
2026-04-06 17:40:12 +09:00

128 lines
3.5 KiB
TypeScript

/// <reference types="bun-types" />
import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"
const logMock = mock(() => {})
mock.module("../shared/logger", () => ({
log: logMock,
}))
afterAll(() => { mock.restore() })
const { createPreemptiveCompactionHook } = await import("./preemptive-compaction")
type AssistantHistoryMessage = {
info: {
id: string
role: "assistant"
}
parts: Array<{ type: string; text?: string }>
}
function createMockCtx(sessionHistory: AssistantHistoryMessage[]) {
return {
client: {
session: {
messages: mock(() => Promise.resolve({ data: sessionHistory })),
summarize: mock(() => Promise.resolve({})),
},
tui: {
showToast: mock(() => Promise.resolve({})),
},
},
directory: "/tmp/test",
}
}
function appendAssistantHistory(
sessionHistory: AssistantHistoryMessage[],
input: {
id: string
parts: AssistantHistoryMessage["parts"]
},
): void {
sessionHistory.push({
info: {
id: input.id,
role: "assistant",
},
parts: input.parts,
})
}
function buildAssistantUpdate(input: {
sessionID: string
id: string
parts: unknown[]
}) {
return {
event: {
type: "message.updated",
properties: {
info: {
id: input.id,
role: "assistant",
sessionID: input.sessionID,
providerID: "opencode",
modelID: "kimi-k2.5-free",
finish: true,
tokens: { input: 1000, output: 10, reasoning: 0, cache: { read: 0, write: 0 } },
parts: input.parts,
},
},
},
}
}
describe("preemptive-compaction degradation monitor regressions", () => {
beforeEach(() => {
logMock.mockClear()
})
it("does not re-arm monitoring after recovery-triggered compaction", async () => {
// given
const sessionHistory: AssistantHistoryMessage[] = []
const ctx = createMockCtx(sessionHistory)
const hook = createPreemptiveCompactionHook(ctx as never, {} as never)
const sessionID = "ses_recovery_compaction_guard"
const stepOnlyParts = [{ type: "step-start" }, { type: "step-finish" }]
await hook.event({
event: {
type: "session.compacted",
properties: { sessionID },
},
})
// when
appendAssistantHistory(sessionHistory, { id: "msg_1", parts: stepOnlyParts })
await hook.event(buildAssistantUpdate({ sessionID, id: "msg_1", parts: stepOnlyParts }))
appendAssistantHistory(sessionHistory, { id: "msg_2", parts: stepOnlyParts })
await hook.event(buildAssistantUpdate({ sessionID, id: "msg_2", parts: stepOnlyParts }))
appendAssistantHistory(sessionHistory, { id: "msg_3", parts: stepOnlyParts })
await hook.event(buildAssistantUpdate({ sessionID, id: "msg_3", parts: stepOnlyParts }))
await hook.event({
event: {
type: "session.compacted",
properties: { sessionID },
},
})
appendAssistantHistory(sessionHistory, { id: "msg_4", parts: stepOnlyParts })
await hook.event(buildAssistantUpdate({ sessionID, id: "msg_4", parts: stepOnlyParts }))
appendAssistantHistory(sessionHistory, { id: "msg_5", parts: stepOnlyParts })
await hook.event(buildAssistantUpdate({ sessionID, id: "msg_5", parts: stepOnlyParts }))
appendAssistantHistory(sessionHistory, { id: "msg_6", parts: stepOnlyParts })
await hook.event(buildAssistantUpdate({ sessionID, id: "msg_6", parts: stepOnlyParts }))
// then
expect(ctx.client.session.summarize).toHaveBeenCalledTimes(1)
})
})