From efd085cc772ccaaba9f8e205417ab8eeaa9c4730 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 4 Jul 2026 18:58:32 -0700 Subject: [PATCH] Update readme and tests --- docs/README.md | 1 + .../PlanningModeModal.initial.test.tsx | 79 +++++++++++++------ .../src/__tests__/routes-auth.test.ts | 26 ++++++ 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/docs/README.md b/docs/README.md index f4327fb91f..8331ee7290 100644 --- a/docs/README.md +++ b/docs/README.md @@ -138,6 +138,7 @@ FN-7088 links previously-unlinked first-class testing and baseline docs here so | [Mission Completion Gate Contract](./missions-completion-contract.md) | Decision record for mission completion gate invariants and acceptance flow | | [Lost-Work Tasks Incident (2026-05-23)](./incidents/2026-05-23-lost-work-tasks.md) | Incident catalog of 9 lost-work tasks from no-op finalize and reuse-handoff bugs | +| [GitLab Parity Inventory (FN-7421)](./gitlab-parity-inventory.md) | Implementation map for first-class GitLab support: import, linked issue tracking, comments, auth/settings UI, CLI/extension, and Command Center surfaces to mirror or explicitly exclude | ## External Resources diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx index b20ea39e37..428013c8e5 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { act, render, renderHook, screen, fireEvent, waitFor, within } from "@testing-library/react"; import * as api from "../../api"; import { PlanningModeModal } from "../PlanningModeModal"; @@ -183,6 +183,18 @@ describe("PlanningModeModal", () => { }); }); + /* + FNXC:PlanningMode 2026-07-04-17:04: + The draft-creation debounce tests assert a NEGATIVE (a 300ms debounce interval elapsing without spawning a duplicate + createPlanningDraft). They previously did this with real-time `setTimeout(350)` sleeps, burning ~2.1s of wall-clock per + run for zero added signal (FN-5048: do not add slow tests). Those tests now drive fake timers via + `vi.advanceTimersByTimeAsync`, which advances the debounce deterministically and flushes the mock's promise + microtasks between timers. This afterEach restores real timers so the remaining real-timer + waitFor tests are unaffected. + */ + afterEach(() => { + vi.useRealTimers(); + }); + describe("Initial view", () => { it("renders the initial input view when open", () => { const { container } = render( @@ -539,6 +551,9 @@ describe("PlanningModeModal", () => { }); it("auto-creates a draft after typing and reuses it when starting", async () => { + // FNXC:PlanningMode 2026-07-04-17:04: fake timers drive the 300ms create-draft debounce deterministically + // (advanceTimersByTimeAsync flushes the mock promise between timers), replacing real-time sleeps. + vi.useFakeTimers(); render( { const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build a detailed auth system plan" } }); - await waitFor(() => { - expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); + await act(async () => { + await vi.advanceTimersByTimeAsync(350); }); + expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); expect(mockCreatePlanningDraft).toHaveBeenCalledWith( "Build a detailed auth system plan", undefined, @@ -569,28 +585,35 @@ describe("PlanningModeModal", () => { expect(sidebarItem?.textContent).toBe("Build a detailed auth system plan"); fireEvent.change(textarea, { target: { value: "Build a detailed auth system plan with extras" } }); - await new Promise((resolve) => setTimeout(resolve, 350)); + // Let the debounce interval elapse; the existing draft must be reused, not re-created. + await act(async () => { + await vi.advanceTimersByTimeAsync(350); + }); expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); fireEvent.click(screen.getByText("Start Planning")); - await waitFor(() => { - expect(mockStartPlanningStreaming).toHaveBeenCalledWith( - "Build a detailed auth system plan with extras", - undefined, - undefined, - { - planningDepth: "medium", - customQuestionCount: undefined, - }, - "draft-123", - ); + await act(async () => { + await vi.advanceTimersByTimeAsync(50); }); + expect(mockStartPlanningStreaming).toHaveBeenCalledWith( + "Build a detailed auth system plan with extras", + undefined, + undefined, + { + planningDepth: "medium", + customQuestionCount: undefined, + }, + "draft-123", + ); }); // FNXC:PlanningMode 2026-07-01-00:00: regression — deliberate typing must not spawn one draft per keystroke. // Original symptom: each character created a new draft while the create-draft request was in flight, because // the create-suppression guard only checked draftSessionIdRef, which is populated after the round-trip resolves. it("creates exactly one draft when keystrokes arrive while the create request is still in flight", async () => { + // FNXC:PlanningMode 2026-07-04-17:04: fake timers drive the 300ms debounce deterministically; the in-flight + // create stays unresolved (resolveCreate) so the suppression sentinel is what collapses keystrokes to one create. + vi.useFakeTimers(); let resolveCreate: ((value: { sessionId: string; title: string }) => void) | undefined; mockCreatePlanningDraft.mockImplementation( () => @@ -613,28 +636,38 @@ describe("PlanningModeModal", () => { // First keystroke → debounce (300ms) fires the create; it stays in flight (unresolved). fireEvent.change(textarea, { target: { value: "Build" } }); - await waitFor(() => { - expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); + await act(async () => { + await vi.advanceTimersByTimeAsync(350); }); + expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); // Subsequent keystrokes while the create is still in flight must be suppressed by the // synchronous in-flight sentinel — not each spawn another draft. fireEvent.change(textarea, { target: { value: "Build a" } }); - await new Promise((resolve) => setTimeout(resolve, 350)); + await act(async () => { + await vi.advanceTimersByTimeAsync(350); + }); fireEvent.change(textarea, { target: { value: "Build an" } }); - await new Promise((resolve) => setTimeout(resolve, 350)); + await act(async () => { + await vi.advanceTimersByTimeAsync(350); + }); fireEvent.change(textarea, { target: { value: "Build an auth" } }); - await new Promise((resolve) => setTimeout(resolve, 350)); + await act(async () => { + await vi.advanceTimersByTimeAsync(350); + }); expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); // Once the create resolves and further edits arrive, they patch the single draft — no new create. resolveCreate?.({ sessionId: "draft-123", title: "New planning session" }); - await waitFor(() => { - expect(document.querySelector(".planning-sidebar-item-title")).not.toBeNull(); + await act(async () => { + await vi.advanceTimersByTimeAsync(0); }); + expect(document.querySelector(".planning-sidebar-item-title")).not.toBeNull(); fireEvent.change(textarea, { target: { value: "Build an auth system" } }); - await new Promise((resolve) => setTimeout(resolve, 350)); + await act(async () => { + await vi.advanceTimersByTimeAsync(350); + }); expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1); }); diff --git a/packages/dashboard/src/__tests__/routes-auth.test.ts b/packages/dashboard/src/__tests__/routes-auth.test.ts index e74d325797..b69ae4c4aa 100644 --- a/packages/dashboard/src/__tests__/routes-auth.test.ts +++ b/packages/dashboard/src/__tests__/routes-auth.test.ts @@ -2188,6 +2188,32 @@ describe("POST /auth/login", () => { expect(observedPromptInput).toBe("manual-code"); }); + it("normalizes Anthropic subscription pasted callback URLs with fragment parameters", async () => { + (authStorage.getOAuthProviders as ReturnType).mockReturnValue([{ id: "anthropic", name: "Anthropic" }]); + + let observedManualInput: string | undefined; + (authStorage.login as ReturnType).mockImplementation(async (_provider: string, callbacks: any) => { + callbacks.onAuth({ url: "https://claude.ai/oauth/authorize?state=expected-state&redirect_uri=http%3A%2F%2Flocalhost%3A53692%2Fcallback" }); + observedManualInput = await callbacks.onManualCodeInput(); + }); + + const app = buildApp(); + const loginReq = REQUEST(app, "POST", "/api/auth/login", JSON.stringify({ provider: "anthropic-subscription", origin: "https://remote.example.com" }), { + "Content-Type": "application/json", + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const callbackUrl = "http://localhost:53692/callback#code=fragment-code&state=expected-state"; + const submitRes = await REQUEST(app, "POST", "/api/auth/manual-code", JSON.stringify({ provider: "anthropic-subscription", code: callbackUrl }), { + "Content-Type": "application/json", + }); + expect(submitRes.status).toBe(200); + + await loginReq; + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(observedManualInput).toBe("code=fragment-code&state=expected-state"); + }); + it("prefers browser login for openai-codex multi-option prompts", async () => { let selectedOption: string | undefined; (authStorage.getOAuthProviders as ReturnType).mockReturnValue([{ id: "openai-codex", name: "OpenAI Codex" }]);