From dad710fac438f98a8d867f77b18c11da967c65fa Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 8 Jul 2026 19:57:13 +0200 Subject: [PATCH] feat: gate unfinished dashboard features --- .env.example | 4 ++ ...st.yml => mockup-sprint-orchestration.yml} | 33 ++++----- dashboard/src/main.tsx | 4 +- .../onboarding/GuidedDashboardTour.tsx | 7 ++ .../src/v2/lib/dashboard-feature-flags.ts | 67 +++++++++++++++++++ dashboard/src/v2/lib/navigation-items.ts | 10 ++- dashboard/src/v2/router/route-prefetch.ts | 49 +++++++++----- .../content/docs/developer-feature-flags.mdx | 29 ++++++++ .../developer-orchestration-debugging.mdx | 2 +- docs-web/content/docs/registry.ts | 15 ++++- docs-web/developer/feature-flags.md | 29 ++++++++ docs-web/developer/index.md | 11 +-- docs-web/developer/orchestration-debugging.md | 2 +- .../docs.developer-feature-flags.lazy.tsx | 11 +++ docs/SUMMARY.md | 1 + docs/dashboard/feature-flags.md | 29 ++++++++ .../mockup-sprint-pentest-scenarios.md | 8 +-- docs/development/mockup-sprint-pentest.md | 18 +++-- .../rapid-orchestration-debugging.md | 2 +- docs/development/testing-and-quality.md | 4 +- docs/index.md | 20 +++--- scripts/sync-docs-web.mjs | 4 ++ src/services/docs-web-catalog-service.ts | 4 ++ tests/backend/ci/workflow-health.test.ts | 31 +++++---- .../lib/dashboard-feature-flags.test.ts | 47 +++++++++++++ tests/dashboard/v2/nodes-page.test.tsx | 6 +- 26 files changed, 352 insertions(+), 95 deletions(-) rename .github/workflows/{mockup-sprint-pentest.yml => mockup-sprint-orchestration.yml} (59%) create mode 100644 dashboard/src/v2/lib/dashboard-feature-flags.ts create mode 100644 docs-web/content/docs/developer-feature-flags.mdx create mode 100644 docs-web/developer/feature-flags.md create mode 100644 docs-web/routes/docs.developer-feature-flags.lazy.tsx create mode 100644 docs/dashboard/feature-flags.md create mode 100644 tests/dashboard/lib/dashboard-feature-flags.test.ts diff --git a/.env.example b/.env.example index 23f4d4558f..510d561ec0 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,6 @@ JULES_API_KEY= DASHBOARD_PORT=4444 + +# Unfinished dashboard surfaces are visible by default in dev/test and hidden by default in production builds. +# Set to true in a production build only when the Nodes page is ready to expose. +# VITE_CODEUX_FEATURE_NODES=true diff --git a/.github/workflows/mockup-sprint-pentest.yml b/.github/workflows/mockup-sprint-orchestration.yml similarity index 59% rename from .github/workflows/mockup-sprint-pentest.yml rename to .github/workflows/mockup-sprint-orchestration.yml index c3e9f92629..de5a96eb4a 100644 --- a/.github/workflows/mockup-sprint-pentest.yml +++ b/.github/workflows/mockup-sprint-orchestration.yml @@ -1,23 +1,22 @@ -name: Mockup Sprint Pentest (temporary dev validation) +name: Mockup Sprint Orchestration on: push: - branches: - - main - # Dev pushes run the full mockup pentest catalog before integration. - - dev + branches: [main, dev] + pull_request: + branches: [main, dev] workflow_dispatch: concurrency: - group: mockup-sprint-pentest-${{ github.workflow }}-${{ github.ref }} + group: mockup-sprint-orchestration-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read jobs: - mockup-sprint-pentest: - name: Mockup sprint pentest + mockup-sprint-orchestration: + name: Mockup sprint orchestration runs-on: ubuntu-latest steps: @@ -40,29 +39,21 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - - name: Build - run: pnpm run build - - name: Verify Docker availability run: | if ! docker version; then - echo "::error::Docker is required for the mockup sprint pentest Docker lane." + echo "::error::Docker is required for the mockup sprint orchestration lane." exit 1 fi - - name: Run mockup sprint pentest - run: | - if [ "${GITHUB_REF_NAME}" = "dev" ]; then - node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000 - else - pnpm run test:e2e:mockup-sprint-pentest - fi + - name: Run full mockup orchestration + run: pnpm run test:orchestration:full - - name: Upload mockup sprint pentest artifacts + - name: Upload mockup sprint orchestration artifacts if: ${{ failure() || hashFiles('.cache/e2e-mockup-sprint-pentest/**') != '' }} uses: actions/upload-artifact@v4 with: - name: mockup-sprint-pentest-artifacts + name: mockup-sprint-orchestration-artifacts path: .cache/e2e-mockup-sprint-pentest/ if-no-files-found: ignore include-hidden-files: true diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx index 1f38c253b8..d62ab69c62 100644 --- a/dashboard/src/main.tsx +++ b/dashboard/src/main.tsx @@ -25,6 +25,7 @@ import { TitleBar } from "./v2/components/TitleBar.js"; import { DashboardAssistantWidget } from "./v2/components/chat/DashboardAssistantWidget.js"; import { AddProjectModal, type AddProjectModalSubmission } from "./v2/components/ui/AddProjectModal.js"; import { ASSISTANT_OPEN_ADD_PROJECT_EVENT } from "./v2/lib/no-project-chat-assistant.js"; +import { isDashboardFeatureEnabled } from "./v2/lib/dashboard-feature-flags.js"; import { buildProjectCreationSettingsOverride } from "./lib/settings-updaters.js"; import { DEFAULT_DASHBOARD_SETTINGS } from "./lib/settings.js"; import "./styles.css"; @@ -455,7 +456,8 @@ const notFoundRoute = createRoute({ component: ErrorPage, }); -const routeTree = rootRoute.addChildren([indexRoute, sprintsRoute, tasksRoute, projectsRoute, chatRoute, agentsRoute, nodesRoute, statsRoute, schedulerRoute, configRoute, memoryRoute, knowledgeRoute, browserRoute, fileBrowserRoute, docsRoute, docsDocumentRoute, liveRoute, notFoundRoute]); +const nodesFeatureEnabled = isDashboardFeatureEnabled("nodes"); +const routeTree = rootRoute.addChildren([indexRoute, sprintsRoute, tasksRoute, projectsRoute, chatRoute, agentsRoute, ...(nodesFeatureEnabled ? [nodesRoute] : []), statsRoute, schedulerRoute, configRoute, memoryRoute, knowledgeRoute, browserRoute, fileBrowserRoute, docsRoute, docsDocumentRoute, liveRoute, notFoundRoute]); // `defaultPreload: "intent"` warms route matching on hover/focus; the page chunks themselves are // prefetched explicitly by the nav components via prefetchRoute() since they are Preact-lazy. const router = createRouter({ routeTree, defaultPreload: "intent", defaultPreloadDelay: 50 }); diff --git a/dashboard/src/v2/components/onboarding/GuidedDashboardTour.tsx b/dashboard/src/v2/components/onboarding/GuidedDashboardTour.tsx index 502a955524..bbb3b3574b 100644 --- a/dashboard/src/v2/components/onboarding/GuidedDashboardTour.tsx +++ b/dashboard/src/v2/components/onboarding/GuidedDashboardTour.tsx @@ -5,6 +5,8 @@ import { ArrowLeft, ArrowRight, BookOpen, Box, CalendarDays, Check, Compass, Eye import { DASHBOARD_TOUR_START_EVENT, DASHBOARD_TOUR_STORAGE_KEY } from "../../lib/onboarding-control.js"; import { useReducedMotion } from "../../hooks/use-reduced-motion.js"; import { useInteractionTokens } from "../../lib/motion/tokens.js"; +import type { DashboardFeatureId } from "../../lib/dashboard-feature-flags.js"; +import { isDashboardFeatureEnabled } from "../../lib/dashboard-feature-flags.js"; type TourStep = { id: string; @@ -13,6 +15,7 @@ type TourStep = { title: string; body: string; accent: "signal" | "ember" | "sky"; + feature?: DashboardFeatureId; }; type RectState = { @@ -94,6 +97,7 @@ const TOUR_STEPS: TourStep[] = [ title: "Nodes", body: "Nodes lets you compose project workflow graphs, configure node widgets, attach flows to agents, and inspect persisted runs.", accent: "signal", + feature: "nodes", }, { id: "stats", @@ -247,6 +251,9 @@ export const GuidedDashboardTour: FunctionComponent = () => { const refreshSteps = useCallback(() => { const steps = TOUR_STEPS.filter((step) => { + if (step.feature && !isDashboardFeatureEnabled(step.feature)) { + return false; + } const element = getTourElement(step.targetId); return element ? isVisibleTarget(element) : false; }); diff --git a/dashboard/src/v2/lib/dashboard-feature-flags.ts b/dashboard/src/v2/lib/dashboard-feature-flags.ts new file mode 100644 index 0000000000..235dba5eb9 --- /dev/null +++ b/dashboard/src/v2/lib/dashboard-feature-flags.ts @@ -0,0 +1,67 @@ +export const DASHBOARD_FEATURE_IDS = ["nodes"] as const; + +export type DashboardFeatureId = typeof DASHBOARD_FEATURE_IDS[number]; + +export type DashboardFeatureFlagMap = Record; + +export type DashboardFeatureFlagValues = Partial>; + +export interface DashboardFeatureFlagSource { + devMode?: boolean; + values?: DashboardFeatureFlagValues; +} + +export const DASHBOARD_FEATURE_ENV_KEYS: Record = { + nodes: "VITE_CODEUX_FEATURE_NODES", +}; + +const ENABLED_VALUES = new Set(["1", "true", "yes", "on", "enabled"]); +const DISABLED_VALUES = new Set(["0", "false", "no", "off", "disabled"]); + +export const parseDashboardFeatureFlagValue = (value: unknown): boolean | null => { + if (typeof value === "boolean") { + return value; + } + if (typeof value !== "string") { + return null; + } + + const normalized = value.trim().toLowerCase(); + if (!normalized) { + return null; + } + if (ENABLED_VALUES.has(normalized)) { + return true; + } + if (DISABLED_VALUES.has(normalized)) { + return false; + } + return null; +}; + +const readDashboardFeatureFlagSource = (): DashboardFeatureFlagSource => { + const env = import.meta.env as ImportMetaEnv & Record; + return { + devMode: Boolean(env.DEV), + values: { + nodes: env[DASHBOARD_FEATURE_ENV_KEYS.nodes], + }, + }; +}; + +export const resolveDashboardFeatureFlags = ( + source: DashboardFeatureFlagSource = readDashboardFeatureFlagSource(), +): DashboardFeatureFlagMap => { + const defaultEnabled = source.devMode ?? false; + + return DASHBOARD_FEATURE_IDS.reduce((flags, feature) => { + const explicitValue = parseDashboardFeatureFlagValue(source.values?.[feature]); + flags[feature] = explicitValue ?? defaultEnabled; + return flags; + }, {} as DashboardFeatureFlagMap); +}; + +export const isDashboardFeatureEnabled = ( + feature: DashboardFeatureId, + flags: DashboardFeatureFlagMap = resolveDashboardFeatureFlags(), +): boolean => flags[feature]; diff --git a/dashboard/src/v2/lib/navigation-items.ts b/dashboard/src/v2/lib/navigation-items.ts index 1f1333feac..cff92a22ea 100644 --- a/dashboard/src/v2/lib/navigation-items.ts +++ b/dashboard/src/v2/lib/navigation-items.ts @@ -17,6 +17,8 @@ import { Zap, } from "lucide-preact"; import type { DashboardExperienceMode } from "../../types.js"; +import type { DashboardFeatureFlagMap, DashboardFeatureId } from "./dashboard-feature-flags.js"; +import { isDashboardFeatureEnabled, resolveDashboardFeatureFlags } from "./dashboard-feature-flags.js"; import { normalizeDashboardExperienceMode } from "./experience-mode.js"; export const EXTERNAL_DOCS_URL = "https://github.com/codeux-ai/codeux#readme"; @@ -51,6 +53,7 @@ interface BaseNavigationItem { group: NavigationItemGroup; dockSection: NavigationDockSection; tourId: string; + feature?: DashboardFeatureId; } export interface RouteNavigationItem extends BaseNavigationItem { @@ -71,6 +74,7 @@ export type PrimaryNavigationItem = NavigationItem & { interface GetPrimaryNavigationItemsOptions { browserVisible?: boolean; unavailableBrowserReason?: string; + featureFlags?: DashboardFeatureFlagMap; } export const ALL_NAVIGATION_ITEMS: readonly NavigationItem[] = [ @@ -79,7 +83,7 @@ export const ALL_NAVIGATION_ITEMS: readonly NavigationItem[] = [ { id: "sprints", icon: Layers, label: "Sprints", path: "/sprints", color: "text-ember-500", group: "workspace", dockSection: "right", tourId: "nav-sprints", kind: "route" }, { id: "tasks", icon: ListChecks, label: "Tasks", path: "/tasks", color: "text-signal-400", group: "workspace", dockSection: "right", tourId: "nav-tasks", kind: "route" }, { id: "agents", icon: Cpu, label: "Agents", path: "/agents", color: "text-signal-400", group: "workspace", dockSection: "right", tourId: "nav-agents", kind: "route" }, - { id: "nodes", icon: Workflow, label: "Nodes", path: "/nodes", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-nodes", kind: "route" }, + { id: "nodes", icon: Workflow, label: "Nodes", path: "/nodes", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-nodes", kind: "route", feature: "nodes" }, { id: "stats", icon: BarChart3, label: "Stats", path: "/stats", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-stats", kind: "route" }, { id: "scheduler", icon: CalendarDays, label: "Schedule", path: "/scheduler", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-schedule", kind: "route" }, { id: "memory", icon: Inbox, label: "Memory", path: "/memory", color: "text-signal-500", group: "workspace", dockSection: "right", tourId: "nav-memory", kind: "route" }, @@ -130,11 +134,15 @@ export const getPrimaryNavigationItems = ( ): PrimaryNavigationItem[] => { const normalizedMode = normalizeDashboardExperienceMode(mode); const browserVisible = options.browserVisible ?? true; + const featureFlags = options.featureFlags ?? resolveDashboardFeatureFlags(); return NAVIGATION_ITEM_IDS_BY_MODE[normalizedMode] .map((id) => navigationItemById.get(id)) .filter((item): item is NavigationItem => !!item) .flatMap((item): PrimaryNavigationItem[] => { + if (item.feature && !isDashboardFeatureEnabled(item.feature, featureFlags)) { + return []; + } if (item.id !== "browser" || browserVisible) { return [item]; } diff --git a/dashboard/src/v2/router/route-prefetch.ts b/dashboard/src/v2/router/route-prefetch.ts index 2884d65a6f..aa037070d5 100644 --- a/dashboard/src/v2/router/route-prefetch.ts +++ b/dashboard/src/v2/router/route-prefetch.ts @@ -7,39 +7,52 @@ // The specifiers below resolve to the exact modules used by main.tsx's `lazy()` calls; the bundler // and the ESM module cache dedupe by resolved module id, so a prefetch and the later `lazy()` load // share one chunk and one in-flight request. +import type { DashboardFeatureFlagMap, DashboardFeatureId } from "../lib/dashboard-feature-flags.js"; +import { isDashboardFeatureEnabled } from "../lib/dashboard-feature-flags.js"; + type ModuleImporter = () => Promise; -const componentImporters: Record = { - "/sprints": () => import("../pages/sprints/SprintsPage.js"), - "/projects": () => import("../ProjectsPage.js"), - "/chat": () => import("../ChatPage.js"), - "/tasks": () => import("../TasksPage.js"), - "/agents": () => import("../AgentsPage.js"), - "/nodes": () => import("../NodesPage.js"), - "/stats": () => import("../StatsPage.js"), - "/scheduler": () => import("../SchedulerPage.js"), - "/config": () => import("../SettingsPage.js"), - "/memory": () => import("../MemoryPage.js"), - "/knowledge": () => import("../KnowledgePage.js"), - "/browser": () => import("../BrowserPage.js"), - "/files": () => import("../FileBrowserPage.js"), - "/docs": () => import("../docs-web/DocsWebPage.js"), +interface ComponentImporterEntry { + importer: ModuleImporter; + feature?: DashboardFeatureId; +} + +const componentImporters: Record = { + "/sprints": { importer: () => import("../pages/sprints/SprintsPage.js") }, + "/projects": { importer: () => import("../ProjectsPage.js") }, + "/chat": { importer: () => import("../ChatPage.js") }, + "/tasks": { importer: () => import("../TasksPage.js") }, + "/agents": { importer: () => import("../AgentsPage.js") }, + "/nodes": { importer: () => import("../NodesPage.js"), feature: "nodes" }, + "/stats": { importer: () => import("../StatsPage.js") }, + "/scheduler": { importer: () => import("../SchedulerPage.js") }, + "/config": { importer: () => import("../SettingsPage.js") }, + "/memory": { importer: () => import("../MemoryPage.js") }, + "/knowledge": { importer: () => import("../KnowledgePage.js") }, + "/browser": { importer: () => import("../BrowserPage.js") }, + "/files": { importer: () => import("../FileBrowserPage.js") }, + "/docs": { importer: () => import("../docs-web/DocsWebPage.js") }, }; const startedPaths = new Set(); +export const canPrefetchRoute = (path: string, featureFlags?: DashboardFeatureFlagMap): boolean => { + const entry = componentImporters[path]; + return Boolean(entry && (!entry.feature || isDashboardFeatureEnabled(entry.feature, featureFlags))); +}; + /** * Begin downloading the chunk for `path` if it is code-split and not already requested. * Safe to call repeatedly and on every pointer event — it is a no-op after the first call, * and a failed prefetch is reset so a later real navigation can retry. */ export const prefetchRoute = (path: string): void => { - const importer = componentImporters[path]; - if (!importer || startedPaths.has(path)) { + const entry = componentImporters[path]; + if (!entry || !canPrefetchRoute(path) || startedPaths.has(path)) { return; } startedPaths.add(path); - void importer().catch(() => { + void entry.importer().catch(() => { // Allow a real navigation (or a later intent) to retry the load. startedPaths.delete(path); }); diff --git a/docs-web/content/docs/developer-feature-flags.mdx b/docs-web/content/docs/developer-feature-flags.mdx new file mode 100644 index 0000000000..1c60586766 --- /dev/null +++ b/docs-web/content/docs/developer-feature-flags.mdx @@ -0,0 +1,29 @@ +# Dashboard Feature Flags + +Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests. + +Flags live in `dashboard/src/v2/lib/dashboard-feature-flags.ts`. They are resolved at dashboard bundle time through Vite `import.meta.env` values: + +- Development and test builds enable all flagged unfinished features by default. +- Production builds disable flagged unfinished features by default. +- Explicit env values override the mode default. + +Supported values are `true`, `1`, `yes`, `on`, `enabled`, `false`, `0`, `no`, `off`, and `disabled`. Empty or unrecognized values fall back to the mode default. + +## Current Flags + +| Feature | Env variable | Development default | Production default | Scope | +| --- | --- | --- | --- | --- | +| `nodes` | `VITE_CODEUX_FEATURE_NODES` | enabled | disabled | Hides the unfinished `/nodes` surface from route registration, shared navigation, route prefetch, and the guided dashboard tour. | + +When `nodes` is disabled, the `NodesPage` module remains in source for local development and tests, but `/nodes` is not added to the TanStack route tree. Direct navigation to `/nodes` falls through to the dashboard not-found route. + +## Adding a Flag + +1. Add the feature id and Vite env key in `dashboard-feature-flags.ts`. +2. Attach the `feature` id to any affected navigation item in `dashboard/src/v2/lib/navigation-items.ts`. +3. Gate route registration in `dashboard/src/main.tsx`. +4. Gate route prefetch entries in `dashboard/src/v2/router/route-prefetch.ts`. +5. Gate guided tour steps or other entry points that reference the hidden surface. +6. Add focused tests for default behavior, explicit overrides, and every hidden entry point. +7. Update this page and the matching canonical `docs/` page. diff --git a/docs-web/content/docs/developer-orchestration-debugging.mdx b/docs-web/content/docs/developer-orchestration-debugging.mdx index 0f7022040c..65a4fbf7fb 100644 --- a/docs-web/content/docs/developer-orchestration-debugging.mdx +++ b/docs-web/content/docs/developer-orchestration-debugging.mdx @@ -8,7 +8,7 @@ Use this suite when a sprint stalls, local merges fail, worker-owned attention i | --- | --- | --- | | Fast regressions | `pnpm run test:orchestration:rapid` | Watch-loop, feature merge, and local final-merge regressions without Docker or provider CLIs. | | Mockup merge E2E | `pnpm run test:orchestration:merge-e2e` | Compiled runtime plus `mockup-cli` through a deterministic local merge-conflict DAG. | -| Full mockup pentest | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | +| Full mockup orchestration | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | | Large DAG stress | `pnpm run test:orchestration:large-dag` | Heavy 129-task mockup DAG with wide fan-out and layered joins. | | Full heavy pentest | `pnpm run test:orchestration:pentest` | Default mockup catalog plus heavy stress scenarios. | | Backend broadening | `pnpm run test:backend` | Full backend suite after focused fixes. | diff --git a/docs-web/content/docs/registry.ts b/docs-web/content/docs/registry.ts index cd34085b48..47b75e77bf 100644 --- a/docs-web/content/docs/registry.ts +++ b/docs-web/content/docs/registry.ts @@ -38,6 +38,7 @@ export type DocsSlug = | 'developer-http-api' | 'developer-websocket-realtime' | 'developer-configuration' + | 'developer-feature-flags' | 'developer-settings-reference' | 'developer-sprint-format' | 'developer-building-from-source' @@ -199,21 +200,21 @@ export const docsRegistry: Record = { path: '/docs/user-dashboard-nodes', section: 'User Guide', title: "Nodes", - description: "The Nodes page (/nodes) opens a browser-local canvas workspace for drafting workflow graphs with localStorage persistence, validation, JSON import/export, and agent command metadata.", + description: "The Nodes page (/nodes) opens the browser-local Nodes Canvas workspace for drafting Code UX workflow graphs. It does not require a selected project and does not call the node-flow backend APIs.", }, 'user-dashboard-nodes-canvas': { id: 'user-dashboard-nodes-canvas', path: '/docs/user-dashboard-nodes-canvas', section: 'User Guide', title: "Nodes Canvas", - description: "The Nodes Canvas page is a local graph drafting surface with trigger, agent, task, condition, and output nodes, structural validation, deterministic JSON exchange, and agent command helpers.", + description: "The Nodes Canvas page (/nodes) is a browser-local workspace for drafting Code UX workflow graphs. It combines the canvas, palette, inspector, validation panel, JSON exchange controls, and agent command summary without...", }, 'user-dashboard-node-flows': { id: 'user-dashboard-node-flows', path: '/docs/user-dashboard-node-flows', section: 'User Guide', title: "Node Flows", - description: "Create and operate saved node-flow workflows for the active project, including dynamic widgets, validation, manual runs, scheduling, run inspection, and agent skill attachments.", + description: "The Nodes page (/nodes) is where dashboard users create and operate saved node-flow workflows for the active project. A node flow is a repeatable graph that can be validated, run manually, scheduled, inspected through...", }, 'user-dashboard-scheduler': { id: 'user-dashboard-scheduler', @@ -313,6 +314,13 @@ export const docsRegistry: Record = { title: "Configuration & CLI", description: "This page is the precise reference for every CLI flag, environment variable, and configuration file Code UX consumes.", }, + 'developer-feature-flags': { + id: 'developer-feature-flags', + path: '/docs/developer-feature-flags', + section: 'Developer Reference', + title: "Dashboard Feature Flags", + description: "Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests.", + }, 'developer-settings-reference': { id: 'developer-settings-reference', path: '/docs/developer-settings-reference', @@ -491,6 +499,7 @@ export const orderedDocs: DocsRegistryEntry[] = [ docsRegistry['developer-http-api'], docsRegistry['developer-websocket-realtime'], docsRegistry['developer-configuration'], + docsRegistry['developer-feature-flags'], docsRegistry['developer-settings-reference'], docsRegistry['developer-sprint-format'], docsRegistry['developer-building-from-source'], diff --git a/docs-web/developer/feature-flags.md b/docs-web/developer/feature-flags.md new file mode 100644 index 0000000000..1c60586766 --- /dev/null +++ b/docs-web/developer/feature-flags.md @@ -0,0 +1,29 @@ +# Dashboard Feature Flags + +Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests. + +Flags live in `dashboard/src/v2/lib/dashboard-feature-flags.ts`. They are resolved at dashboard bundle time through Vite `import.meta.env` values: + +- Development and test builds enable all flagged unfinished features by default. +- Production builds disable flagged unfinished features by default. +- Explicit env values override the mode default. + +Supported values are `true`, `1`, `yes`, `on`, `enabled`, `false`, `0`, `no`, `off`, and `disabled`. Empty or unrecognized values fall back to the mode default. + +## Current Flags + +| Feature | Env variable | Development default | Production default | Scope | +| --- | --- | --- | --- | --- | +| `nodes` | `VITE_CODEUX_FEATURE_NODES` | enabled | disabled | Hides the unfinished `/nodes` surface from route registration, shared navigation, route prefetch, and the guided dashboard tour. | + +When `nodes` is disabled, the `NodesPage` module remains in source for local development and tests, but `/nodes` is not added to the TanStack route tree. Direct navigation to `/nodes` falls through to the dashboard not-found route. + +## Adding a Flag + +1. Add the feature id and Vite env key in `dashboard-feature-flags.ts`. +2. Attach the `feature` id to any affected navigation item in `dashboard/src/v2/lib/navigation-items.ts`. +3. Gate route registration in `dashboard/src/main.tsx`. +4. Gate route prefetch entries in `dashboard/src/v2/router/route-prefetch.ts`. +5. Gate guided tour steps or other entry points that reference the hidden surface. +6. Add focused tests for default behavior, explicit overrides, and every hidden entry point. +7. Update this page and the matching canonical `docs/` page. diff --git a/docs-web/developer/index.md b/docs-web/developer/index.md index 6958af9b6e..b3b6eca4d3 100644 --- a/docs-web/developer/index.md +++ b/docs-web/developer/index.md @@ -13,11 +13,12 @@ If you are looking for narrative explanations, the [User Guide](../user/index.md | 3 | [HTTP API reference](./http-api.md) | All REST endpoints exposed by the dashboard server | | 4 | [Realtime WebSocket protocol](./websocket-realtime.md) | The live update stream protocol | | 5 | [Configuration & CLI](./configuration.md) | Every CLI flag, env var, and config search rule | -| 6 | [Settings schema reference](./settings-reference.md) | The full settings tree | -| 7 | [Sprint and subtask file format](./sprint-format.md) | On-disk markdown format with YAML frontmatter | -| 8 | [Building from source](./building-from-source.md) | Build, link, run from a clone | -| 9 | [Testing & quality gates](./testing.md) | Vitest, coverage thresholds, CI gates | -| 10 | [Rapid orchestration debugging suite](./orchestration-debugging.md) | Focused orchestration regressions, mockup sprint lanes, local merge triage, and profiling | +| 6 | [Dashboard feature flags](./feature-flags.md) | Unfinished dashboard surface gating and Vite env overrides | +| 7 | [Settings schema reference](./settings-reference.md) | The full settings tree | +| 8 | [Sprint and subtask file format](./sprint-format.md) | On-disk markdown format with YAML frontmatter | +| 9 | [Building from source](./building-from-source.md) | Build, link, run from a clone | +| 10 | [Testing & quality gates](./testing.md) | Vitest, coverage thresholds, CI gates | +| 11 | [Rapid orchestration debugging suite](./orchestration-debugging.md) | Focused orchestration regressions, mockup sprint lanes, local merge triage, and profiling | ## Versioning diff --git a/docs-web/developer/orchestration-debugging.md b/docs-web/developer/orchestration-debugging.md index 0f7022040c..65a4fbf7fb 100644 --- a/docs-web/developer/orchestration-debugging.md +++ b/docs-web/developer/orchestration-debugging.md @@ -8,7 +8,7 @@ Use this suite when a sprint stalls, local merges fail, worker-owned attention i | --- | --- | --- | | Fast regressions | `pnpm run test:orchestration:rapid` | Watch-loop, feature merge, and local final-merge regressions without Docker or provider CLIs. | | Mockup merge E2E | `pnpm run test:orchestration:merge-e2e` | Compiled runtime plus `mockup-cli` through a deterministic local merge-conflict DAG. | -| Full mockup pentest | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | +| Full mockup orchestration | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, dirty checkout, multi-project overrides. | | Large DAG stress | `pnpm run test:orchestration:large-dag` | Heavy 129-task mockup DAG with wide fan-out and layered joins. | | Full heavy pentest | `pnpm run test:orchestration:pentest` | Default mockup catalog plus heavy stress scenarios. | | Backend broadening | `pnpm run test:backend` | Full backend suite after focused fixes. | diff --git a/docs-web/routes/docs.developer-feature-flags.lazy.tsx b/docs-web/routes/docs.developer-feature-flags.lazy.tsx new file mode 100644 index 0000000000..e12f61f04b --- /dev/null +++ b/docs-web/routes/docs.developer-feature-flags.lazy.tsx @@ -0,0 +1,11 @@ +import { createLazyFileRoute } from '@tanstack/react-router' +import DeveloperFeatureFlagsContent from '../content/docs/developer-feature-flags.mdx' +import { DocsPage } from '../components/docs/DocsPage' + +export const Route = createLazyFileRoute('/docs/developer-feature-flags')({ + component: () => ( + + + + ) +}) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index d3c5379033..5b782fe1ca 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -67,6 +67,7 @@ - [Stats & Analytics Design System](./dashboard/design-system-stats.md) - Warm Void analytics surfaces and System administration - [Dashboard Onboarding](./dashboard/onboarding.md) - [Project Initialization](./dashboard/project-initialization.md) +- [Dashboard Feature Flags](./dashboard/feature-flags.md) - [Styleguides and Tech Stacks](./dashboard/styleguides-and-tech-stacks.md) - [Sprint Imports](./dashboard/sprint-imports.md) - [Quicksprint Templates](./dashboard/quicksprint-templates.md) diff --git a/docs/dashboard/feature-flags.md b/docs/dashboard/feature-flags.md new file mode 100644 index 0000000000..a52ac971a6 --- /dev/null +++ b/docs/dashboard/feature-flags.md @@ -0,0 +1,29 @@ +# Dashboard Feature Flags + +Dashboard feature flags hide unfinished dashboard surfaces without deleting their implementation or tests. + +Flags live in `dashboard/src/v2/lib/dashboard-feature-flags.ts`. They are resolved at dashboard bundle time through Vite `import.meta.env` values: + +- Development and test builds enable all flagged unfinished features by default. +- Production builds disable flagged unfinished features by default. +- Explicit env values override the mode default. + +Supported values are `true`, `1`, `yes`, `on`, `enabled`, `false`, `0`, `no`, `off`, and `disabled`. Empty or unrecognized values fall back to the mode default. + +## Current Flags + +| Feature | Env variable | Development default | Production default | Scope | +| --- | --- | --- | --- | --- | +| `nodes` | `VITE_CODEUX_FEATURE_NODES` | enabled | disabled | Hides the unfinished `/nodes` surface from route registration, shared navigation, route prefetch, and the guided dashboard tour. | + +When `nodes` is disabled, the `NodesPage` module remains in source for local development and tests, but `/nodes` is not added to the TanStack route tree. Direct navigation to `/nodes` falls through to the dashboard not-found route. + +## Adding a Flag + +1. Add the feature id and Vite env key in `dashboard-feature-flags.ts`. +2. Attach the `feature` id to any affected navigation item in `dashboard/src/v2/lib/navigation-items.ts`. +3. Gate route registration in `dashboard/src/main.tsx`. +4. Gate route prefetch entries in `dashboard/src/v2/router/route-prefetch.ts`. +5. Gate guided tour steps or other entry points that reference the hidden surface. +6. Add focused tests for default behavior, explicit overrides, and every hidden entry point. +7. Update this page and the matching `docs-web/` page. diff --git a/docs/development/mockup-sprint-pentest-scenarios.md b/docs/development/mockup-sprint-pentest-scenarios.md index 5322a7543e..7c79384e43 100644 --- a/docs/development/mockup-sprint-pentest-scenarios.md +++ b/docs/development/mockup-sprint-pentest-scenarios.md @@ -37,11 +37,11 @@ The runner supports restart stress with `--restart-every-ms --restart-c ## CI Coverage -`.github/workflows/mockup-sprint-pentest.yml` runs the credential-free mockup sprint pentest lane on `ubuntu-latest` with Node 22 and pnpm 10.33.0. It installs dependencies with `pnpm install --frozen-lockfile --ignore-scripts`, builds the compiled runtime, verifies Docker with `docker version`, and then runs `pnpm run test:e2e:mockup-sprint-pentest`. On `push` to `dev`, it runs `node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000` so the full catalog, including heavy stress scenarios, is always covered before dev integration. +`.github/workflows/mockup-sprint-orchestration.yml` runs the credential-free mockup sprint orchestration lane on `ubuntu-latest` with Node 22 and pnpm 10.33.0. It installs dependencies with `pnpm install --frozen-lockfile --ignore-scripts`, verifies Docker with `docker version`, and then runs `pnpm run test:orchestration:full`, which builds the compiled runtime and executes the deterministic mockup scenario catalog without the heavy pentest-only stress scenarios. -The workflow is intentionally Linux-only because the lane exercises Docker containers. It runs on `push` to `main`, manual `workflow_dispatch`, and `dev` pushes for full-catalog validation. +The workflow is intentionally Linux-only because the lane exercises Docker containers. It runs on pushes and pull requests targeting `dev` or `main`, plus manual `workflow_dispatch`. -CI uploads `.cache/e2e-mockup-sprint-pentest/` as `mockup-sprint-pentest-artifacts` when the runner fails or when artifact files exist. Hidden files are included and artifacts are retained for 5 days. +CI uploads `.cache/e2e-mockup-sprint-pentest/` as `mockup-sprint-orchestration-artifacts` when the runner fails or when artifact files exist. Hidden files are included and artifacts are retained for 5 days. ## Local Validation @@ -76,7 +76,7 @@ Run the heavy 129-task DAG stress scenario: pnpm run test:orchestration:large-dag ``` -Run the full pentest catalog, including heavy stress scenarios: +Run the full pentest catalog, including heavy stress scenarios, as a manual local lane: ```bash pnpm run test:orchestration:pentest diff --git a/docs/development/mockup-sprint-pentest.md b/docs/development/mockup-sprint-pentest.md index 5cbc3aff17..c29d6e4163 100644 --- a/docs/development/mockup-sprint-pentest.md +++ b/docs/development/mockup-sprint-pentest.md @@ -127,30 +127,28 @@ Unless `--keep-artifacts` is set, the runner removes `home/tmp` after cleanup wh ## CI Behavior -The CI lane is `.github/workflows/mockup-sprint-pentest.yml`, named `Mockup Sprint Pentest (temporary dev validation)`. +The CI lane is `.github/workflows/mockup-sprint-orchestration.yml`, named `Mockup Sprint Orchestration`. -It runs on `ubuntu-latest` because this lane forces Docker-backed CLI execution. The workflow installs dependencies with pnpm 10.33.0 on Node 22, runs `pnpm run build`, verifies Docker with `docker version`, and then runs: +It runs on `ubuntu-latest` because this lane forces Docker-backed CLI execution. The workflow installs dependencies with pnpm 10.33.0 on Node 22, verifies Docker with `docker version`, and then runs: ```bash -pnpm run test:e2e:mockup-sprint-pentest +pnpm run test:orchestration:full ``` -On `push` to `dev`, it runs the full pentest catalog instead: - -```bash -node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000 -``` +The heavier `pnpm run test:orchestration:pentest` lane remains available for manual validation but is not part of CI. Triggers: - `push` to `main`. -- `push` to `dev` as a full-catalog sprint validation trigger. +- `push` to `dev`. +- `pull_request` targeting `main`. +- `pull_request` targeting `dev`. - Manual `workflow_dispatch`. Artifact policy: - Upload path: `.cache/e2e-mockup-sprint-pentest/`. -- Artifact name: `mockup-sprint-pentest-artifacts`. +- Artifact name: `mockup-sprint-orchestration-artifacts`. - Upload condition: workflow failure or existing artifact files. - Hidden files are included because the artifact root is under `.cache/`. - Retention is 5 days. diff --git a/docs/development/rapid-orchestration-debugging.md b/docs/development/rapid-orchestration-debugging.md index 9dcba6773e..c3c5bb9b34 100644 --- a/docs/development/rapid-orchestration-debugging.md +++ b/docs/development/rapid-orchestration-debugging.md @@ -11,7 +11,7 @@ The suite is intentionally split into fast deterministic lanes and slower compil | Fast regressions | `pnpm run test:orchestration:rapid` | Watch-loop, feature merge, and local final-merge regressions without Docker or provider CLIs. | Seconds to a few minutes | | Mockup merge E2E | `pnpm run test:orchestration:merge-e2e` | Compiled runtime plus `mockup-cli` through a deterministic local merge-conflict DAG. | Up to 15 minutes | | Completion conflict E2E | `pnpm run test:orchestration:completion-conflict` | Compiled runtime final LOCAL merge conflict repair after default-branch mutation during orchestration. | Up to 20 minutes | -| Full mockup pentest | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, multi-project overrides. | Longer-running | +| Full mockup orchestration | `pnpm run test:orchestration:full` | All deterministic mockup scenarios: smoke, CI repair, merge conflict, parallel DAG, multi-project overrides. | Longer-running | | Large DAG stress | `pnpm run test:orchestration:large-dag` | Heavy 129-task mockup DAG with wide fan-out and layered joins. | Long-running | | Full heavy pentest | `pnpm run test:orchestration:pentest` | Default mockup catalog plus heavy stress scenarios. | Long-running | | Backend broadening | `pnpm run test:backend` | Full backend suite after focused fixes. | Medium | diff --git a/docs/development/testing-and-quality.md b/docs/development/testing-and-quality.md index 9cbd322163..0fc0fbd93e 100644 --- a/docs/development/testing-and-quality.md +++ b/docs/development/testing-and-quality.md @@ -257,9 +257,9 @@ OPENROUTER_API_KEY=... CODEUX_E2E_OPENROUTER_MODEL=openai/gpt-5-mini node script Set `CODEUX_E2E_OPENROUTER_MODEL` only when validating a different OpenRouter model intentionally. -### Mockup Sprint Pentest Policy +### Mockup Sprint Orchestration Policy -The credential-free mockup sprint pentest workflow is `.github/workflows/mockup-sprint-pentest.yml`. It validates sprint orchestration through the hidden internal `mockup-cli` provider, requires Docker, and runs `pnpm run test:e2e:mockup-sprint-pentest` after `pnpm run build`. +The credential-free mockup sprint orchestration workflow is `.github/workflows/mockup-sprint-orchestration.yml`. It validates sprint orchestration through the hidden internal `mockup-cli` provider, requires Docker, and runs `pnpm run test:orchestration:full` on pushes and pull requests targeting `dev` or `main`. The heavy `test:orchestration:pentest` lane remains manual-only and is not part of CI. Use [Mockup Sprint Pentest](./mockup-sprint-pentest.md) for local commands, CI trigger and artifact policy, covered scenarios, and the distinction between this no-secret lane and the credentialed OpenRouter validation lane. diff --git a/docs/index.md b/docs/index.md index 30ea6524f7..41d131974d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,15 +22,16 @@ Use this page as the main entrypoint. 13. [Sprint Imports](./dashboard/sprint-imports.md) 14. [Styleguides and Tech Stacks](./dashboard/styleguides-and-tech-stacks.md) 15. [Project Initialization](./dashboard/project-initialization.md) -16. [Quicksprint Templates](./dashboard/quicksprint-templates.md) -17. [Nodes](./dashboard/nodes.md) -18. [Nodes Canvas](./dashboard/nodes-canvas.md) -19. [Node Flows Dashboard](./dashboard/node-flows.md) -20. [Scheduler](./dashboard/scheduler.md) -21. [Memory Architecture and Search](./dashboard/memory.md) -22. [Mobile Responsiveness](./dashboard/mobile-responsiveness.md) -23. [Testing and Quality](./development/testing-and-quality.md) -24. [Glossary](./reference/glossary.md) +16. [Dashboard Feature Flags](./dashboard/feature-flags.md) +17. [Quicksprint Templates](./dashboard/quicksprint-templates.md) +18. [Nodes](./dashboard/nodes.md) +19. [Nodes Canvas](./dashboard/nodes-canvas.md) +20. [Node Flows Dashboard](./dashboard/node-flows.md) +21. [Scheduler](./dashboard/scheduler.md) +22. [Memory Architecture and Search](./dashboard/memory.md) +23. [Mobile Responsiveness](./dashboard/mobile-responsiveness.md) +24. [Testing and Quality](./development/testing-and-quality.md) +25. [Glossary](./reference/glossary.md) ### Path 2: Building or modifying orchestration behavior 1. [System Overview](./architecture/system-overview.md) @@ -178,6 +179,7 @@ Use this page as the main entrypoint. - [Stats & Analytics Design System](./dashboard/design-system-stats.md) - Warm Void analytics surfaces and System administration - [Dashboard Onboarding](./dashboard/onboarding.md) - [Project Initialization](./dashboard/project-initialization.md) +- [Dashboard Feature Flags](./dashboard/feature-flags.md) - [Styleguides and Tech Stacks](./dashboard/styleguides-and-tech-stacks.md) - [Sprint Imports](./dashboard/sprint-imports.md) - [Quicksprint Templates](./dashboard/quicksprint-templates.md) diff --git a/scripts/sync-docs-web.mjs b/scripts/sync-docs-web.mjs index f64fa781f2..b70937309b 100644 --- a/scripts/sync-docs-web.mjs +++ b/scripts/sync-docs-web.mjs @@ -27,6 +27,9 @@ const pinnedOrder = new Map([ ["user-dashboard-live-session", 150], ["user-dashboard-chat", 160], ["user-dashboard-agents", 170], + ["user-dashboard-nodes", 175], + ["user-dashboard-nodes-canvas", 176], + ["user-dashboard-node-flows", 177], ["user-dashboard-scheduler", 180], ["user-dashboard-memory", 190], ["user-dashboard-knowledge", 200], @@ -41,6 +44,7 @@ const pinnedOrder = new Map([ ["developer-http-api", 330], ["developer-websocket-realtime", 340], ["developer-configuration", 350], + ["developer-feature-flags", 355], ["developer-settings-reference", 360], ["developer-sprint-format", 370], ["developer-building-from-source", 380], diff --git a/src/services/docs-web-catalog-service.ts b/src/services/docs-web-catalog-service.ts index c5a034518d..76a0c66166 100644 --- a/src/services/docs-web-catalog-service.ts +++ b/src/services/docs-web-catalog-service.ts @@ -30,6 +30,9 @@ const PINNED_ORDER = new Map([ ["user-dashboard-live-session", 150], ["user-dashboard-chat", 160], ["user-dashboard-agents", 170], + ["user-dashboard-nodes", 175], + ["user-dashboard-nodes-canvas", 176], + ["user-dashboard-node-flows", 177], ["user-dashboard-scheduler", 180], ["user-dashboard-memory", 190], ["user-dashboard-knowledge", 200], @@ -43,6 +46,7 @@ const PINNED_ORDER = new Map([ ["developer-http-api", 330], ["developer-websocket-realtime", 340], ["developer-configuration", 350], + ["developer-feature-flags", 355], ["developer-settings-reference", 360], ["developer-sprint-format", 370], ["developer-building-from-source", 380], diff --git a/tests/backend/ci/workflow-health.test.ts b/tests/backend/ci/workflow-health.test.ts index 6f924ab3a7..bf3e775080 100644 --- a/tests/backend/ci/workflow-health.test.ts +++ b/tests/backend/ci/workflow-health.test.ts @@ -9,7 +9,7 @@ const WORKFLOWS = { playwright: ".github/workflows/playwright.yml", releaseChecks: ".github/workflows/release-checks.yml", openrouterSprintE2e: ".github/workflows/openrouter-sprint-e2e.yml", - mockupSprintPentest: ".github/workflows/mockup-sprint-pentest.yml", + mockupSprintOrchestration: ".github/workflows/mockup-sprint-orchestration.yml", } as const; const PLAYWRIGHT_CONFIG = "playwright.config.ts"; @@ -252,14 +252,14 @@ describe("GitHub workflow health", () => { expect(config).toContain("...devices['Pixel 5']"); }); - it("keeps mockup sprint pentest on a Docker-backed no-secret Linux CI lane", async () => { - const workflow = await readRepoFile(WORKFLOWS.mockupSprintPentest); - const job = getJobBlock(workflow, "mockup-sprint-pentest"); + it("keeps mockup sprint orchestration on a Docker-backed no-secret Linux CI lane", async () => { + const workflow = await readRepoFile(WORKFLOWS.mockupSprintOrchestration); + const job = getJobBlock(workflow, "mockup-sprint-orchestration"); - expect(workflow).toContain("Mockup Sprint Pentest (temporary dev validation)"); - expectConcurrencyCancellation(workflow, "Mockup sprint pentest"); - expect(workflow).toMatch(/push:\n branches:\n - main\n # Dev pushes run the full mockup pentest catalog/); - expect(workflow).toContain("- dev"); + expect(workflow).toContain("Mockup Sprint Orchestration"); + expectConcurrencyCancellation(workflow, "Mockup sprint orchestration"); + expect(workflow).toMatch(/push:\n branches: \[main, dev\]/); + expect(workflow).toMatch(/pull_request:\n branches: \[main, dev\]/); expect(workflow).toContain("workflow_dispatch:"); expect(job).toContain("runs-on: ubuntu-latest"); @@ -268,15 +268,14 @@ describe("GitHub workflow health", () => { expect(job).toContain("run_install: false"); expect(job).toContain("uses: actions/setup-node@v5"); expect(job).toContain("node-version: 22"); - expect(job).toContain("run: pnpm install --frozen-lockfile"); - expect(job).toContain("run: pnpm run build"); + expect(job).toContain("run: pnpm install --frozen-lockfile --ignore-scripts"); expect(job).toContain("docker version"); - expect(job).toContain("Docker is required for the mockup sprint pentest Docker lane."); - expect(job).toContain('if [ "${GITHUB_REF_NAME}" = "dev" ]; then'); - expect(job).toContain("node scripts/e2e/run-mockup-sprint-pentest.mjs --scenario pentest --timeout-ms 3600000"); - expect(job).toContain("pnpm run test:e2e:mockup-sprint-pentest"); - expectCommandBefore(job, "run: pnpm run build", "- name: Verify Docker availability"); - expectCommandBefore(job, "docker version", 'if [ "${GITHUB_REF_NAME}" = "dev" ]; then'); + expect(job).toContain("Docker is required for the mockup sprint orchestration lane."); + expect(job).toContain("run: pnpm run test:orchestration:full"); + expect(job).not.toContain("run: pnpm run build"); + expect(job).not.toContain("test:orchestration:pentest"); + expect(job).not.toContain("run-mockup-sprint-pentest.mjs --scenario pentest"); + expectCommandBefore(job, "docker version", "run: pnpm run test:orchestration:full"); expect(job).toMatch(/if: \$\{\{ failure\(\) \|\| hashFiles\('\.cache\/e2e-mockup-sprint-pentest\/\*\*'\) != '' \}\}/); expect(job).toContain("uses: actions/upload-artifact@v4"); diff --git a/tests/dashboard/lib/dashboard-feature-flags.test.ts b/tests/dashboard/lib/dashboard-feature-flags.test.ts new file mode 100644 index 0000000000..0794e3218f --- /dev/null +++ b/tests/dashboard/lib/dashboard-feature-flags.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { + parseDashboardFeatureFlagValue, + resolveDashboardFeatureFlags, +} from "../../../dashboard/src/v2/lib/dashboard-feature-flags.js"; +import { getPrimaryNavigationItems } from "../../../dashboard/src/v2/lib/navigation-items.js"; +import { canPrefetchRoute } from "../../../dashboard/src/v2/router/route-prefetch.js"; + +const navigationLabels = (featureFlags: { nodes: boolean }): string[] => ( + getPrimaryNavigationItems("EXPERT", { featureFlags }).map((item) => item.label) +); + +describe("dashboard feature flags", () => { + it("parses explicit enabled and disabled values", () => { + expect(parseDashboardFeatureFlagValue("true")).toBe(true); + expect(parseDashboardFeatureFlagValue("1")).toBe(true); + expect(parseDashboardFeatureFlagValue("enabled")).toBe(true); + expect(parseDashboardFeatureFlagValue("false")).toBe(false); + expect(parseDashboardFeatureFlagValue("0")).toBe(false); + expect(parseDashboardFeatureFlagValue("off")).toBe(false); + expect(parseDashboardFeatureFlagValue("")).toBeNull(); + expect(parseDashboardFeatureFlagValue("maybe")).toBeNull(); + }); + + it("shows unfinished features by default in development mode", () => { + expect(resolveDashboardFeatureFlags({ devMode: true })).toEqual({ nodes: true }); + }); + + it("hides unfinished features by default outside development mode", () => { + expect(resolveDashboardFeatureFlags({ devMode: false })).toEqual({ nodes: false }); + }); + + it("lets explicit values override the mode default", () => { + expect(resolveDashboardFeatureFlags({ devMode: true, values: { nodes: "false" } })).toEqual({ nodes: false }); + expect(resolveDashboardFeatureFlags({ devMode: false, values: { nodes: "true" } })).toEqual({ nodes: true }); + }); + + it("filters Nodes from shared navigation and prefetch when disabled", () => { + expect(navigationLabels({ nodes: false })).not.toContain("Nodes"); + expect(canPrefetchRoute("/nodes", { nodes: false })).toBe(false); + }); + + it("keeps Nodes in shared navigation and prefetch when enabled", () => { + expect(navigationLabels({ nodes: true })).toContain("Nodes"); + expect(canPrefetchRoute("/nodes", { nodes: true })).toBe(true); + }); +}); diff --git a/tests/dashboard/v2/nodes-page.test.tsx b/tests/dashboard/v2/nodes-page.test.tsx index e655834661..cfc75b8d61 100644 --- a/tests/dashboard/v2/nodes-page.test.tsx +++ b/tests/dashboard/v2/nodes-page.test.tsx @@ -133,10 +133,12 @@ describe("NodesPage", () => { expect(screen.getByRole("button", { name: /Project Trigger trigger node/i })).toBeInTheDocument(); }); - it("registers /nodes in route prefetch and shared navigation", () => { + it("feature-gates /nodes route registration and prefetch while keeping shared metadata", () => { expect(mainSource).toContain('import("./v2/NodesPage.js")'); expect(mainSource).toContain('path: "/nodes"'); - expect(prefetchSource).toContain('"/nodes": () => import("../NodesPage.js")'); + expect(mainSource).toContain('...(nodesFeatureEnabled ? [nodesRoute] : [])'); + expect(prefetchSource).toContain('"/nodes": { importer: () => import("../NodesPage.js"), feature: "nodes" }'); + expect(prefetchSource).toContain("canPrefetchRoute(path)"); const navItem = ALL_NAVIGATION_ITEMS.find((item) => item.id === "nodes"); expect(navItem).toBeDefined();