Merge pull request 'Integrate M4-LLM error handling into develop' (#349) from feature/m4-llm-integration into develop
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #349
This commit was merged in pull request #349.
This commit is contained in:
152
apps/web/src/components/chat/Chat.test.tsx
Normal file
152
apps/web/src/components/chat/Chat.test.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @file Chat.test.tsx
|
||||
* @description Tests for Chat component error handling in imperative handle methods
|
||||
*/
|
||||
|
||||
import { createRef } from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, it, expect, beforeEach, vi, afterEach, type MockedFunction } from "vitest";
|
||||
import { Chat, type ChatRef } from "./Chat";
|
||||
import * as useChatModule from "@/hooks/useChat";
|
||||
import * as useWebSocketModule from "@/hooks/useWebSocket";
|
||||
import * as authModule from "@/lib/auth/auth-context";
|
||||
|
||||
// Mock scrollIntoView (not available in JSDOM)
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/auth/auth-context", () => ({
|
||||
useAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useChat", () => ({
|
||||
useChat: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useWebSocket", () => ({
|
||||
useWebSocket: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./MessageList", () => ({
|
||||
MessageList: (): React.ReactElement => <div data-testid="message-list" />,
|
||||
}));
|
||||
|
||||
vi.mock("./ChatInput", () => ({
|
||||
ChatInput: ({
|
||||
onSend,
|
||||
}: {
|
||||
onSend: (content: string) => Promise<void>;
|
||||
disabled: boolean;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
}): React.ReactElement => (
|
||||
<button data-testid="chat-input" onClick={(): void => void onSend("test message")}>
|
||||
Send
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockUseAuth = authModule.useAuth as MockedFunction<typeof authModule.useAuth>;
|
||||
const mockUseChat = useChatModule.useChat as MockedFunction<typeof useChatModule.useChat>;
|
||||
const mockUseWebSocket = useWebSocketModule.useWebSocket as MockedFunction<
|
||||
typeof useWebSocketModule.useWebSocket
|
||||
>;
|
||||
|
||||
function createMockUseChatReturn(
|
||||
overrides: Partial<useChatModule.UseChatReturn> = {}
|
||||
): useChatModule.UseChatReturn {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
id: "welcome",
|
||||
role: "assistant",
|
||||
content: "Hello!",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
conversationId: null,
|
||||
conversationTitle: null,
|
||||
sendMessage: vi.fn().mockResolvedValue(undefined),
|
||||
loadConversation: vi.fn().mockResolvedValue(undefined),
|
||||
startNewConversation: vi.fn(),
|
||||
setMessages: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Chat", () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { id: "user-1", name: "Test User", email: "test@test.com" },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
} as unknown as ReturnType<typeof authModule.useAuth>);
|
||||
|
||||
mockUseWebSocket.mockReturnValue({
|
||||
isConnected: true,
|
||||
socket: null,
|
||||
connectionError: null,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("loadConversation via ref", () => {
|
||||
it("should delegate to useChat.loadConversation", async () => {
|
||||
const mockLoadConversation = vi.fn().mockResolvedValue(undefined);
|
||||
mockUseChat.mockReturnValue(
|
||||
createMockUseChatReturn({ loadConversation: mockLoadConversation })
|
||||
);
|
||||
|
||||
const ref = createRef<ChatRef>();
|
||||
render(<Chat ref={ref} />);
|
||||
|
||||
await ref.current?.loadConversation("conv-123");
|
||||
|
||||
expect(mockLoadConversation).toHaveBeenCalledWith("conv-123");
|
||||
});
|
||||
|
||||
it("should not re-throw when useChat.loadConversation handles errors internally", async () => {
|
||||
// useChat.loadConversation handles errors internally (sets error state, logs, calls onError)
|
||||
// and does NOT re-throw, so the imperative handle should resolve cleanly
|
||||
const mockLoadConversation = vi.fn().mockResolvedValue(undefined);
|
||||
mockUseChat.mockReturnValue(
|
||||
createMockUseChatReturn({ loadConversation: mockLoadConversation })
|
||||
);
|
||||
|
||||
const ref = createRef<ChatRef>();
|
||||
render(<Chat ref={ref} />);
|
||||
|
||||
// Should resolve without throwing
|
||||
await expect(ref.current?.loadConversation("conv-123")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessage delegation", () => {
|
||||
it("should delegate to useChat.sendMessage", async () => {
|
||||
const mockSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
mockUseChat.mockReturnValue(createMockUseChatReturn({ sendMessage: mockSendMessage }));
|
||||
|
||||
const ref = createRef<ChatRef>();
|
||||
const { getByTestId } = render(<Chat ref={ref} />);
|
||||
|
||||
const sendButton = getByTestId("chat-input");
|
||||
sendButton.click();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockSendMessage).toHaveBeenCalledWith("test message");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -73,9 +73,6 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
||||
} = useChat({
|
||||
model: "llama3.2",
|
||||
...(initialProjectId !== undefined && { projectId: initialProjectId }),
|
||||
onError: (_err) => {
|
||||
// Error is handled by the useChat hook's state
|
||||
},
|
||||
});
|
||||
|
||||
// Connect to WebSocket for real-time updates (when we have a user)
|
||||
@@ -96,8 +93,8 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
|
||||
|
||||
// Expose methods to parent via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
loadConversation: async (conversationId: string): Promise<void> => {
|
||||
await loadConversation(conversationId);
|
||||
loadConversation: async (cId: string): Promise<void> => {
|
||||
await loadConversation(cId);
|
||||
},
|
||||
startNewConversation: (projectId?: string | null): void => {
|
||||
startNewConversation(projectId);
|
||||
|
||||
@@ -33,6 +33,9 @@ const mockSendChatMessage = chatApi.sendChatMessage as MockedFunction<
|
||||
const mockCreateConversation = ideasApi.createConversation as MockedFunction<
|
||||
typeof ideasApi.createConversation
|
||||
>;
|
||||
const mockUpdateConversation = ideasApi.updateConversation as MockedFunction<
|
||||
typeof ideasApi.updateConversation
|
||||
>;
|
||||
const mockGetIdea = ideasApi.getIdea as MockedFunction<typeof ideasApi.getIdea>;
|
||||
|
||||
/**
|
||||
@@ -156,6 +159,7 @@ describe("useChat", () => {
|
||||
});
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("API Error"));
|
||||
|
||||
const onError = vi.fn();
|
||||
@@ -165,11 +169,11 @@ describe("useChat", () => {
|
||||
await result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("API Error");
|
||||
expect(result.current.error).toBe("Unable to send message. Please try again.");
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
||||
// Should have welcome + user + error message
|
||||
expect(result.current.messages).toHaveLength(3);
|
||||
expect(result.current.messages[2]?.content).toContain("Error: API Error");
|
||||
expect(result.current.messages[2]?.content).toBe("Something went wrong. Please try again.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -373,6 +377,44 @@ describe("useChat", () => {
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0]?.id).toBe("welcome");
|
||||
});
|
||||
|
||||
it("should set sanitized error and call onError when getIdea rejects", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockGetIdea.mockRejectedValueOnce(new Error("Not found"));
|
||||
|
||||
const onError = vi.fn();
|
||||
const { result } = renderHook(() => useChat({ onError }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadConversation("conv-missing");
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Unable to load conversation. Please try again.");
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to load conversation",
|
||||
expect.objectContaining({
|
||||
errorType: "LOAD_ERROR",
|
||||
ideaId: "conv-missing",
|
||||
timestamp: expect.any(String) as string,
|
||||
})
|
||||
);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("should not re-throw when getIdea rejects", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockGetIdea.mockRejectedValueOnce(new Error("Server error"));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
// Should resolve without throwing - errors are handled internally
|
||||
await act(async () => {
|
||||
await expect(result.current.loadConversation("conv-err")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Unable to load conversation. Please try again.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("startNewConversation", () => {
|
||||
@@ -403,6 +445,7 @@ describe("useChat", () => {
|
||||
|
||||
describe("clearError", () => {
|
||||
it("should clear error state", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("Test error"));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
@@ -411,7 +454,7 @@ describe("useChat", () => {
|
||||
await result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Test error");
|
||||
expect(result.current.error).toBe("Unable to send message. Please try again.");
|
||||
|
||||
act(() => {
|
||||
result.current.clearError();
|
||||
@@ -420,4 +463,237 @@ describe("useChat", () => {
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("error context logging", () => {
|
||||
it("should log comprehensive error context when sendMessage fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("LLM timeout"));
|
||||
|
||||
const { result } = renderHook(() => useChat({ model: "llama3.2" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Hello world");
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to send chat message",
|
||||
expect.objectContaining({
|
||||
errorType: "LLM_ERROR",
|
||||
messageLength: 11,
|
||||
messagePreview: "Hello world",
|
||||
model: "llama3.2",
|
||||
timestamp: expect.any(String) as string,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should truncate long message previews to 50 characters", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("Failed"));
|
||||
|
||||
const longMessage = "A".repeat(100);
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage(longMessage);
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to send chat message",
|
||||
expect.objectContaining({
|
||||
messagePreview: "A".repeat(50),
|
||||
messageLength: 100,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should include message count in error context", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
// First successful message
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("OK"));
|
||||
mockCreateConversation.mockResolvedValueOnce(createMockIdea("conv-1", "Test", ""));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("First");
|
||||
});
|
||||
|
||||
// Second message fails
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("Fail"));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Second");
|
||||
});
|
||||
|
||||
// messageCount should reflect messages including the new user message
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to send chat message",
|
||||
expect.objectContaining({
|
||||
messageCount: expect.any(Number) as number,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LLM vs persistence error separation", () => {
|
||||
it("should show LLM error and add error message to chat when API fails", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("Model not available"));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Unable to send message. Please try again.");
|
||||
// Should have welcome + user + error message
|
||||
expect(result.current.messages).toHaveLength(3);
|
||||
expect(result.current.messages[2]?.content).toBe("Something went wrong. Please try again.");
|
||||
});
|
||||
|
||||
it("should keep assistant message visible when save fails", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("Great answer!"));
|
||||
mockCreateConversation.mockRejectedValueOnce(new Error("Database connection lost"));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
// Assistant message should still be visible
|
||||
expect(result.current.messages).toHaveLength(3); // welcome + user + assistant
|
||||
expect(result.current.messages[2]?.content).toBe("Great answer!");
|
||||
|
||||
// Error should indicate persistence failure
|
||||
expect(result.current.error).toContain("Message sent but failed to save");
|
||||
});
|
||||
|
||||
it("should log with PERSISTENCE_ERROR type when save fails", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("Response"));
|
||||
mockCreateConversation.mockRejectedValueOnce(new Error("DB error"));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Test");
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to save conversation",
|
||||
expect.objectContaining({
|
||||
errorType: "PERSISTENCE_ERROR",
|
||||
})
|
||||
);
|
||||
|
||||
// Should NOT have logged as LLM_ERROR
|
||||
const llmErrorCalls = consoleSpy.mock.calls.filter((call) => {
|
||||
const ctx: unknown = call[1];
|
||||
return (
|
||||
typeof ctx === "object" &&
|
||||
ctx !== null &&
|
||||
"errorType" in ctx &&
|
||||
(ctx as { errorType: string }).errorType === "LLM_ERROR"
|
||||
);
|
||||
});
|
||||
expect(llmErrorCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should use different user-facing messages for LLM vs save errors", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
// Test LLM error message
|
||||
mockSendChatMessage.mockRejectedValueOnce(new Error("Timeout"));
|
||||
const { result: result1 } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result1.current.sendMessage("Test");
|
||||
});
|
||||
|
||||
const llmError = result1.current.error;
|
||||
|
||||
// Test save error message
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("OK"));
|
||||
mockCreateConversation.mockRejectedValueOnce(new Error("DB down"));
|
||||
const { result: result2 } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result2.current.sendMessage("Test");
|
||||
});
|
||||
|
||||
const saveError = result2.current.error;
|
||||
|
||||
// They should be different
|
||||
expect(llmError).toBe("Unable to send message. Please try again.");
|
||||
expect(saveError).toContain("Message sent but failed to save");
|
||||
expect(llmError).not.toEqual(saveError);
|
||||
});
|
||||
|
||||
it("should handle non-Error throws from LLM API", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockRejectedValueOnce("string error");
|
||||
|
||||
const onError = vi.fn();
|
||||
const { result } = renderHook(() => useChat({ onError }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Unable to send message. Please try again.");
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(result.current.messages[2]?.content).toBe("Something went wrong. Please try again.");
|
||||
});
|
||||
|
||||
it("should handle non-Error throws from persistence layer", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("OK"));
|
||||
mockCreateConversation.mockRejectedValueOnce("DB string error");
|
||||
|
||||
const onError = vi.fn();
|
||||
const { result } = renderHook(() => useChat({ onError }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Hello");
|
||||
});
|
||||
|
||||
// Assistant message should still be visible
|
||||
expect(result.current.messages[2]?.content).toBe("OK");
|
||||
expect(result.current.error).toBe("Message sent but failed to save. Please try again.");
|
||||
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
||||
});
|
||||
|
||||
it("should handle updateConversation failure for existing conversations", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
// First message succeeds and creates conversation
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("First response"));
|
||||
mockCreateConversation.mockResolvedValueOnce(createMockIdea("conv-1", "Test", ""));
|
||||
|
||||
const { result } = renderHook(() => useChat());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("First");
|
||||
});
|
||||
|
||||
expect(result.current.conversationId).toBe("conv-1");
|
||||
|
||||
// Second message succeeds but updateConversation fails
|
||||
mockSendChatMessage.mockResolvedValueOnce(createMockChatResponse("Second response"));
|
||||
mockUpdateConversation.mockRejectedValueOnce(new Error("Connection reset"));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage("Second");
|
||||
});
|
||||
|
||||
// Assistant message should still be visible
|
||||
expect(result.current.messages[4]?.content).toBe("Second response");
|
||||
expect(result.current.error).toBe("Message sent but failed to save. Please try again.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,18 +208,41 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
||||
? generateTitle(content)
|
||||
: (conversationTitle ?? "Chat Conversation");
|
||||
|
||||
// Save conversation
|
||||
await saveConversation(finalMessages, title);
|
||||
// Save conversation (separate error handling from LLM errors)
|
||||
try {
|
||||
await saveConversation(finalMessages, title);
|
||||
} catch (saveErr) {
|
||||
const saveErrorMsg =
|
||||
saveErr instanceof Error ? saveErr.message : "Unknown persistence error";
|
||||
setError("Message sent but failed to save. Please try again.");
|
||||
onError?.(saveErr instanceof Error ? saveErr : new Error(saveErrorMsg));
|
||||
console.error("Failed to save conversation", {
|
||||
error: saveErr,
|
||||
errorType: "PERSISTENCE_ERROR",
|
||||
conversationId,
|
||||
detail: saveErrorMsg,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : "Failed to send message";
|
||||
setError(errorMsg);
|
||||
setError("Unable to send message. Please try again.");
|
||||
onError?.(err instanceof Error ? err : new Error(errorMsg));
|
||||
console.error("Failed to send chat message", {
|
||||
error: err,
|
||||
errorType: "LLM_ERROR",
|
||||
conversationId,
|
||||
messageLength: content.length,
|
||||
messagePreview: content.substring(0, 50),
|
||||
model,
|
||||
messageCount: messagesRef.current.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Add error message to chat
|
||||
const errorMessage: Message = {
|
||||
id: `error-${String(Date.now())}`,
|
||||
role: "assistant",
|
||||
content: `Error: ${errorMsg}`,
|
||||
content: "Something went wrong. Please try again.",
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
@@ -259,8 +282,14 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
||||
setConversationTitle(idea.title ?? null);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : "Failed to load conversation";
|
||||
setError(errorMsg);
|
||||
setError("Unable to load conversation. Please try again.");
|
||||
onError?.(err instanceof Error ? err : new Error(errorMsg));
|
||||
console.error("Failed to load conversation", {
|
||||
error: err,
|
||||
errorType: "LOAD_ERROR",
|
||||
ideaId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ describe("useChatOverlay", () => {
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it("should not change minimized state when toggling", () => {
|
||||
it("should reset minimized state when toggling closed", () => {
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
act(() => {
|
||||
@@ -274,8 +274,24 @@ describe("useChatOverlay", () => {
|
||||
result.current.toggle();
|
||||
});
|
||||
|
||||
// Should close but keep minimized state for next open
|
||||
// Should close and reset minimized (invariant: cannot be minimized when closed)
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
|
||||
it("should preserve minimized state when toggling open", () => {
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
// Start closed
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.toggle();
|
||||
});
|
||||
|
||||
// Should open and not be minimized
|
||||
expect(result.current.isOpen).toBe(true);
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -305,4 +321,153 @@ describe("useChatOverlay", () => {
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("state invariant enforcement", () => {
|
||||
it("should reject invalid localStorage state: closed AND minimized", () => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
// This violates the invariant: cannot be minimized when closed
|
||||
localStorageMock.setItem(
|
||||
"chatOverlayState",
|
||||
JSON.stringify({ isOpen: false, isMinimized: true })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
// Should fall back to defaults since invariant is violated
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
|
||||
it("should accept valid state: open and minimized", () => {
|
||||
localStorageMock.setItem(
|
||||
"chatOverlayState",
|
||||
JSON.stringify({ isOpen: true, isMinimized: true })
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
expect(result.current.isOpen).toBe(true);
|
||||
expect(result.current.isMinimized).toBe(true);
|
||||
});
|
||||
|
||||
it("should reset isMinimized when closing via close()", () => {
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
act(() => {
|
||||
result.current.open();
|
||||
result.current.minimize();
|
||||
});
|
||||
|
||||
expect(result.current.isMinimized).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.close();
|
||||
});
|
||||
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("minimize/toggleMinimize guards", () => {
|
||||
it("should not allow minimize when overlay is closed", () => {
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
// Overlay starts closed
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.minimize();
|
||||
});
|
||||
|
||||
// Should remain unchanged - cannot minimize when closed
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
|
||||
it("should not allow toggleMinimize when overlay is closed", () => {
|
||||
const { result } = renderHook(() => useChatOverlay());
|
||||
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
|
||||
act(() => {
|
||||
result.current.toggleMinimize();
|
||||
});
|
||||
|
||||
// Should remain unchanged - cannot toggle minimize when closed
|
||||
expect(result.current.isOpen).toBe(false);
|
||||
expect(result.current.isMinimized).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("storage error handling", () => {
|
||||
it("should call onStorageError when localStorage save fails", () => {
|
||||
const onStorageError = vi.fn();
|
||||
const quotaError = new DOMException("Storage full", "QuotaExceededError");
|
||||
|
||||
const { result } = renderHook(() => useChatOverlay({ onStorageError }));
|
||||
|
||||
// Make setItem throw
|
||||
vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
|
||||
throw quotaError;
|
||||
});
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.open();
|
||||
});
|
||||
|
||||
expect(onStorageError).toHaveBeenCalledWith(
|
||||
"Storage is full. Chat state may not persist across page refreshes."
|
||||
);
|
||||
});
|
||||
|
||||
it("should show appropriate message for SecurityError", () => {
|
||||
const onStorageError = vi.fn();
|
||||
const securityError = new DOMException("Blocked", "SecurityError");
|
||||
|
||||
const { result } = renderHook(() => useChatOverlay({ onStorageError }));
|
||||
|
||||
vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
|
||||
throw securityError;
|
||||
});
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
act(() => {
|
||||
result.current.open();
|
||||
});
|
||||
|
||||
expect(onStorageError).toHaveBeenCalledWith(
|
||||
"Storage is unavailable in this browser mode. Chat state will not persist."
|
||||
);
|
||||
});
|
||||
|
||||
it("should only notify once per error type", () => {
|
||||
const onStorageError = vi.fn();
|
||||
const quotaError = new DOMException("Storage full", "QuotaExceededError");
|
||||
|
||||
// Set up spy BEFORE rendering so all saves (including initial) throw
|
||||
vi.spyOn(window.localStorage, "setItem").mockImplementation(() => {
|
||||
throw quotaError;
|
||||
});
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
|
||||
const { result } = renderHook(() => useChatOverlay({ onStorageError }));
|
||||
|
||||
// Initial render triggers a save which throws → first notification
|
||||
// Multiple state changes that trigger more saves
|
||||
act(() => {
|
||||
result.current.open();
|
||||
});
|
||||
act(() => {
|
||||
result.current.minimize();
|
||||
});
|
||||
act(() => {
|
||||
result.current.expand();
|
||||
});
|
||||
|
||||
// Should only have been called once for QuotaExceededError despite multiple failures
|
||||
expect(onStorageError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description Hook for managing the global chat overlay state
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { safeJsonParse, isChatOverlayState } from "@/lib/utils/safe-json";
|
||||
|
||||
interface ChatOverlayState {
|
||||
@@ -11,6 +11,10 @@ interface ChatOverlayState {
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
export interface UseChatOverlayOptions {
|
||||
onStorageError?: (message: string) => void;
|
||||
}
|
||||
|
||||
interface UseChatOverlayResult extends ChatOverlayState {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
@@ -27,6 +31,23 @@ const DEFAULT_STATE: ChatOverlayState = {
|
||||
isMinimized: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user-friendly error message for localStorage failures
|
||||
*/
|
||||
function getStorageErrorMessage(error: unknown): string {
|
||||
if (error instanceof DOMException) {
|
||||
switch (error.name) {
|
||||
case "QuotaExceededError":
|
||||
return "Storage is full. Chat state may not persist across page refreshes.";
|
||||
case "SecurityError":
|
||||
return "Storage is unavailable in this browser mode. Chat state will not persist.";
|
||||
case "InvalidStateError":
|
||||
return "Storage is disabled. Chat state will not persist across page refreshes.";
|
||||
}
|
||||
}
|
||||
return "Unable to save chat state. It may not persist across page refreshes.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Load state from localStorage with runtime type validation
|
||||
*/
|
||||
@@ -48,9 +69,13 @@ function loadState(): ChatOverlayState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save state to localStorage
|
||||
* Save state to localStorage, notifying on error (once per error type)
|
||||
*/
|
||||
function saveState(state: ChatOverlayState): void {
|
||||
function saveState(
|
||||
state: ChatOverlayState,
|
||||
onStorageError: ((message: string) => void) | undefined,
|
||||
notifiedErrors: Set<string>
|
||||
): void {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
@@ -58,20 +83,36 @@ function saveState(state: ChatOverlayState): void {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
const errorName =
|
||||
error instanceof DOMException
|
||||
? error.name
|
||||
: error instanceof Error
|
||||
? error.constructor.name
|
||||
: "UnknownError";
|
||||
console.warn("Failed to save chat overlay state to localStorage:", error);
|
||||
|
||||
if (onStorageError && !notifiedErrors.has(errorName)) {
|
||||
notifiedErrors.add(errorName);
|
||||
onStorageError(getStorageErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing chat overlay state
|
||||
* Persists state to localStorage for consistency across page refreshes
|
||||
* Persists state to localStorage for consistency across page refreshes.
|
||||
* Enforces invariant: cannot be minimized when closed.
|
||||
*/
|
||||
export function useChatOverlay(): UseChatOverlayResult {
|
||||
export function useChatOverlay(options: UseChatOverlayOptions = {}): UseChatOverlayResult {
|
||||
const { onStorageError } = options;
|
||||
const [state, setState] = useState<ChatOverlayState>(loadState);
|
||||
const notifiedErrorsRef = useRef<Set<string>>(new Set());
|
||||
const onStorageErrorRef = useRef(onStorageError);
|
||||
onStorageErrorRef.current = onStorageError;
|
||||
|
||||
// Persist state changes to localStorage
|
||||
useEffect(() => {
|
||||
saveState(state);
|
||||
saveState(state, onStorageErrorRef.current, notifiedErrorsRef.current);
|
||||
}, [state]);
|
||||
|
||||
const open = useCallback(() => {
|
||||
@@ -79,11 +120,11 @@ export function useChatOverlay(): UseChatOverlayResult {
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setState((prev) => ({ ...prev, isOpen: false }));
|
||||
setState({ isOpen: false, isMinimized: false });
|
||||
}, []);
|
||||
|
||||
const minimize = useCallback(() => {
|
||||
setState((prev) => ({ ...prev, isMinimized: true }));
|
||||
setState((prev) => (prev.isOpen ? { ...prev, isMinimized: true } : prev));
|
||||
}, []);
|
||||
|
||||
const expand = useCallback(() => {
|
||||
@@ -91,11 +132,14 @@ export function useChatOverlay(): UseChatOverlayResult {
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setState((prev) => ({ ...prev, isOpen: !prev.isOpen }));
|
||||
setState((prev) => {
|
||||
const newIsOpen = !prev.isOpen;
|
||||
return { isOpen: newIsOpen, isMinimized: newIsOpen ? prev.isMinimized : false };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleMinimize = useCallback(() => {
|
||||
setState((prev) => ({ ...prev, isMinimized: !prev.isMinimized }));
|
||||
setState((prev) => (prev.isOpen ? { ...prev, isMinimized: !prev.isMinimized } : prev));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
||||
@@ -170,7 +170,12 @@ describe("isMessageArray", () => {
|
||||
describe("isChatOverlayState", () => {
|
||||
it("should return true for a valid ChatOverlayState", () => {
|
||||
expect(isChatOverlayState({ isOpen: true, isMinimized: false })).toBe(true);
|
||||
expect(isChatOverlayState({ isOpen: false, isMinimized: true })).toBe(true);
|
||||
expect(isChatOverlayState({ isOpen: true, isMinimized: true })).toBe(true);
|
||||
expect(isChatOverlayState({ isOpen: false, isMinimized: false })).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject invalid state: closed AND minimized (invariant violation)", () => {
|
||||
expect(isChatOverlayState({ isOpen: false, isMinimized: true })).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-object values", () => {
|
||||
|
||||
@@ -76,12 +76,16 @@ export function isMessageArray(value: unknown): value is {
|
||||
/**
|
||||
* Type guard: validates ChatOverlayState shape
|
||||
* Expects { isOpen: boolean, isMinimized: boolean }
|
||||
* Enforces invariant: cannot be minimized when closed
|
||||
*/
|
||||
export function isChatOverlayState(
|
||||
value: unknown
|
||||
): value is { isOpen: boolean; isMinimized: boolean } {
|
||||
if (!isRecord(value)) return false;
|
||||
return typeof value.isOpen === "boolean" && typeof value.isMinimized === "boolean";
|
||||
if (typeof value.isOpen !== "boolean" || typeof value.isMinimized !== "boolean") return false;
|
||||
// Invariant: cannot be minimized when closed
|
||||
if (!value.isOpen && value.isMinimized) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -524,6 +524,71 @@ QA automation generates report files in `docs/reports/qa-automation/pending/`. C
|
||||
|
||||
---
|
||||
|
||||
## Sprint Completion Protocol
|
||||
|
||||
When all tasks in `docs/tasks.md` are `done` (or triaged as `deferred`), archive the sprint artifacts before stopping. This preserves them for post-mortems, variance calibration, and historical reference.
|
||||
|
||||
### Archive Steps
|
||||
|
||||
1. **Create archive directory** (if it doesn't exist):
|
||||
|
||||
```bash
|
||||
mkdir -p docs/tasks/
|
||||
```
|
||||
|
||||
2. **Move tasks.md to archive:**
|
||||
|
||||
```bash
|
||||
mv docs/tasks.md docs/tasks/{milestone-name}-tasks.md
|
||||
```
|
||||
|
||||
Example: `docs/tasks/M6-AgentOrchestration-Fixes-tasks.md`
|
||||
|
||||
3. **Move learnings to archive:**
|
||||
|
||||
```bash
|
||||
mv docs/orchestrator-learnings.json docs/tasks/{milestone-name}-learnings.json
|
||||
```
|
||||
|
||||
4. **Commit the archive:**
|
||||
|
||||
```bash
|
||||
git add docs/tasks/
|
||||
git rm docs/tasks.md docs/orchestrator-learnings.json 2>/dev/null || true
|
||||
git commit -m "chore(orchestrator): Archive {milestone-name} sprint artifacts
|
||||
|
||||
{completed}/{total} tasks completed, {deferred} deferred.
|
||||
Archived to docs/tasks/ for post-mortem reference."
|
||||
git push
|
||||
```
|
||||
|
||||
5. **Run final retrospective** — review variance patterns and propose updates to estimation heuristics.
|
||||
|
||||
### Recovery
|
||||
|
||||
If an orchestrator starts and `docs/tasks.md` does not exist, check `docs/tasks/` for the most recent archive:
|
||||
|
||||
```bash
|
||||
ls -t docs/tasks/*-tasks.md 2>/dev/null | head -1
|
||||
```
|
||||
|
||||
If found, this may indicate another session archived the file. The orchestrator should:
|
||||
|
||||
1. Report what it found in `docs/tasks/`
|
||||
2. Ask whether to resume from the archived file or bootstrap fresh
|
||||
3. If resuming: copy the archive back to `docs/tasks.md` and continue
|
||||
|
||||
### Retention Policy
|
||||
|
||||
Keep all archived sprints indefinitely. They are small text files and valuable for:
|
||||
|
||||
- Post-mortem analysis
|
||||
- Estimation variance calibration across milestones
|
||||
- Understanding what was deferred and why
|
||||
- Onboarding new orchestrators to project history
|
||||
|
||||
---
|
||||
|
||||
## Kickstart Message Format
|
||||
|
||||
```markdown
|
||||
|
||||
239
docs/tasks/M6-AgentOrchestration-Fixes-learnings.json
Normal file
239
docs/tasks/M6-AgentOrchestration-Fixes-learnings.json
Normal file
@@ -0,0 +1,239 @@
|
||||
{
|
||||
"project": "mosaic-stack",
|
||||
"milestone": "M6-AgentOrchestration",
|
||||
"created_at": "2026-02-05T20:00:00Z",
|
||||
"learnings": [
|
||||
{
|
||||
"task_id": "MS-SEC-001",
|
||||
"task_type": "AUTH_ADD",
|
||||
"estimate_k": 15,
|
||||
"actual_k": 0.3,
|
||||
"variance_pct": -98,
|
||||
"characteristics": {
|
||||
"file_count": 1,
|
||||
"keywords": ["authentication", "orchestrator API", "ApiKeyGuard"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Investigate. Possible causes: (1) Auth already existed, (2) Task was trivial decorator addition, (3) Reporting error. Need to verify task completion quality.",
|
||||
"flags": ["CRITICAL", "NEEDS_INVESTIGATION"],
|
||||
"captured_at": "2026-02-05T15:30:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-SEC-003",
|
||||
"task_type": "ERROR_HANDLING",
|
||||
"estimate_k": 8,
|
||||
"actual_k": 18.5,
|
||||
"variance_pct": 131,
|
||||
"characteristics": {
|
||||
"file_count": 4,
|
||||
"keywords": ["secret scanner", "error state", "scan result type", "Zod schema"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Task required adding new fields to existing type, updating all callers, modifying error messages, comprehensive error path tests. Type interface changes cascade through codebase.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T16:42:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-SEC-006",
|
||||
"task_type": "CONFIG_DEFAULT_CHANGE",
|
||||
"estimate_k": 10,
|
||||
"actual_k": 18,
|
||||
"variance_pct": 80,
|
||||
"characteristics": {
|
||||
"file_count": 3,
|
||||
"keywords": ["Docker sandbox", "default enabled", "security warning", "config test"]
|
||||
},
|
||||
"analysis": "Underestimated test coverage needed. New config test file (8 tests) + security warning tests (2 tests) required more tokens than simple default flip.",
|
||||
"flags": [],
|
||||
"captured_at": "2026-02-05T16:05:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-SEC-010",
|
||||
"task_type": "INPUT_VALIDATION",
|
||||
"estimate_k": 5,
|
||||
"actual_k": 8.5,
|
||||
"variance_pct": 70,
|
||||
"characteristics": {
|
||||
"file_count": 2,
|
||||
"keywords": ["OAuth callback", "error sanitization", "allowlist", "encodeURIComponent"]
|
||||
},
|
||||
"analysis": "Underestimated allowlist complexity. Required 18 OAuth 2.0/OIDC error codes, URL encoding for all params, and 5 comprehensive security tests.",
|
||||
"flags": [],
|
||||
"captured_at": "2026-02-05T16:36:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-SEC-011",
|
||||
"task_type": "CONFIG_EXTERNALIZATION",
|
||||
"estimate_k": 8,
|
||||
"actual_k": 15,
|
||||
"variance_pct": 87.5,
|
||||
"characteristics": {
|
||||
"file_count": 2,
|
||||
"keywords": ["OIDC", "federation", "env vars", "trailing slash normalization"]
|
||||
},
|
||||
"analysis": "Underestimated integration complexity. Required reusing auth.config OIDC vars, handling trailing slash differences between auth config and JWT validation, adding fail-fast logic, and 5 new tests.",
|
||||
"flags": [],
|
||||
"captured_at": "2026-02-05T16:45:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-SEC-012",
|
||||
"task_type": "BUG_FIX_SIMPLE",
|
||||
"estimate_k": 3,
|
||||
"actual_k": 12.5,
|
||||
"variance_pct": 317,
|
||||
"characteristics": {
|
||||
"file_count": 2,
|
||||
"keywords": ["boolean logic", "nullish coalescing", "ReactFlow", "handleDeleteSelected"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Estimate was for simple operator change (?? to ||), but task expanded to add 13 comprehensive tests covering all boolean logic scenarios. 'Simple fix' tasks with untested code should include test addition in estimate.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T16:55:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-HIGH-001",
|
||||
"task_type": "NULLABLE_REFACTOR",
|
||||
"estimate_k": 8,
|
||||
"actual_k": 12.5,
|
||||
"variance_pct": 56,
|
||||
"characteristics": {
|
||||
"file_count": 2,
|
||||
"keywords": ["OpenAI", "nullable client", "embedding service", "graceful degradation"]
|
||||
},
|
||||
"analysis": "Making a service client nullable requires updating all call sites with null checks and adding tests for the unconfigured path. Estimate should include caller updates.",
|
||||
"flags": [],
|
||||
"captured_at": "2026-02-05T17:27:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-HIGH-004",
|
||||
"task_type": "OBSERVABILITY_ADD",
|
||||
"estimate_k": 10,
|
||||
"actual_k": 22,
|
||||
"variance_pct": 120,
|
||||
"characteristics": {
|
||||
"file_count": 2,
|
||||
"keywords": ["rate limiter", "fallback", "health check", "degraded mode"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Adding observability to a service requires: (1) tracking state variables, (2) new methods for status exposure, (3) integration with health check system, (4) comprehensive test coverage for all states. Estimate 2x for 'add health check' tasks.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T18:02:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-HIGH-006",
|
||||
"task_type": "RATE_LIMITING_ADD",
|
||||
"estimate_k": 8,
|
||||
"actual_k": 25,
|
||||
"variance_pct": 213,
|
||||
"characteristics": {
|
||||
"file_count": 3,
|
||||
"keywords": ["rate limiting", "catch-all route", "IP extraction", "X-Forwarded-For"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Adding rate limiting requires: (1) understanding existing throttle infrastructure, (2) IP extraction helpers for proxy setups, (3) new test file for rate limit behavior, (4) Retry-After header testing. Estimate 3x for rate limiting tasks.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T18:22:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-HIGH-007",
|
||||
"task_type": "CONFIG_VALIDATION",
|
||||
"estimate_k": 5,
|
||||
"actual_k": 18,
|
||||
"variance_pct": 260,
|
||||
"characteristics": {
|
||||
"file_count": 4,
|
||||
"keywords": ["UUID validation", "federation", "startup validation", "config file"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - 'Simple validation' tasks expand to: (1) new config module/file, (2) validation function with edge cases, (3) module init hook integration, (4) updating callers to use new config getter, (5) 18 comprehensive tests. Estimate 3-4x for config validation tasks.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T18:35:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-HIGH-008",
|
||||
"task_type": "SECURITY_REFACTOR",
|
||||
"estimate_k": 12,
|
||||
"actual_k": 25,
|
||||
"variance_pct": 108,
|
||||
"characteristics": {
|
||||
"file_count": 5,
|
||||
"keywords": ["CSRF", "fetch replacement", "API client", "FormData upload"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Routing fetch() through API client required: (1) adding new apiPostFormData() method for FormData, (2) finding additional calls not in original finding, (3) updating test mocks to handle CSRF fetches, (4) handling different Content-Type scenarios. Multi-file refactors expand beyond listed files.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T18:50:00Z"
|
||||
},
|
||||
{
|
||||
"task_id": "MS-HIGH-009",
|
||||
"task_type": "FEATURE_GATING",
|
||||
"estimate_k": 10,
|
||||
"actual_k": 30,
|
||||
"variance_pct": 200,
|
||||
"characteristics": {
|
||||
"file_count": 6,
|
||||
"keywords": ["NODE_ENV", "mock data", "Coming Soon component", "environment check"]
|
||||
},
|
||||
"analysis": "CRITICAL VARIANCE - Feature gating requires: (1) creating reusable placeholder component, (2) tests for the component, (3) updating multiple pages, (4) environment-specific logic in each page. Creating reusable UI components adds significant overhead.",
|
||||
"flags": ["CRITICAL"],
|
||||
"captured_at": "2026-02-05T19:05:00Z"
|
||||
}
|
||||
],
|
||||
"phase_summaries": [
|
||||
{
|
||||
"phase": 4,
|
||||
"name": "Remaining Medium Findings",
|
||||
"issue": "#347",
|
||||
"total_tasks": 12,
|
||||
"completed": 12,
|
||||
"failed": 0,
|
||||
"deferred": 0,
|
||||
"total_estimate_k": 117,
|
||||
"total_actual_k": 231,
|
||||
"variance_pct": 97,
|
||||
"analysis": "Phase 4 estimates consistently under-predicted actual usage. Average task used 2x estimated tokens. Primary driver: DTO creation and comprehensive test suites expand scope beyond the core fix. The N+1 query fix (MS-P4-009) and TOCTOU race fix (MS-P4-010) were particularly complex. All 12 tasks completed successfully with zero failures.",
|
||||
"test_counts": {
|
||||
"api": 2397,
|
||||
"web": 653,
|
||||
"orchestrator": 642,
|
||||
"shared": 17,
|
||||
"ui": 11
|
||||
},
|
||||
"completed_at": "2026-02-06T14:22:00Z"
|
||||
},
|
||||
{
|
||||
"phase": 5,
|
||||
"name": "Low Priority - Cleanup + Performance",
|
||||
"issue": "#340",
|
||||
"total_tasks": 17,
|
||||
"completed": 17,
|
||||
"failed": 0,
|
||||
"deferred": 0,
|
||||
"total_estimate_k": 155,
|
||||
"total_actual_k": 878,
|
||||
"variance_pct": 466,
|
||||
"analysis": "Phase 5 estimates were consistently 5-6x lower than actual usage. Primary drivers: (1) workers spend significant tokens reading context files before implementing fixes, (2) comprehensive test creation dominates usage, (3) multi-finding batched tasks (e.g. MS-P5-009 at 93K for 2 findings) expand beyond estimates. All 17 tasks completed successfully with zero failures across 26 findings.",
|
||||
"test_counts": {
|
||||
"api": 2432,
|
||||
"web": 786,
|
||||
"orchestrator": 682,
|
||||
"shared": 17,
|
||||
"ui": 11
|
||||
},
|
||||
"completed_at": "2026-02-06T18:54:00Z"
|
||||
}
|
||||
],
|
||||
"proposed_adjustments": [
|
||||
{
|
||||
"category": "AUTH_ADD",
|
||||
"current_heuristic": "15-25K",
|
||||
"proposed_heuristic": "NO CHANGE NEEDED",
|
||||
"confidence": "HIGH",
|
||||
"evidence": ["MS-SEC-001"],
|
||||
"notes": "Investigation complete: -98% variance was REPORTING ANOMALY, not estimation error. Actual implementation was 276 lines (guard + tests + docs). Token usage reporting may have bug. Heuristic is accurate."
|
||||
}
|
||||
],
|
||||
"investigation_queue": [
|
||||
{
|
||||
"task_id": "MS-SEC-001",
|
||||
"question": "Did this task actually add authentication, or was auth already present?",
|
||||
"priority": "HIGH",
|
||||
"status": "CLOSED",
|
||||
"resolution": "LEGITIMATE COMPLETION - Implementation verified: OrchestratorApiKeyGuard with 82 lines of guard code, 169 lines of tests, 6 files changed, 276 total lines. The 0.3K token usage was a REPORTING ANOMALY, not incomplete work.",
|
||||
"verified_at": "2026-02-05T20:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
89
docs/tasks/M6-AgentOrchestration-Fixes-tasks.md
Normal file
89
docs/tasks/M6-AgentOrchestration-Fixes-tasks.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Tasks
|
||||
|
||||
| id | status | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used |
|
||||
| ----------- | -------- | --------------------------------------------------------------------- | ----- | ------------ | ------------ | ----------- | ----------- | -------- | -------------------- | -------------------- | -------- | ----- |
|
||||
| MS-SEC-001 | done | SEC-ORCH-2: Add authentication to orchestrator API | #337 | orchestrator | fix/security | | MS-SEC-002 | worker-1 | 2026-02-05T15:15:00Z | 2026-02-05T15:25:00Z | 15K | 0.3K |
|
||||
| MS-SEC-002 | done | SEC-WEB-2: Fix WikiLinkRenderer XSS (sanitize HTML before wiki-links) | #337 | web | fix/security | MS-SEC-001 | MS-SEC-003 | worker-1 | 2026-02-05T15:26:00Z | 2026-02-05T15:35:00Z | 8K | 8.5K |
|
||||
| MS-SEC-003 | done | SEC-ORCH-1: Fix secret scanner error handling (return error state) | #337 | orchestrator | fix/security | MS-SEC-002 | MS-SEC-004 | worker-1 | 2026-02-05T15:36:00Z | 2026-02-05T15:42:00Z | 8K | 18.5K |
|
||||
| MS-SEC-004 | done | SEC-API-2+3: Fix guards swallowing DB errors (propagate as 500s) | #337 | api | fix/security | MS-SEC-003 | MS-SEC-005 | worker-1 | 2026-02-05T15:43:00Z | 2026-02-05T15:50:00Z | 10K | 15K |
|
||||
| MS-SEC-005 | done | SEC-API-1: Validate OIDC config at startup (fail fast if missing) | #337 | api | fix/security | MS-SEC-004 | MS-SEC-006 | worker-1 | 2026-02-05T15:51:00Z | 2026-02-05T15:58:00Z | 8K | 12K |
|
||||
| MS-SEC-006 | done | SEC-ORCH-3: Enable Docker sandbox by default, warn when disabled | #337 | orchestrator | fix/security | MS-SEC-005 | MS-SEC-007 | worker-1 | 2026-02-05T15:59:00Z | 2026-02-05T16:05:00Z | 10K | 18K |
|
||||
| MS-SEC-007 | done | SEC-ORCH-4: Add auth to inter-service communication (API key) | #337 | orchestrator | fix/security | MS-SEC-006 | MS-SEC-008 | worker-1 | 2026-02-05T16:06:00Z | 2026-02-05T16:12:00Z | 15K | 12.5K |
|
||||
| MS-SEC-008 | done | SEC-ORCH-5+CQ-ORCH-3: Replace KEYS with SCAN in Valkey client | #337 | orchestrator | fix/security | MS-SEC-007 | MS-SEC-009 | worker-1 | 2026-02-05T16:13:00Z | 2026-02-05T16:19:00Z | 12K | 12.5K |
|
||||
| MS-SEC-009 | done | SEC-ORCH-6: Add Zod validation for deserialized Redis data | #337 | orchestrator | fix/security | MS-SEC-008 | MS-SEC-010 | worker-1 | 2026-02-05T16:20:00Z | 2026-02-05T16:28:00Z | 12K | 12.5K |
|
||||
| MS-SEC-010 | done | SEC-WEB-1: Sanitize OAuth callback error parameter | #337 | web | fix/security | MS-SEC-009 | MS-SEC-011 | worker-1 | 2026-02-05T16:30:00Z | 2026-02-05T16:36:00Z | 5K | 8.5K |
|
||||
| MS-SEC-011 | done | CQ-API-6: Replace hardcoded OIDC values with env vars | #337 | api | fix/security | MS-SEC-010 | MS-SEC-012 | worker-1 | 2026-02-05T16:37:00Z | 2026-02-05T16:45:00Z | 8K | 15K |
|
||||
| MS-SEC-012 | done | CQ-WEB-5: Fix boolean logic bug in ReactFlowEditor | #337 | web | fix/security | MS-SEC-011 | MS-SEC-013 | worker-1 | 2026-02-05T16:46:00Z | 2026-02-05T16:55:00Z | 3K | 12.5K |
|
||||
| MS-SEC-013 | done | SEC-API-4: Add workspaceId query verification tests | #337 | api | fix/security | MS-SEC-012 | MS-SEC-V01 | worker-1 | 2026-02-05T16:56:00Z | 2026-02-05T17:05:00Z | 20K | 18.5K |
|
||||
| MS-SEC-V01 | done | Phase 1 Verification: Run full quality gates | #337 | all | fix/security | MS-SEC-013 | MS-HIGH-001 | worker-1 | 2026-02-05T17:06:00Z | 2026-02-05T17:18:00Z | 5K | 2K |
|
||||
| MS-HIGH-001 | done | SEC-API-5: Fix OpenAI embedding service dummy key handling | #338 | api | fix/high | MS-SEC-V01 | MS-HIGH-002 | worker-1 | 2026-02-05T17:19:00Z | 2026-02-05T17:27:00Z | 8K | 12.5K |
|
||||
| MS-HIGH-002 | done | SEC-API-6: Add structured logging for embedding failures | #338 | api | fix/high | MS-HIGH-001 | MS-HIGH-003 | worker-1 | 2026-02-05T17:28:00Z | 2026-02-05T17:36:00Z | 8K | 12K |
|
||||
| MS-HIGH-003 | done | SEC-API-7: Bind CSRF token to session with HMAC | #338 | api | fix/high | MS-HIGH-002 | MS-HIGH-004 | worker-1 | 2026-02-05T17:37:00Z | 2026-02-05T17:50:00Z | 12K | 12.5K |
|
||||
| MS-HIGH-004 | done | SEC-API-8: Log ERROR on rate limiter fallback, add health check | #338 | api | fix/high | MS-HIGH-003 | MS-HIGH-005 | worker-1 | 2026-02-05T17:51:00Z | 2026-02-05T18:02:00Z | 10K | 22K |
|
||||
| MS-HIGH-005 | done | SEC-API-9: Implement proper system admin role | #338 | api | fix/high | MS-HIGH-004 | MS-HIGH-006 | worker-1 | 2026-02-05T18:03:00Z | 2026-02-05T18:12:00Z | 15K | 8.5K |
|
||||
| MS-HIGH-006 | done | SEC-API-10: Add rate limiting to auth catch-all | #338 | api | fix/high | MS-HIGH-005 | MS-HIGH-007 | worker-1 | 2026-02-05T18:13:00Z | 2026-02-05T18:22:00Z | 8K | 25K |
|
||||
| MS-HIGH-007 | done | SEC-API-11: Validate DEFAULT_WORKSPACE_ID as UUID | #338 | api | fix/high | MS-HIGH-006 | MS-HIGH-008 | worker-1 | 2026-02-05T18:23:00Z | 2026-02-05T18:35:00Z | 5K | 18K |
|
||||
| MS-HIGH-008 | done | SEC-WEB-3: Route all fetch() through API client (CSRF) | #338 | web | fix/high | MS-HIGH-007 | MS-HIGH-009 | worker-1 | 2026-02-05T18:36:00Z | 2026-02-05T18:50:00Z | 12K | 25K |
|
||||
| MS-HIGH-009 | done | SEC-WEB-4: Gate mock data behind NODE_ENV check | #338 | web | fix/high | MS-HIGH-008 | MS-HIGH-010 | worker-1 | 2026-02-05T18:51:00Z | 2026-02-05T19:05:00Z | 10K | 30K |
|
||||
| MS-HIGH-010 | done | SEC-WEB-5: Log auth errors, distinguish backend down | #338 | web | fix/high | MS-HIGH-009 | MS-HIGH-011 | worker-1 | 2026-02-05T19:06:00Z | 2026-02-05T19:18:00Z | 8K | 12.5K |
|
||||
| MS-HIGH-011 | done | SEC-WEB-6: Enforce WSS, add connect_error handling | #338 | web | fix/high | MS-HIGH-010 | MS-HIGH-012 | worker-1 | 2026-02-05T19:19:00Z | 2026-02-05T19:32:00Z | 8K | 15K |
|
||||
| MS-HIGH-012 | done | SEC-WEB-7+CQ-WEB-7: Implement optimistic rollback on Kanban | #338 | web | fix/high | MS-HIGH-011 | MS-HIGH-013 | worker-1 | 2026-02-05T19:33:00Z | 2026-02-05T19:55:00Z | 12K | 35K |
|
||||
| MS-HIGH-013 | done | SEC-WEB-8: Handle non-OK responses in ActiveProjectsWidget | #338 | web | fix/high | MS-HIGH-012 | MS-HIGH-014 | worker-1 | 2026-02-05T19:56:00Z | 2026-02-05T20:05:00Z | 8K | 18.5K |
|
||||
| MS-HIGH-014 | done | SEC-WEB-9: Disable QuickCaptureWidget with Coming Soon | #338 | web | fix/high | MS-HIGH-013 | MS-HIGH-015 | worker-1 | 2026-02-05T20:06:00Z | 2026-02-05T20:18:00Z | 5K | 12.5K |
|
||||
| MS-HIGH-015 | done | SEC-WEB-10+11: Standardize API base URL and auth mechanism | #338 | web | fix/high | MS-HIGH-014 | MS-HIGH-016 | worker-1 | 2026-02-05T20:19:00Z | 2026-02-05T20:30:00Z | 12K | 8.5K |
|
||||
| MS-HIGH-016 | done | SEC-ORCH-7: Add circuit breaker to coordinator loops | #338 | coordinator | fix/high | MS-HIGH-015 | MS-HIGH-017 | worker-1 | 2026-02-05T20:31:00Z | 2026-02-05T20:42:00Z | 15K | 18.5K |
|
||||
| MS-HIGH-017 | done | SEC-ORCH-8: Log queue corruption, backup file | #338 | coordinator | fix/high | MS-HIGH-016 | MS-HIGH-018 | worker-1 | 2026-02-05T20:43:00Z | 2026-02-05T20:50:00Z | 10K | 12.5K |
|
||||
| MS-HIGH-018 | done | SEC-ORCH-9: Whitelist allowed env vars in Docker | #338 | orchestrator | fix/high | MS-HIGH-017 | MS-HIGH-019 | worker-1 | 2026-02-05T20:51:00Z | 2026-02-05T21:00:00Z | 10K | 32K |
|
||||
| MS-HIGH-019 | done | SEC-ORCH-10: Add CapDrop, ReadonlyRootfs, PidsLimit | #338 | orchestrator | fix/high | MS-HIGH-018 | MS-HIGH-020 | worker-1 | 2026-02-05T21:01:00Z | 2026-02-05T21:10:00Z | 12K | 25K |
|
||||
| MS-HIGH-020 | done | SEC-ORCH-11: Add rate limiting to orchestrator API | #338 | orchestrator | fix/high | MS-HIGH-019 | MS-HIGH-021 | worker-1 | 2026-02-05T21:11:00Z | 2026-02-05T21:20:00Z | 10K | 12.5K |
|
||||
| MS-HIGH-021 | done | SEC-ORCH-12: Add max concurrent agents limit | #338 | orchestrator | fix/high | MS-HIGH-020 | MS-HIGH-022 | worker-1 | 2026-02-05T21:21:00Z | 2026-02-05T21:28:00Z | 8K | 12.5K |
|
||||
| MS-HIGH-022 | done | SEC-ORCH-13: Block YOLO mode in production | #338 | orchestrator | fix/high | MS-HIGH-021 | MS-HIGH-023 | worker-1 | 2026-02-05T21:29:00Z | 2026-02-05T21:35:00Z | 8K | 12K |
|
||||
| MS-HIGH-023 | done | SEC-ORCH-14: Sanitize issue body for prompt injection | #338 | coordinator | fix/high | MS-HIGH-022 | MS-HIGH-024 | worker-1 | 2026-02-05T21:36:00Z | 2026-02-05T21:42:00Z | 12K | 12.5K |
|
||||
| MS-HIGH-024 | done | SEC-ORCH-15: Warn when VALKEY_PASSWORD not set | #338 | orchestrator | fix/high | MS-HIGH-023 | MS-HIGH-025 | worker-1 | 2026-02-05T21:43:00Z | 2026-02-05T21:50:00Z | 5K | 6.5K |
|
||||
| MS-HIGH-025 | done | CQ-ORCH-6: Fix N+1 with MGET for batch retrieval | #338 | orchestrator | fix/high | MS-HIGH-024 | MS-HIGH-026 | worker-1 | 2026-02-05T21:51:00Z | 2026-02-05T21:58:00Z | 10K | 8.5K |
|
||||
| MS-HIGH-026 | done | CQ-ORCH-1: Add session cleanup on terminal states | #338 | orchestrator | fix/high | MS-HIGH-025 | MS-HIGH-027 | worker-1 | 2026-02-05T21:59:00Z | 2026-02-05T22:07:00Z | 10K | 12.5K |
|
||||
| MS-HIGH-027 | done | CQ-API-1: Fix WebSocket timer leak (clearTimeout in catch) | #338 | api | fix/high | MS-HIGH-026 | MS-HIGH-028 | worker-1 | 2026-02-05T22:08:00Z | 2026-02-05T22:15:00Z | 8K | 12K |
|
||||
| MS-HIGH-028 | done | CQ-API-2: Fix runner jobs interval leak (clearInterval) | #338 | api | fix/high | MS-HIGH-027 | MS-HIGH-029 | worker-1 | 2026-02-05T22:16:00Z | 2026-02-05T22:24:00Z | 8K | 12K |
|
||||
| MS-HIGH-029 | done | CQ-WEB-1: Fix useWebSocket stale closure (use refs) | #338 | web | fix/high | MS-HIGH-028 | MS-HIGH-030 | worker-1 | 2026-02-05T22:25:00Z | 2026-02-05T22:32:00Z | 10K | 12.5K |
|
||||
| MS-HIGH-030 | done | CQ-WEB-4: Fix useChat stale messages (functional updates) | #338 | web | fix/high | MS-HIGH-029 | MS-HIGH-V01 | worker-1 | 2026-02-05T22:33:00Z | 2026-02-05T22:38:00Z | 10K | 12K |
|
||||
| MS-HIGH-V01 | done | Phase 2 Verification: Run full quality gates | #338 | all | fix/high | MS-HIGH-030 | MS-MED-001 | worker-1 | 2026-02-05T22:40:00Z | 2026-02-05T22:45:00Z | 5K | 2K |
|
||||
| MS-MED-001 | done | CQ-ORCH-4: Fix AbortController timeout cleanup in finally | #339 | orchestrator | fix/medium | MS-HIGH-V01 | MS-MED-002 | worker-1 | 2026-02-05T22:50:00Z | 2026-02-05T22:55:00Z | 8K | 6K |
|
||||
| MS-MED-002 | done | CQ-API-4: Remove Redis event listeners in onModuleDestroy | #339 | api | fix/medium | MS-MED-001 | MS-MED-003 | worker-1 | 2026-02-05T22:56:00Z | 2026-02-05T23:00:00Z | 8K | 5K |
|
||||
| MS-MED-003 | done | SEC-ORCH-16: Implement real health and readiness checks | #339 | orchestrator | fix/medium | MS-MED-002 | MS-MED-004 | worker-1 | 2026-02-05T23:01:00Z | 2026-02-05T23:10:00Z | 12K | 12K |
|
||||
| MS-MED-004 | done | SEC-ORCH-19: Validate agentId path parameter as UUID | #339 | orchestrator | fix/medium | MS-MED-003 | MS-MED-005 | worker-1 | 2026-02-05T23:11:00Z | 2026-02-05T23:15:00Z | 8K | 4K |
|
||||
| MS-MED-005 | done | SEC-API-24: Sanitize error messages in global exception filter | #339 | api | fix/medium | MS-MED-004 | MS-MED-006 | worker-1 | 2026-02-05T23:16:00Z | 2026-02-05T23:25:00Z | 10K | 12K |
|
||||
| MS-MED-006 | deferred | SEC-WEB-16: Add Content Security Policy headers | #339 | web | fix/medium | MS-MED-005 | MS-MED-007 | | | | 12K | |
|
||||
| MS-MED-007 | done | CQ-API-3: Make activity logging fire-and-forget | #339 | api | fix/medium | MS-MED-006 | MS-MED-008 | worker-1 | 2026-02-05T23:28:00Z | 2026-02-05T23:32:00Z | 8K | 5K |
|
||||
| MS-MED-008 | deferred | CQ-ORCH-2: Use Valkey as single source of truth for sessions | #339 | orchestrator | fix/medium | MS-MED-007 | MS-MED-V01 | | | | 15K | |
|
||||
| MS-MED-V01 | done | Phase 3 Verification: Run full quality gates | #339 | all | fix/medium | MS-MED-008 | | worker-1 | 2026-02-05T23:35:00Z | 2026-02-06T00:30:00Z | 5K | 2K |
|
||||
| MS-P4-001 | done | CQ-WEB-2: Fix missing dependency in FilterBar useEffect | #347 | web | fix/security | MS-MED-V01 | MS-P4-002 | worker-1 | 2026-02-06T13:10:00Z | 2026-02-06T13:13:00Z | 10K | 12K |
|
||||
| MS-P4-002 | done | CQ-WEB-3: Fix race condition in LinkAutocomplete (AbortController) | #347 | web | fix/security | MS-P4-001 | MS-P4-003 | worker-1 | 2026-02-06T13:14:00Z | 2026-02-06T13:20:00Z | 12K | 25K |
|
||||
| MS-P4-003 | done | SEC-API-17: Block data: URI scheme in markdown renderer | #347 | api | fix/security | MS-P4-002 | MS-P4-004 | worker-1 | 2026-02-06T13:21:00Z | 2026-02-06T13:25:00Z | 8K | 12K |
|
||||
| MS-P4-004 | done | SEC-API-19+20: Validate brain search length and limit params | #347 | api | fix/security | MS-P4-003 | MS-P4-005 | worker-1 | 2026-02-06T13:26:00Z | 2026-02-06T13:32:00Z | 8K | 25K |
|
||||
| MS-P4-005 | done | SEC-API-21: Add DTO validation for semantic/hybrid search body | #347 | api | fix/security | MS-P4-004 | MS-P4-006 | worker-1 | 2026-02-06T13:33:00Z | 2026-02-06T13:39:00Z | 10K | 25K |
|
||||
| MS-P4-006 | done | SEC-API-12: Throw error when CurrentUser decorator has no user | #347 | api | fix/security | MS-P4-005 | MS-P4-007 | worker-1 | 2026-02-06T13:40:00Z | 2026-02-06T13:44:00Z | 8K | 15K |
|
||||
| MS-P4-007 | done | SEC-ORCH-20: Bind orchestrator to 127.0.0.1, configurable via env | #347 | orchestrator | fix/security | MS-P4-006 | MS-P4-008 | worker-1 | 2026-02-06T13:45:00Z | 2026-02-06T13:48:00Z | 5K | 12K |
|
||||
| MS-P4-008 | done | SEC-ORCH-22: Validate Docker image tag format before pull | #347 | orchestrator | fix/security | MS-P4-007 | MS-P4-009 | worker-1 | 2026-02-06T13:49:00Z | 2026-02-06T13:53:00Z | 8K | 15K |
|
||||
| MS-P4-009 | done | CQ-API-7: Fix N+1 query in knowledge tag lookup (use findMany) | #347 | api | fix/security | MS-P4-008 | MS-P4-010 | worker-1 | 2026-02-06T13:54:00Z | 2026-02-06T14:04:00Z | 8K | 25K |
|
||||
| MS-P4-010 | done | CQ-ORCH-5: Fix TOCTOU race in agent state transitions | #347 | orchestrator | fix/security | MS-P4-009 | MS-P4-011 | worker-1 | 2026-02-06T14:05:00Z | 2026-02-06T14:10:00Z | 15K | 25K |
|
||||
| MS-P4-011 | done | CQ-ORCH-7: Graceful Docker container shutdown before force remove | #347 | orchestrator | fix/security | MS-P4-010 | MS-P4-012 | worker-1 | 2026-02-06T14:11:00Z | 2026-02-06T14:14:00Z | 10K | 15K |
|
||||
| MS-P4-012 | done | CQ-ORCH-9: Deduplicate spawn validation logic | #347 | orchestrator | fix/security | MS-P4-011 | MS-P4-V01 | worker-1 | 2026-02-06T14:15:00Z | 2026-02-06T14:18:00Z | 10K | 25K |
|
||||
| MS-P4-V01 | done | Phase 4 Verification: Run full quality gates | #347 | all | fix/security | MS-P4-012 | | worker-1 | 2026-02-06T14:19:00Z | 2026-02-06T14:22:00Z | 5K | 2K |
|
||||
| MS-P5-001 | done | SEC-API-25+26: ValidationPipe strict mode + CORS Origin validation | #340 | api | fix/security | MS-P4-V01 | MS-P5-002 | worker-1 | 2026-02-06T15:00:00Z | 2026-02-06T15:04:00Z | 10K | 47K |
|
||||
| MS-P5-002 | done | SEC-API-27: Move RLS context setting inside transaction boundary | #340 | api | fix/security | MS-P5-001 | MS-P5-003 | worker-1 | 2026-02-06T15:05:00Z | 2026-02-06T15:10:00Z | 8K | 48K |
|
||||
| MS-P5-003 | done | SEC-API-28: Replace MCP console.error with NestJS Logger | #340 | api | fix/security | MS-P5-002 | MS-P5-004 | worker-1 | 2026-02-06T15:11:00Z | 2026-02-06T15:15:00Z | 5K | 40K |
|
||||
| MS-P5-004 | done | CQ-API-5: Document throttler in-memory fallback as best-effort | #340 | api | fix/security | MS-P5-003 | MS-P5-005 | worker-1 | 2026-02-06T15:16:00Z | 2026-02-06T15:19:00Z | 5K | 38K |
|
||||
| MS-P5-005 | done | SEC-ORCH-28+29: Add Valkey connection timeout + workItems MaxLength | #340 | orchestrator | fix/security | MS-P5-004 | MS-P5-006 | worker-1 | 2026-02-06T15:20:00Z | 2026-02-06T15:24:00Z | 8K | 72K |
|
||||
| MS-P5-006 | done | SEC-ORCH-30: Prevent container name collision with unique suffix | #340 | orchestrator | fix/security | MS-P5-005 | MS-P5-007 | worker-1 | 2026-02-06T15:25:00Z | 2026-02-06T15:27:00Z | 5K | 55K |
|
||||
| MS-P5-007 | done | CQ-ORCH-10: Make BullMQ job retention configurable via env vars | #340 | orchestrator | fix/security | MS-P5-006 | MS-P5-008 | worker-1 | 2026-02-06T15:28:00Z | 2026-02-06T15:32:00Z | 8K | 66K |
|
||||
| MS-P5-008 | done | SEC-WEB-26+29: Remove console.log + fix formatTime error handling | #340 | web | fix/security | MS-P5-007 | MS-P5-009 | worker-1 | 2026-02-06T15:33:00Z | 2026-02-06T15:37:00Z | 5K | 50K |
|
||||
| MS-P5-009 | done | SEC-WEB-27+28: Robust email validation + role cast validation | #340 | web | fix/security | MS-P5-008 | MS-P5-010 | worker-1 | 2026-02-06T15:38:00Z | 2026-02-06T15:48:00Z | 8K | 93K |
|
||||
| MS-P5-010 | done | SEC-WEB-30+31+36: Validate JSON.parse/localStorage deserialization | #340 | web | fix/security | MS-P5-009 | MS-P5-011 | worker-1 | 2026-02-06T15:49:00Z | 2026-02-06T15:56:00Z | 15K | 76K |
|
||||
| MS-P5-011 | done | SEC-WEB-32+34: Add input maxLength limits + API request timeout | #340 | web | fix/security | MS-P5-010 | MS-P5-012 | worker-1 | 2026-02-06T15:57:00Z | 2026-02-06T18:12:00Z | 10K | 50K |
|
||||
| MS-P5-012 | done | SEC-WEB-33+35: Fix Mermaid error display + useWorkspaceId error | #340 | web | fix/security | MS-P5-011 | MS-P5-013 | worker-1 | 2026-02-06T18:13:00Z | 2026-02-06T18:18:00Z | 8K | 55K |
|
||||
| MS-P5-013 | done | SEC-WEB-37: Gate federation mock data behind NODE_ENV check | #340 | web | fix/security | MS-P5-012 | MS-P5-014 | worker-1 | 2026-02-06T18:19:00Z | 2026-02-06T18:25:00Z | 8K | 54K |
|
||||
| MS-P5-014 | done | CQ-WEB-8: Add React.memo to performance-sensitive components | #340 | web | fix/security | MS-P5-013 | MS-P5-015 | worker-1 | 2026-02-06T18:26:00Z | 2026-02-06T18:32:00Z | 15K | 82K |
|
||||
| MS-P5-015 | done | CQ-WEB-9: Replace DOM manipulation in LinkAutocomplete | #340 | web | fix/security | MS-P5-014 | MS-P5-016 | worker-1 | 2026-02-06T18:33:00Z | 2026-02-06T18:37:00Z | 10K | 37K |
|
||||
| MS-P5-016 | done | CQ-WEB-10: Add loading/error states to pages with mock data | #340 | web | fix/security | MS-P5-015 | MS-P5-017 | worker-1 | 2026-02-06T18:38:00Z | 2026-02-06T18:45:00Z | 15K | 66K |
|
||||
| MS-P5-017 | done | CQ-WEB-11+12: Fix accessibility labels + SSR window check | #340 | web | fix/security | MS-P5-016 | MS-P5-V01 | worker-1 | 2026-02-06T18:46:00Z | 2026-02-06T18:51:00Z | 12K | 65K |
|
||||
| MS-P5-V01 | done | Phase 5 Verification: Run full quality gates | #340 | all | fix/security | MS-P5-017 | | worker-1 | 2026-02-06T18:52:00Z | 2026-02-06T18:54:00Z | 5K | 2K |
|
||||
Reference in New Issue
Block a user