fix(dashboard-tests): green Full Suite after chat tags, TaskForm meta, remote lifecycle

- Replace undefined --space-2xs in ChatView tag menu with calc(--space-xs / 2)
- Stub fetchChatTags/create/rename/delete on streaming-thread api mock for useChat mount
- Expect TaskForm model/tracking callbacks with TaskFormValueChangeMeta source
- Drive remote tunnel lifecycle via one stop then error status (no double Stop race)
This commit is contained in:
gsxdsm
2026-07-24 23:02:16 -07:00
parent 330e4970f0
commit a03eb0b39c
4 changed files with 38 additions and 12 deletions

View File

@@ -2788,5 +2788,10 @@ Queued-message banners stack above the composer input with a capped scroll area,
.chat-tag-create-row .input { min-width: 0; }
@media (max-width: 768px) {
.chat-session-tags { max-height: calc(var(--space-lg) * 2); }
.chat-tag-menu-item { gap: var(--space-2xs); }
/*
FNXC:ChatTags 2026-07-24-23:05:
Compact mobile tag-menu gap must use a defined token. --space-2xs is intentionally
undefined (space-token-defined hygiene); half of --space-xs matches the prior intent.
*/
.chat-tag-menu-item { gap: calc(var(--space-xs) / 2); }
}

View File

@@ -16,6 +16,12 @@ vi.mock("../../sse-bus", () => ({
subscribeSse: vi.fn(() => () => {}),
}));
/*
FNXC:ChatTags 2026-07-24-23:05:
useChat loads tags on mount via fetchChatTags/createChatTag/renameChatTag/deleteChatTag.
Partial ../../api factories must stub those exports or CI fails with
"No fetchChatTags export is defined on the mock" once tag APIs ship.
*/
vi.mock("../../api", () => ({
fetchSettings: vi.fn().mockResolvedValue({}),
fetchChatSessions: vi.fn(),
@@ -27,6 +33,10 @@ vi.mock("../../api", () => ({
streamChatResponse: vi.fn(),
attachChatStream: vi.fn(),
cancelChatResponse: vi.fn(),
fetchChatTags: vi.fn().mockResolvedValue({ tags: [] }),
createChatTag: vi.fn().mockResolvedValue({ tag: { id: "tag-1", name: "t", createdAt: "2026-04-08T00:00:00.000Z" } }),
renameChatTag: vi.fn().mockResolvedValue({ tag: { id: "tag-1", name: "t", createdAt: "2026-04-08T00:00:00.000Z" } }),
deleteChatTag: vi.fn().mockResolvedValue({ success: true }),
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: {} },
]),

View File

@@ -587,11 +587,17 @@ describe("SettingsModal", () => {
});
it("shows lifecycle state changes for start and stop actions, including error state", async () => {
/*
FNXC:SettingsRemote 2026-07-24-23:05:
Full Suite flake: a second Stop Tunnel click races loadRemoteData after the first stop.
Product UI only shows Stop while state is running/starting; after stop refresh returns
error, the control is Start Tunnel. Drive start → running → one stop → error message
instead of double-stop, and keep running stable across start-poll ticks.
*/
mockFetchRemoteStatus
.mockResolvedValueOnce({ provider: null, state: "stopped", url: null, lastError: null })
.mockResolvedValueOnce({ provider: "tailscale", state: "starting", url: null, lastError: null })
.mockResolvedValueOnce({ provider: "tailscale", state: "running", url: "https://tail.example", lastError: null })
.mockResolvedValueOnce({ provider: "tailscale", state: "error", url: null, lastError: "Tunnel crashed" });
.mockResolvedValue({ provider: "tailscale", state: "running", url: "https://tail.example", lastError: null });
await renderModalSection("remote", "Remote Access");
@@ -607,15 +613,18 @@ describe("SettingsModal", () => {
expect(await screen.findByRole("button", { name: "Stop Tunnel" })).toBeInTheDocument();
mockFetchRemoteStatus.mockResolvedValue({
provider: "tailscale",
state: "error",
url: null,
lastError: "Tunnel crashed",
});
await settingsModalUser.click(screen.getByRole("button", { name: "Stop Tunnel" }));
await waitFor(() => {
expect(mockStopRemoteTunnel).toHaveBeenCalledTimes(1);
});
await settingsModalUser.click(screen.getByRole("button", { name: "Stop Tunnel" }));
await waitFor(() => {
expect(mockStopRemoteTunnel).toHaveBeenCalledTimes(2);
});
expect(await screen.findByText("Tunnel crashed")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Start Tunnel" })).toBeInTheDocument();
});
it("shows external tunnel panel with actions when external tunnel is detected", async () => {

View File

@@ -1038,8 +1038,9 @@ describe("TaskForm preset selection (FN-819)", () => {
expect(onPresetModeChange).toHaveBeenCalledWith("preset");
expect(onSelectedPresetIdChange).toHaveBeenCalledWith("fast");
expect(onExecutorModelChange).toHaveBeenCalledWith("anthropic/claude-sonnet-4-5");
expect(onValidatorModelChange).toHaveBeenCalledWith("openai/gpt-4o");
// FNXC:TaskForm 2026-07-24-23:05: model/preset handlers pass TaskFormValueChangeMeta so NewTaskModal can ignore initialization seeds vs user picks.
expect(onExecutorModelChange).toHaveBeenCalledWith("anthropic/claude-sonnet-4-5", { source: "user" });
expect(onValidatorModelChange).toHaveBeenCalledWith("openai/gpt-4o", { source: "user" });
});
it("switching to default clears preset and model overrides", async () => {
@@ -1080,8 +1081,8 @@ describe("TaskForm preset selection (FN-819)", () => {
expect(onPresetModeChange).toHaveBeenCalledWith("default");
expect(onSelectedPresetIdChange).toHaveBeenCalledWith("");
expect(onExecutorModelChange).toHaveBeenCalledWith("");
expect(onValidatorModelChange).toHaveBeenCalledWith("");
expect(onExecutorModelChange).toHaveBeenCalledWith("", { source: "user" });
expect(onValidatorModelChange).toHaveBeenCalledWith("", { source: "user" });
});
it("switching to custom clears preset ID", async () => {
@@ -1564,7 +1565,8 @@ describe("TaskForm focus behavior (FN-1459)", () => {
});
await waitFor(() => {
expect(onGithubTrackingEnabledChange).toHaveBeenCalledWith(true);
// FNXC:TaskForm 2026-07-24-23:05: settings-default seed marks source initialization so create flow can avoid treating it as a user edit.
expect(onGithubTrackingEnabledChange).toHaveBeenCalledWith(true, { source: "initialization" });
});
});