generated from nhcarrigan/template
bf411adeb7
## Summary This PR resolves several critical bugs that were blocking the permission modal and causing config loss: - **Permission modal not appearing** - Fixed z-index issues and runtime errors - **Config store race condition** - Resolved critical race condition causing settings to be lost - **Excessive logging** - Removed redundant fmt layer that was writing to hidden stdout - **System tool prompts** - Prevented unnecessary permission prompts for built-in tools - **Permission batching** - Added support for parallel permission requests - **ExitPlanMode tool** - Fixed ExitPlanMode tool not functioning correctly ## Changes Made ### Permission Modal Fixes - Updated z-index to proper value (9999) to ensure modal appears above all other UI elements - Fixed runtime errors that were preventing modal from rendering - Resolved issues with permission grants not being properly applied ### Config Store Race Condition - Fixed critical race condition where multiple rapid config updates would result in lost settings - Ensured config writes are properly sequenced to prevent data loss - Added proper synchronisation for config store operations ### Logging Cleanup - Removed redundant fmt formatting layer that was outputting to hidden stdout - Cleaned up excessive debug logging added during troubleshooting - Removed temporary debugging documentation files ### UX Improvements - Added close confirmation modal with minimise to tray option - Implemented batching for parallel permission requests - Added debug console for viewing frontend and backend logs ### ExitPlanMode Fix - Fixed ExitPlanMode tool not functioning correctly, ensuring proper transitions out of plan mode ## Issues Resolved Closes #112 - Permission flow now properly handles multiple tool requests Closes #113 - ExitPlanMode tool now functions correctly Closes #126 - Debug console feature added (partial - basic implementation complete) ## Test Plan - [x] Permission modal appears and functions correctly - [x] Config settings persist across app restarts - [x] No excessive logging in production builds - [x] System tools don't trigger permission prompts - [x] Parallel permission requests are properly batched - [x] Debug console displays frontend and backend logs - [x] ExitPlanMode properly exits plan mode --- ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #127 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
416 lines
12 KiB
TypeScript
416 lines
12 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { mapMessageToState, extractTextFromMessage, extractToolInfo } from "./stateMapper";
|
|
import type { ClaudeStreamMessage } from "$lib/types/messages";
|
|
|
|
describe("stateMapper", () => {
|
|
describe("mapMessageToState", () => {
|
|
it("returns idle for system init message", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "system",
|
|
subtype: "init",
|
|
session_id: "test-session",
|
|
cwd: "/home/test",
|
|
tools: ["Read", "Write", "Edit"],
|
|
};
|
|
expect(mapMessageToState(message)).toBe("idle");
|
|
});
|
|
|
|
it("returns null for non-init system messages", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "system",
|
|
subtype: "compact_boundary",
|
|
};
|
|
expect(mapMessageToState(message)).toBeNull();
|
|
});
|
|
|
|
it("returns searching for Read tool", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "Read",
|
|
input: { file_path: "/test/file.txt" },
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("searching");
|
|
});
|
|
|
|
it("returns coding for Edit tool", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "Edit",
|
|
input: { file_path: "/test/file.txt", old_string: "foo", new_string: "bar" },
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("coding");
|
|
});
|
|
|
|
it("returns mcp for mcp__ prefixed tools", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "mcp__github__list_repos",
|
|
input: {},
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("mcp");
|
|
});
|
|
|
|
it("returns thinking for Task tool", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "Task",
|
|
input: { prompt: "test task" },
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("thinking");
|
|
});
|
|
|
|
it("returns typing for text content", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [{ type: "text", text: "Hello, Naomi!" }],
|
|
model: "claude-3",
|
|
stop_reason: "end_turn",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("typing");
|
|
});
|
|
|
|
it("returns success for result success message", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "result",
|
|
subtype: "success",
|
|
result: "Task completed",
|
|
};
|
|
expect(mapMessageToState(message)).toBe("success");
|
|
});
|
|
|
|
it("returns error for result error message", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "result",
|
|
subtype: "error_max_turns",
|
|
};
|
|
expect(mapMessageToState(message)).toBe("error");
|
|
});
|
|
|
|
it("returns null for user messages", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "user",
|
|
message: { content: [{ type: "text", text: "Hello" }] },
|
|
};
|
|
expect(mapMessageToState(message)).toBeNull();
|
|
});
|
|
|
|
it("returns typing for unknown tool", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "SomeUnknownTool",
|
|
input: {},
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("typing");
|
|
});
|
|
|
|
it("returns thinking for thinking content block", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [{ type: "thinking", thinking: "Analyzing the problem..." }],
|
|
model: "claude-3",
|
|
stop_reason: "end_turn",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("thinking");
|
|
});
|
|
|
|
it("returns null for assistant message with no recognizable content", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [],
|
|
model: "claude-3",
|
|
stop_reason: "end_turn",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBeNull();
|
|
});
|
|
|
|
it("returns thinking for thinking_delta stream event", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_delta",
|
|
index: 0,
|
|
delta: {
|
|
type: "thinking_delta",
|
|
thinking: "Thinking...",
|
|
},
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("thinking");
|
|
});
|
|
|
|
it("returns typing for text_delta stream event", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_delta",
|
|
index: 0,
|
|
delta: {
|
|
type: "text_delta",
|
|
text: "Hello",
|
|
},
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("typing");
|
|
});
|
|
|
|
it("returns thinking for thinking content_block_start", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_start",
|
|
index: 0,
|
|
content_block: {
|
|
type: "thinking",
|
|
thinking: "",
|
|
},
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("thinking");
|
|
});
|
|
|
|
it("returns typing for text content_block_start", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_start",
|
|
index: 0,
|
|
content_block: {
|
|
type: "text",
|
|
text: "",
|
|
},
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("typing");
|
|
});
|
|
|
|
it("returns correct state for tool_use content_block_start", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_start",
|
|
index: 0,
|
|
content_block: {
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "Read",
|
|
input: {},
|
|
},
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBe("searching");
|
|
});
|
|
|
|
it("returns null for stream_event with unrecognized type", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "message_start",
|
|
},
|
|
};
|
|
expect(mapMessageToState(message)).toBeNull();
|
|
});
|
|
|
|
it("returns null for result with unknown subtype", () => {
|
|
const message = {
|
|
type: "result",
|
|
subtype: "unknown_type",
|
|
} as unknown as ClaudeStreamMessage;
|
|
expect(mapMessageToState(message)).toBeNull();
|
|
});
|
|
|
|
it("returns null for unknown message type", () => {
|
|
const message = {
|
|
type: "unknown_type",
|
|
} as unknown as ClaudeStreamMessage;
|
|
expect(mapMessageToState(message)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("extractTextFromMessage", () => {
|
|
it("extracts text from assistant message", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{ type: "text", text: "Hello!" },
|
|
{ type: "text", text: "How are you?" },
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "end_turn",
|
|
},
|
|
};
|
|
expect(extractTextFromMessage(message)).toBe("Hello!\nHow are you?");
|
|
});
|
|
|
|
it("returns null for assistant message without text", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "Read",
|
|
input: { file_path: "/test/file.txt" },
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
expect(extractTextFromMessage(message)).toBeNull();
|
|
});
|
|
|
|
it("extracts text from stream_event delta", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_delta",
|
|
index: 0,
|
|
delta: { type: "text_delta", text: "streaming text" },
|
|
},
|
|
};
|
|
expect(extractTextFromMessage(message)).toBe("streaming text");
|
|
});
|
|
|
|
it("extracts result from result message", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "result",
|
|
subtype: "success",
|
|
result: "Completed successfully",
|
|
};
|
|
expect(extractTextFromMessage(message)).toBe("Completed successfully");
|
|
});
|
|
|
|
it("returns null for result without result field", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "result",
|
|
subtype: "success",
|
|
};
|
|
expect(extractTextFromMessage(message)).toBeNull();
|
|
});
|
|
|
|
it("returns null for stream_event without delta text", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "stream_event",
|
|
event: {
|
|
type: "content_block_start",
|
|
index: 0,
|
|
content_block: {
|
|
type: "text",
|
|
text: "",
|
|
},
|
|
},
|
|
};
|
|
expect(extractTextFromMessage(message)).toBeNull();
|
|
});
|
|
|
|
it("returns null for unknown message type", () => {
|
|
const message = {
|
|
type: "unknown",
|
|
} as unknown as ClaudeStreamMessage;
|
|
expect(extractTextFromMessage(message)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("extractToolInfo", () => {
|
|
it("extracts tool info from assistant message", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "assistant",
|
|
message: {
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-1",
|
|
name: "Read",
|
|
input: { file_path: "/test/file.txt" },
|
|
},
|
|
{
|
|
type: "tool_use",
|
|
id: "tool-2",
|
|
name: "Edit",
|
|
input: { file_path: "/test/file.txt", old_string: "a", new_string: "b" },
|
|
},
|
|
],
|
|
model: "claude-3",
|
|
stop_reason: "tool_use",
|
|
},
|
|
};
|
|
const tools = extractToolInfo(message);
|
|
expect(tools).toHaveLength(2);
|
|
expect(tools[0]).toEqual({
|
|
name: "Read",
|
|
input: { file_path: "/test/file.txt" },
|
|
});
|
|
expect(tools[1]).toEqual({
|
|
name: "Edit",
|
|
input: { file_path: "/test/file.txt", old_string: "a", new_string: "b" },
|
|
});
|
|
});
|
|
|
|
it("returns empty array for non-assistant messages", () => {
|
|
const message: ClaudeStreamMessage = {
|
|
type: "user",
|
|
message: { content: [{ type: "text", text: "Hello" }] },
|
|
};
|
|
expect(extractToolInfo(message)).toEqual([]);
|
|
});
|
|
});
|
|
});
|