## Summary Restores green **Full Suite (non-blocking)** runs on `main`. Recent main merges left i18n key parity, schema baseline bookkeeping (0011→0012), heartbeat tool inventory (FN-8058 `fn_task_logs_read`), and merger whitespace-classification mocks (execFile `git diff -p -w :2: :3:`) out of date, so all four test shards failed. ## Root causes observed on main - **Shard 4 / `@fusion/i18n`**: missing `skipConfirmationDialogs*` + `reviewBudgetExhausted` in non-en locales; orphan `awaitingApprovalPlanReviewReplanCap` - **Shard 3 / `@fusion/core`**: `SCHEMA_BASELINE_VERSION` advanced to `0012` while tests still equated it with `OWNER_PROJECT_ID_SPLIT_VERSION` (`0011`) and omitted `0012` from applied-migration lists - **Shards 1–2 / `@fusion/engine`**: tool count/snapshot drift for `fn_task_logs_read`; merger tests still mocked `git diff-tree` for trivial classification after the execFile `:2:`/`:3:` cutover; mock provider `updateTask` arity drift ## Changes - Locale catalogs: add missing keys, drop orphan key - Schema applier tests: immutable 0011 identity + baseline 0012 lists - Heartbeat + gating snapshots: include `fn_task_logs_read` - Merger unit mocks: recognize `git diff -p -w :2:path :3:path` - Mock provider: accept optional third `updateTask` arg ## Test plan - [x] `pnpm --filter @fusion/i18n exec vitest run` — 23/23 - [x] `pnpm --filter @fusion/core exec vitest run src/__tests__/postgres/schema-applier.test.ts` (immutable + automation upgrade) — pass - [x] `pnpm --filter @fusion/core exec vitest run` project-identity + satellite-fusiondir — pass - [x] Engine suites from failed CI shards (file-scoped, hermes/openclaw/paperclip/grok, reliability post-finalize/mission, heartbeat, gating, merger recovery/prompt, mock-provider, etc.) — pass - [ ] Full Suite workflow green on merge to main <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved project data isolation across backend operations. - Added safer optional toast handling when UI components render outside the full application shell. - Added support for reading task logs during agent heartbeat sessions. - **Bug Fixes** - Prevented runtime probes from hanging and avoided scanning large binary files. - Improved path handling for workspaces with missing descendants. - Corrected task retry state resets and GitHub import/issue-close behavior. - **Style** - Improved chat, terminal, and settings spacing. - Added clearer accessibility labeling for the auto-merge control. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
217 lines
7.9 KiB
TypeScript
217 lines
7.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { screen, waitFor } from "@testing-library/react";
|
|
import { userEvent } from "@testing-library/user-event";
|
|
import { ChatView } from "../ChatView";
|
|
import {
|
|
activeSessionFixture,
|
|
defaultModelsResponse,
|
|
installChatViewEnv,
|
|
mockFetchModels,
|
|
mockViewportMode,
|
|
renderWithAct,
|
|
setupMockChat,
|
|
setupMockRooms,
|
|
createRoomFixture,
|
|
} from "./ChatView.test-harness";
|
|
import { estimateChatTokens, formatTokenCount } from "../../utils/estimateChatTokens";
|
|
|
|
/*
|
|
FNXC:DashboardTests 2026-06-27-00:00:
|
|
Chat context-window coverage is surface-enumeration driven: desktop Direct chat renders the estimate, while mobile, floating-narrow, rooms, and unknown-model states must omit the element entirely so no empty header shell survives in constrained layouts.
|
|
*/
|
|
|
|
vi.mock("../../hooks/useChat");
|
|
vi.mock("../../hooks/useChatRooms");
|
|
vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../../hooks/useNavigationHistory")>();
|
|
return {
|
|
...actual,
|
|
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
|
|
};
|
|
});
|
|
vi.mock("../../api", () => ({
|
|
fetchSettings: vi.fn().mockResolvedValue({}),
|
|
fetchModels: vi.fn().mockResolvedValue({
|
|
models: [
|
|
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 },
|
|
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 },
|
|
],
|
|
favoriteProviders: [],
|
|
favoriteModels: [],
|
|
defaultProvider: "anthropic",
|
|
defaultModelId: "claude-sonnet-4-5",
|
|
}),
|
|
fetchAgents: vi.fn().mockResolvedValue([
|
|
{ id: "agent-001", name: "Alpha", role: "executor", state: "idle", icon: undefined, createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z", metadata: {} },
|
|
]),
|
|
fetchDiscoveredSkills: vi.fn().mockResolvedValue([]),
|
|
fetchTasks: vi.fn().mockResolvedValue([]),
|
|
searchFiles: vi.fn().mockResolvedValue({ files: [] }),
|
|
}));
|
|
|
|
installChatViewEnv();
|
|
|
|
function expectNoContextWindowShell() {
|
|
expect(screen.queryByTestId("chat-thread-context-window")).not.toBeInTheDocument();
|
|
expect(document.querySelector(".chat-thread-header-context")).not.toBeInTheDocument();
|
|
}
|
|
|
|
/*
|
|
FNXC:ChatHeader 2026-07-16-00:00:
|
|
The mobile and floating-narrow session switcher is rendered only in the direct-thread pane. Tests exercising its header must select the session from the list first, matching the mobile drill-in flow.
|
|
*/
|
|
async function openMobileDirectThread(sessionId = "session-001") {
|
|
await userEvent.click(screen.getByTestId(`chat-session-${sessionId}`));
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("chat-mobile-session-trigger")).toBeInTheDocument();
|
|
});
|
|
}
|
|
|
|
function setupDirectChat(options: { content?: string; streamingText?: string } = {}) {
|
|
const content = options.content ?? "abcd";
|
|
setupMockChat({
|
|
sessions: [activeSessionFixture],
|
|
filteredSessions: [activeSessionFixture],
|
|
activeSession: activeSessionFixture,
|
|
messages: [
|
|
{
|
|
id: "msg-001",
|
|
sessionId: activeSessionFixture.id,
|
|
role: "user",
|
|
content,
|
|
createdAt: "2026-04-08T00:00:00.000Z",
|
|
},
|
|
],
|
|
isStreaming: options.streamingText !== undefined,
|
|
streamingText: options.streamingText ?? "",
|
|
});
|
|
}
|
|
|
|
describe("ChatView context-window indicator", () => {
|
|
it("renders the estimated token budget in the desktop Direct-chat header", async () => {
|
|
setupDirectChat({ content: "abcd" });
|
|
|
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
|
|
|
const indicator = await screen.findByTestId("chat-thread-context-window");
|
|
expect(indicator).toHaveTextContent("1 / 200k");
|
|
expect(indicator).toHaveAttribute("aria-label", "Estimated 1 of 200k context tokens");
|
|
});
|
|
|
|
it("does not render an indicator shell in mobile Direct chat", async () => {
|
|
const restoreViewport = mockViewportMode("mobile");
|
|
try {
|
|
setupDirectChat({ content: "abcd" });
|
|
|
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
|
await openMobileDirectThread();
|
|
|
|
/*
|
|
FNXC:DashboardTests 2026-07-14-20:15:
|
|
Mobile Direct chat uses the narrow layout shell; restored sessions stay on the list/header surface (no automatic mobile-direct-thread trigger). The invariant under test is no context-window chrome in constrained mobile Direct.
|
|
*/
|
|
expect(document.querySelector(".chat-view--narrow")).toBeTruthy();
|
|
expectNoContextWindowShell();
|
|
} finally {
|
|
restoreViewport.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("does not render an indicator shell in the narrowed floating chat modal", async () => {
|
|
const restoreViewport = mockViewportMode("desktop");
|
|
const originalResizeObserver = globalThis.ResizeObserver;
|
|
const rectSpy = vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
|
|
x: 0,
|
|
y: 0,
|
|
width: 560,
|
|
height: 640,
|
|
top: 0,
|
|
right: 560,
|
|
bottom: 640,
|
|
left: 0,
|
|
toJSON: () => ({}),
|
|
});
|
|
|
|
class MockResizeObserver implements ResizeObserver {
|
|
readonly observe = vi.fn();
|
|
readonly unobserve = vi.fn();
|
|
readonly disconnect = vi.fn();
|
|
constructor(_callback: ResizeObserverCallback) {}
|
|
}
|
|
|
|
globalThis.ResizeObserver = MockResizeObserver;
|
|
try {
|
|
setupDirectChat({ content: "abcd" });
|
|
|
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} floating />);
|
|
await openMobileDirectThread();
|
|
|
|
/*
|
|
FNXC:DashboardTests 2026-07-14-20:15:
|
|
Floating narrow chat is marked chat-view--floating + --narrow; do not require the mobile session trigger (only present after explicit mobile-direct-thread entry).
|
|
*/
|
|
expect(document.querySelector(".chat-view--floating.chat-view--narrow")).toBeTruthy();
|
|
expectNoContextWindowShell();
|
|
} finally {
|
|
globalThis.ResizeObserver = originalResizeObserver;
|
|
rectSpy.mockRestore();
|
|
restoreViewport.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("does not render an indicator shell when the active model context window is unknown", async () => {
|
|
mockFetchModels.mockResolvedValue({
|
|
...defaultModelsResponse,
|
|
models: [{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 0 }],
|
|
});
|
|
setupDirectChat({ content: "abcd" });
|
|
|
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
|
|
|
await waitFor(() => {
|
|
expectNoContextWindowShell();
|
|
});
|
|
});
|
|
|
|
it("includes in-flight streaming text in the displayed estimate", async () => {
|
|
const content = "a".repeat(3996);
|
|
const streamingText = "abcd";
|
|
setupDirectChat({ content, streamingText });
|
|
const expectedUsed = formatTokenCount(estimateChatTokens([{ content }], streamingText));
|
|
|
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
|
|
|
const indicator = await screen.findByTestId("chat-thread-context-window");
|
|
expect(expectedUsed).toBe("~1k");
|
|
expect(indicator).toHaveTextContent(`${expectedUsed} / 200k`);
|
|
expect(indicator).not.toHaveTextContent("999 / 200k");
|
|
});
|
|
|
|
it("does not render an indicator shell in rooms scope", async () => {
|
|
const room = createRoomFixture("context-room");
|
|
localStorage.setItem("fusion:chat-scope", "rooms");
|
|
setupDirectChat({ content: "abcd" });
|
|
setupMockRooms({
|
|
rooms: [room],
|
|
activeRoom: room,
|
|
activeRoomMembers: [],
|
|
messages: [
|
|
{
|
|
id: "room-msg-001",
|
|
roomId: room.id,
|
|
role: "user",
|
|
content: "Room hello",
|
|
createdAt: "2026-04-08T00:00:00.000Z",
|
|
senderAgentId: null,
|
|
mentions: [],
|
|
},
|
|
],
|
|
});
|
|
|
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
|
|
|
expect(document.querySelector(".chat-room-thread-header")).toBeInTheDocument();
|
|
expectNoContextWindowShell();
|
|
});
|
|
});
|