Roo-Code/apps/cli/src/extension-client/client.test.ts
Chris Estreich ea9717d7ba
Add a TUI (#10480)
Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: Daniel <57051444+daniel-lxs@users.noreply.github.com>
2026-01-09 19:19:58 -08:00

809 lines
26 KiB
TypeScript

/**
* Tests for the Roo Code Client
*
* These tests verify:
* - State detection logic
* - Event emission
* - Response sending
* - State transitions
*/
import {
type ClineMessage,
type ExtensionMessage,
createMockClient,
AgentLoopState,
detectAgentState,
isIdleAsk,
isResumableAsk,
isInteractiveAsk,
isNonBlockingAsk,
} from "./index.js"
// =============================================================================
// Test Helpers
// =============================================================================
function createMessage(overrides: Partial<ClineMessage>): ClineMessage {
return {
ts: Date.now() + Math.random() * 1000, // Unique timestamp
type: "say",
...overrides,
}
}
function createStateMessage(messages: ClineMessage[]): ExtensionMessage {
return {
type: "state",
state: {
clineMessages: messages,
},
}
}
// =============================================================================
// State Detection Tests
// =============================================================================
describe("detectAgentState", () => {
describe("NO_TASK state", () => {
it("should return NO_TASK for empty messages array", () => {
const state = detectAgentState([])
expect(state.state).toBe(AgentLoopState.NO_TASK)
expect(state.isWaitingForInput).toBe(false)
expect(state.isRunning).toBe(false)
})
it("should return NO_TASK for undefined messages", () => {
const state = detectAgentState(undefined as unknown as ClineMessage[])
expect(state.state).toBe(AgentLoopState.NO_TASK)
})
})
describe("STREAMING state", () => {
it("should detect streaming when partial is true", () => {
const messages = [createMessage({ type: "ask", ask: "tool", partial: true })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.STREAMING)
expect(state.isStreaming).toBe(true)
expect(state.isWaitingForInput).toBe(false)
})
it("should detect streaming when api_req_started has no cost", () => {
const messages = [
createMessage({
say: "api_req_started",
text: JSON.stringify({ tokensIn: 100 }), // No cost field
}),
]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.STREAMING)
expect(state.isStreaming).toBe(true)
})
it("should NOT be streaming when api_req_started has cost", () => {
const messages = [
createMessage({
say: "api_req_started",
text: JSON.stringify({ cost: 0.001, tokensIn: 100 }),
}),
]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.isStreaming).toBe(false)
})
})
describe("WAITING_FOR_INPUT state", () => {
it("should detect waiting for tool approval", () => {
const messages = [createMessage({ type: "ask", ask: "tool", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.isWaitingForInput).toBe(true)
expect(state.currentAsk).toBe("tool")
expect(state.requiredAction).toBe("approve")
})
it("should detect waiting for command approval", () => {
const messages = [createMessage({ type: "ask", ask: "command", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.currentAsk).toBe("command")
expect(state.requiredAction).toBe("approve")
})
it("should detect waiting for followup answer", () => {
const messages = [createMessage({ type: "ask", ask: "followup", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.currentAsk).toBe("followup")
expect(state.requiredAction).toBe("answer")
})
it("should detect waiting for browser_action_launch approval", () => {
const messages = [createMessage({ type: "ask", ask: "browser_action_launch", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.requiredAction).toBe("approve")
})
it("should detect waiting for use_mcp_server approval", () => {
const messages = [createMessage({ type: "ask", ask: "use_mcp_server", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.requiredAction).toBe("approve")
})
})
describe("IDLE state", () => {
it("should detect completion_result as idle", () => {
const messages = [createMessage({ type: "ask", ask: "completion_result", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.IDLE)
expect(state.isWaitingForInput).toBe(true)
expect(state.requiredAction).toBe("start_task")
})
it("should detect api_req_failed as idle", () => {
const messages = [createMessage({ type: "ask", ask: "api_req_failed", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.IDLE)
expect(state.requiredAction).toBe("retry_or_new_task")
})
it("should detect mistake_limit_reached as idle", () => {
const messages = [createMessage({ type: "ask", ask: "mistake_limit_reached", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.IDLE)
expect(state.requiredAction).toBe("proceed_or_new_task")
})
it("should detect auto_approval_max_req_reached as idle", () => {
const messages = [createMessage({ type: "ask", ask: "auto_approval_max_req_reached", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.IDLE)
expect(state.requiredAction).toBe("start_new_task")
})
it("should detect resume_completed_task as idle", () => {
const messages = [createMessage({ type: "ask", ask: "resume_completed_task", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.IDLE)
expect(state.requiredAction).toBe("start_new_task")
})
})
describe("RESUMABLE state", () => {
it("should detect resume_task as resumable", () => {
const messages = [createMessage({ type: "ask", ask: "resume_task", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RESUMABLE)
expect(state.isWaitingForInput).toBe(true)
expect(state.requiredAction).toBe("resume_or_abandon")
})
})
describe("RUNNING state", () => {
it("should detect running for say messages", () => {
const messages = [
createMessage({
say: "api_req_started",
text: JSON.stringify({ cost: 0.001 }),
}),
createMessage({ say: "text", text: "Working on it..." }),
]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.isRunning).toBe(true)
expect(state.isWaitingForInput).toBe(false)
})
it("should detect running for command_output (non-blocking)", () => {
const messages = [createMessage({ type: "ask", ask: "command_output", partial: false })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.requiredAction).toBe("continue_or_abort")
})
})
})
// =============================================================================
// Type Guard Tests
// =============================================================================
describe("Type Guards", () => {
describe("isIdleAsk", () => {
it("should return true for idle asks", () => {
expect(isIdleAsk("completion_result")).toBe(true)
expect(isIdleAsk("api_req_failed")).toBe(true)
expect(isIdleAsk("mistake_limit_reached")).toBe(true)
expect(isIdleAsk("auto_approval_max_req_reached")).toBe(true)
expect(isIdleAsk("resume_completed_task")).toBe(true)
})
it("should return false for non-idle asks", () => {
expect(isIdleAsk("tool")).toBe(false)
expect(isIdleAsk("followup")).toBe(false)
expect(isIdleAsk("resume_task")).toBe(false)
})
})
describe("isInteractiveAsk", () => {
it("should return true for interactive asks", () => {
expect(isInteractiveAsk("tool")).toBe(true)
expect(isInteractiveAsk("command")).toBe(true)
expect(isInteractiveAsk("followup")).toBe(true)
expect(isInteractiveAsk("browser_action_launch")).toBe(true)
expect(isInteractiveAsk("use_mcp_server")).toBe(true)
})
it("should return false for non-interactive asks", () => {
expect(isInteractiveAsk("completion_result")).toBe(false)
expect(isInteractiveAsk("command_output")).toBe(false)
})
})
describe("isResumableAsk", () => {
it("should return true for resumable asks", () => {
expect(isResumableAsk("resume_task")).toBe(true)
})
it("should return false for non-resumable asks", () => {
expect(isResumableAsk("completion_result")).toBe(false)
expect(isResumableAsk("tool")).toBe(false)
})
})
describe("isNonBlockingAsk", () => {
it("should return true for non-blocking asks", () => {
expect(isNonBlockingAsk("command_output")).toBe(true)
})
it("should return false for blocking asks", () => {
expect(isNonBlockingAsk("tool")).toBe(false)
expect(isNonBlockingAsk("followup")).toBe(false)
})
})
})
// =============================================================================
// ExtensionClient Tests
// =============================================================================
describe("ExtensionClient", () => {
describe("State queries", () => {
it("should return NO_TASK when not initialized", () => {
const { client } = createMockClient()
expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
expect(client.isInitialized()).toBe(false)
})
it("should update state when receiving messages", () => {
const { client } = createMockClient()
const message = createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })])
client.handleMessage(message)
expect(client.isInitialized()).toBe(true)
expect(client.getCurrentState()).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(client.isWaitingForInput()).toBe(true)
expect(client.getCurrentAsk()).toBe("tool")
})
})
describe("Event emission", () => {
it("should emit stateChange events", () => {
const { client } = createMockClient()
const stateChanges: AgentLoopState[] = []
client.onStateChange((event) => {
stateChanges.push(event.currentState.state)
})
client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })]))
expect(stateChanges).toContain(AgentLoopState.WAITING_FOR_INPUT)
})
it("should emit waitingForInput events", () => {
const { client } = createMockClient()
const waitingEvents: string[] = []
client.onWaitingForInput((event) => {
waitingEvents.push(event.ask)
})
client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "followup", partial: false })]))
expect(waitingEvents).toContain("followup")
})
it("should allow unsubscribing from events", () => {
const { client } = createMockClient()
let callCount = 0
const unsubscribe = client.onStateChange(() => {
callCount++
})
client.handleMessage(createStateMessage([createMessage({ say: "text" })]))
expect(callCount).toBe(1)
unsubscribe()
client.handleMessage(createStateMessage([createMessage({ say: "text", ts: Date.now() + 1 })]))
expect(callCount).toBe(1) // Should not increase
})
})
describe("Response methods", () => {
it("should send approve response", () => {
const { client, sentMessages } = createMockClient()
client.approve()
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "askResponse",
askResponse: "yesButtonClicked",
text: undefined,
images: undefined,
})
})
it("should send reject response", () => {
const { client, sentMessages } = createMockClient()
client.reject()
expect(sentMessages).toHaveLength(1)
const msg = sentMessages[0]
expect(msg).toBeDefined()
expect(msg?.askResponse).toBe("noButtonClicked")
})
it("should send text response", () => {
const { client, sentMessages } = createMockClient()
client.respond("My answer", ["image-data"])
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "askResponse",
askResponse: "messageResponse",
text: "My answer",
images: ["image-data"],
})
})
it("should send newTask message", () => {
const { client, sentMessages } = createMockClient()
client.newTask("Build a web app")
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "newTask",
text: "Build a web app",
images: undefined,
})
})
it("should send clearTask message", () => {
const { client, sentMessages } = createMockClient()
client.clearTask()
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "clearTask",
})
})
it("should send cancelTask message", () => {
const { client, sentMessages } = createMockClient()
client.cancelTask()
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "cancelTask",
})
})
it("should send terminal continue operation", () => {
const { client, sentMessages } = createMockClient()
client.continueTerminal()
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "terminalOperation",
terminalOperation: "continue",
})
})
it("should send terminal abort operation", () => {
const { client, sentMessages } = createMockClient()
client.abortTerminal()
expect(sentMessages).toHaveLength(1)
expect(sentMessages[0]).toEqual({
type: "terminalOperation",
terminalOperation: "abort",
})
})
})
describe("Message handling", () => {
it("should handle JSON string messages", () => {
const { client } = createMockClient()
const message = JSON.stringify(
createStateMessage([createMessage({ type: "ask", ask: "completion_result", partial: false })]),
)
client.handleMessage(message)
expect(client.getCurrentState()).toBe(AgentLoopState.IDLE)
})
it("should ignore invalid JSON", () => {
const { client } = createMockClient()
client.handleMessage("not valid json")
expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
})
it("should handle messageUpdated messages", () => {
const { client } = createMockClient()
// First, set initial state
client.handleMessage(
createStateMessage([createMessage({ ts: 123, type: "ask", ask: "tool", partial: true })]),
)
expect(client.isStreaming()).toBe(true)
// Now update the message
client.handleMessage({
type: "messageUpdated",
clineMessage: createMessage({ ts: 123, type: "ask", ask: "tool", partial: false }),
})
expect(client.isStreaming()).toBe(false)
expect(client.isWaitingForInput()).toBe(true)
})
})
describe("Reset functionality", () => {
it("should reset state", () => {
const { client } = createMockClient()
client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })]))
expect(client.isInitialized()).toBe(true)
expect(client.getCurrentState()).toBe(AgentLoopState.WAITING_FOR_INPUT)
client.reset()
expect(client.isInitialized()).toBe(false)
expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
})
})
})
// =============================================================================
// Integration Tests
// =============================================================================
describe("Integration", () => {
it("should handle a complete task flow", () => {
const { client } = createMockClient()
const states: AgentLoopState[] = []
client.onStateChange((event) => {
states.push(event.currentState.state)
})
// 1. Task starts, API request begins
client.handleMessage(
createStateMessage([
createMessage({
say: "api_req_started",
text: JSON.stringify({}), // No cost = streaming
}),
]),
)
expect(client.isStreaming()).toBe(true)
// 2. API request completes
client.handleMessage(
createStateMessage([
createMessage({
say: "api_req_started",
text: JSON.stringify({ cost: 0.001 }),
}),
createMessage({ say: "text", text: "I'll help you with that." }),
]),
)
expect(client.isStreaming()).toBe(false)
expect(client.isRunning()).toBe(true)
// 3. Tool ask (partial)
client.handleMessage(
createStateMessage([
createMessage({
say: "api_req_started",
text: JSON.stringify({ cost: 0.001 }),
}),
createMessage({ say: "text", text: "I'll help you with that." }),
createMessage({ type: "ask", ask: "tool", partial: true }),
]),
)
expect(client.isStreaming()).toBe(true)
// 4. Tool ask (complete)
client.handleMessage(
createStateMessage([
createMessage({
say: "api_req_started",
text: JSON.stringify({ cost: 0.001 }),
}),
createMessage({ say: "text", text: "I'll help you with that." }),
createMessage({ type: "ask", ask: "tool", partial: false }),
]),
)
expect(client.isWaitingForInput()).toBe(true)
expect(client.getCurrentAsk()).toBe("tool")
// 5. User approves, task completes
client.handleMessage(
createStateMessage([
createMessage({
say: "api_req_started",
text: JSON.stringify({ cost: 0.001 }),
}),
createMessage({ say: "text", text: "I'll help you with that." }),
createMessage({ type: "ask", ask: "tool", partial: false }),
createMessage({ say: "text", text: "File created." }),
createMessage({ type: "ask", ask: "completion_result", partial: false }),
]),
)
expect(client.getCurrentState()).toBe(AgentLoopState.IDLE)
expect(client.getCurrentAsk()).toBe("completion_result")
// Verify we saw the expected state transitions
expect(states).toContain(AgentLoopState.STREAMING)
expect(states).toContain(AgentLoopState.RUNNING)
expect(states).toContain(AgentLoopState.WAITING_FOR_INPUT)
expect(states).toContain(AgentLoopState.IDLE)
})
})
// =============================================================================
// Edge Case Tests
// =============================================================================
describe("Edge Cases", () => {
describe("Messages with missing or empty text field", () => {
it("should handle ask message with missing text field", () => {
const messages = [createMessage({ type: "ask", ask: "tool", partial: false })]
// text is undefined by default
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.currentAsk).toBe("tool")
})
it("should handle ask message with empty text field", () => {
const messages = [createMessage({ type: "ask", ask: "followup", partial: false, text: "" })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.currentAsk).toBe("followup")
})
it("should handle say message with missing text field", () => {
const messages = [createMessage({ say: "text" })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RUNNING)
})
})
describe("api_req_started edge cases", () => {
it("should handle api_req_started with empty text field as streaming", () => {
const messages = [createMessage({ say: "api_req_started", text: "" })]
const state = detectAgentState(messages)
// Empty text is treated as "no text yet" = still in progress (streaming)
// This matches the behavior: !message.text is true for "" (falsy)
expect(state.state).toBe(AgentLoopState.STREAMING)
expect(state.isStreaming).toBe(true)
})
it("should handle api_req_started with invalid JSON", () => {
const messages = [createMessage({ say: "api_req_started", text: "not valid json" })]
const state = detectAgentState(messages)
// Invalid JSON should not crash, should return not streaming
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.isStreaming).toBe(false)
})
it("should handle api_req_started with null text", () => {
const messages = [createMessage({ say: "api_req_started", text: undefined })]
const state = detectAgentState(messages)
// No text means still in progress (streaming)
expect(state.state).toBe(AgentLoopState.STREAMING)
expect(state.isStreaming).toBe(true)
})
it("should handle api_req_started with cost of 0", () => {
const messages = [createMessage({ say: "api_req_started", text: JSON.stringify({ cost: 0 }) })]
const state = detectAgentState(messages)
// cost: 0 is defined (not undefined), so NOT streaming
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.isStreaming).toBe(false)
})
it("should handle api_req_started with cost of null", () => {
const messages = [createMessage({ say: "api_req_started", text: JSON.stringify({ cost: null }) })]
const state = detectAgentState(messages)
// cost: null is defined (not undefined), so NOT streaming
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.isStreaming).toBe(false)
})
it("should find api_req_started when it's not the last message", () => {
const messages = [
createMessage({ say: "api_req_started", text: JSON.stringify({ tokensIn: 100 }) }), // No cost = streaming
createMessage({ say: "text", text: "Some text" }),
]
const state = detectAgentState(messages)
// Last message is say:text, but api_req_started has no cost
expect(state.state).toBe(AgentLoopState.STREAMING)
expect(state.isStreaming).toBe(true)
})
})
describe("Rapid state transitions", () => {
it("should handle multiple rapid state changes", () => {
const { client } = createMockClient()
const states: AgentLoopState[] = []
client.onStateChange((event) => {
states.push(event.currentState.state)
})
// Rapid updates
client.handleMessage(createStateMessage([createMessage({ say: "text" })]))
client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: true })]))
client.handleMessage(createStateMessage([createMessage({ type: "ask", ask: "tool", partial: false })]))
client.handleMessage(
createStateMessage([createMessage({ type: "ask", ask: "completion_result", partial: false })]),
)
// Should have tracked all transitions
expect(states.length).toBeGreaterThanOrEqual(3)
expect(states).toContain(AgentLoopState.STREAMING)
expect(states).toContain(AgentLoopState.WAITING_FOR_INPUT)
expect(states).toContain(AgentLoopState.IDLE)
})
})
describe("Message array edge cases", () => {
it("should handle single message array", () => {
const messages = [createMessage({ say: "text", text: "Hello" })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RUNNING)
expect(state.lastMessage).toBeDefined()
expect(state.lastMessageTs).toBe(messages[0]!.ts)
})
it("should use last message for state detection", () => {
// Multiple messages, last one determines state
const messages = [
createMessage({ type: "ask", ask: "tool", partial: false }),
createMessage({ say: "text", text: "Tool executed" }),
createMessage({ type: "ask", ask: "completion_result", partial: false }),
]
const state = detectAgentState(messages)
// Last message is completion_result, so IDLE
expect(state.state).toBe(AgentLoopState.IDLE)
expect(state.currentAsk).toBe("completion_result")
})
it("should handle very long message arrays", () => {
// Create many messages
const messages: ClineMessage[] = []
for (let i = 0; i < 100; i++) {
messages.push(createMessage({ say: "text", text: `Message ${i}` }))
}
messages.push(createMessage({ type: "ask", ask: "followup", partial: false }))
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state.currentAsk).toBe("followup")
})
})
describe("State message edge cases", () => {
it("should handle state message with empty clineMessages", () => {
const { client } = createMockClient()
client.handleMessage({
type: "state",
state: {
clineMessages: [],
},
})
expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
expect(client.isInitialized()).toBe(true)
})
it("should handle state message with missing clineMessages", () => {
const { client } = createMockClient()
client.handleMessage({
type: "state",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
state: {} as any,
})
// Should not crash, state should remain unchanged
expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
})
it("should handle state message with missing state field", () => {
const { client } = createMockClient()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client.handleMessage({ type: "state" } as any)
// Should not crash
expect(client.getCurrentState()).toBe(AgentLoopState.NO_TASK)
})
})
describe("Partial to complete transitions", () => {
it("should transition from streaming to waiting when partial becomes false", () => {
const ts = Date.now()
const messages1 = [createMessage({ ts, type: "ask", ask: "tool", partial: true })]
const messages2 = [createMessage({ ts, type: "ask", ask: "tool", partial: false })]
const state1 = detectAgentState(messages1)
const state2 = detectAgentState(messages2)
expect(state1.state).toBe(AgentLoopState.STREAMING)
expect(state1.isWaitingForInput).toBe(false)
expect(state2.state).toBe(AgentLoopState.WAITING_FOR_INPUT)
expect(state2.isWaitingForInput).toBe(true)
})
it("should handle partial say messages", () => {
const messages = [createMessage({ say: "text", text: "Typing...", partial: true })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.STREAMING)
expect(state.isStreaming).toBe(true)
})
})
describe("Unknown message types", () => {
it("should handle unknown ask types gracefully", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messages = [createMessage({ type: "ask", ask: "unknown_type" as any, partial: false })]
const state = detectAgentState(messages)
// Unknown ask type should default to RUNNING
expect(state.state).toBe(AgentLoopState.RUNNING)
})
it("should handle unknown say types gracefully", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messages = [createMessage({ say: "unknown_say_type" as any })]
const state = detectAgentState(messages)
expect(state.state).toBe(AgentLoopState.RUNNING)
})
})
})