2026-03-17 16:31:11 +09:00
|
|
|
import { describe, expect, test } from "bun:test"
|
|
|
|
|
import {
|
|
|
|
|
detectRepetitiveToolUse,
|
|
|
|
|
recordToolCall,
|
|
|
|
|
resolveCircuitBreakerSettings,
|
|
|
|
|
} from "./loop-detector"
|
|
|
|
|
|
|
|
|
|
function buildWindow(
|
|
|
|
|
toolNames: string[],
|
|
|
|
|
override?: Parameters<typeof resolveCircuitBreakerSettings>[0]
|
|
|
|
|
) {
|
|
|
|
|
const settings = resolveCircuitBreakerSettings(override)
|
|
|
|
|
|
|
|
|
|
return toolNames.reduce(
|
|
|
|
|
(window, toolName) => recordToolCall(window, toolName, settings),
|
|
|
|
|
undefined as ReturnType<typeof recordToolCall> | undefined
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("loop-detector", () => {
|
|
|
|
|
describe("resolveCircuitBreakerSettings", () => {
|
|
|
|
|
describe("#given nested circuit breaker config", () => {
|
|
|
|
|
test("#when resolved #then nested values override defaults", () => {
|
|
|
|
|
const result = resolveCircuitBreakerSettings({
|
|
|
|
|
maxToolCalls: 200,
|
|
|
|
|
circuitBreaker: {
|
|
|
|
|
maxToolCalls: 120,
|
|
|
|
|
windowSize: 10,
|
|
|
|
|
repetitionThresholdPercent: 70,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({
|
2026-03-17 13:29:06 -06:00
|
|
|
enabled: true,
|
2026-03-17 16:31:11 +09:00
|
|
|
maxToolCalls: 120,
|
|
|
|
|
windowSize: 10,
|
|
|
|
|
repetitionThresholdPercent: 70,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-17 13:29:06 -06:00
|
|
|
|
|
|
|
|
describe("#given no enabled config", () => {
|
|
|
|
|
test("#when resolved #then enabled defaults to true", () => {
|
|
|
|
|
const result = resolveCircuitBreakerSettings({
|
|
|
|
|
circuitBreaker: {
|
|
|
|
|
maxToolCalls: 100,
|
|
|
|
|
windowSize: 5,
|
|
|
|
|
repetitionThresholdPercent: 60,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.enabled).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("#given enabled is false in config", () => {
|
|
|
|
|
test("#when resolved #then enabled is false", () => {
|
|
|
|
|
const result = resolveCircuitBreakerSettings({
|
|
|
|
|
circuitBreaker: {
|
|
|
|
|
enabled: false,
|
|
|
|
|
maxToolCalls: 100,
|
|
|
|
|
windowSize: 5,
|
|
|
|
|
repetitionThresholdPercent: 60,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.enabled).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("#given enabled is true in config", () => {
|
|
|
|
|
test("#when resolved #then enabled is true", () => {
|
|
|
|
|
const result = resolveCircuitBreakerSettings({
|
|
|
|
|
circuitBreaker: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
maxToolCalls: 100,
|
|
|
|
|
windowSize: 5,
|
|
|
|
|
repetitionThresholdPercent: 60,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(result.enabled).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-17 16:31:11 +09:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("detectRepetitiveToolUse", () => {
|
|
|
|
|
describe("#given recent tools are diverse", () => {
|
|
|
|
|
test("#when evaluated #then it does not trigger", () => {
|
|
|
|
|
const window = buildWindow([
|
|
|
|
|
"read",
|
|
|
|
|
"grep",
|
|
|
|
|
"edit",
|
|
|
|
|
"bash",
|
|
|
|
|
"read",
|
|
|
|
|
"glob",
|
|
|
|
|
"lsp_diagnostics",
|
|
|
|
|
"read",
|
|
|
|
|
"grep",
|
|
|
|
|
"edit",
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const result = detectRepetitiveToolUse(window)
|
|
|
|
|
|
|
|
|
|
expect(result.triggered).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("#given the same tool dominates the recent window", () => {
|
|
|
|
|
test("#when evaluated #then it triggers", () => {
|
|
|
|
|
const window = buildWindow([
|
|
|
|
|
"read",
|
|
|
|
|
"read",
|
|
|
|
|
"read",
|
|
|
|
|
"edit",
|
|
|
|
|
"read",
|
|
|
|
|
"read",
|
|
|
|
|
"read",
|
|
|
|
|
"read",
|
|
|
|
|
"grep",
|
|
|
|
|
"read",
|
|
|
|
|
], {
|
|
|
|
|
circuitBreaker: {
|
|
|
|
|
windowSize: 10,
|
|
|
|
|
repetitionThresholdPercent: 80,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const result = detectRepetitiveToolUse(window)
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
triggered: true,
|
|
|
|
|
toolName: "read",
|
|
|
|
|
repeatedCount: 8,
|
|
|
|
|
sampleSize: 10,
|
|
|
|
|
thresholdPercent: 80,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("#given the window is not full yet", () => {
|
|
|
|
|
test("#when the current sample crosses the threshold #then it still triggers", () => {
|
|
|
|
|
const window = buildWindow(["read", "read", "edit", "read", "read", "read", "read", "read"], {
|
|
|
|
|
circuitBreaker: {
|
|
|
|
|
windowSize: 10,
|
|
|
|
|
repetitionThresholdPercent: 80,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const result = detectRepetitiveToolUse(window)
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
triggered: true,
|
|
|
|
|
toolName: "read",
|
|
|
|
|
repeatedCount: 7,
|
|
|
|
|
sampleSize: 8,
|
|
|
|
|
thresholdPercent: 80,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|