Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
focus-trap timing, or worker-level resource contention, rather than a stray
request.

- **A ratchet on full-module `vi.mock` calls in the frontend test suite**
(`npm run mock:lint`, wired into `npm run lint`). `vi.mock("@/lib/api", ()
=> ({ ... }))` hand-writes the entire module's shape, so when the real
module gains an export, every test that already mocks it that way keeps
compiling and silently hands the new export `undefined`, and the test that
breaks is rarely the one whose PR added it. That is exactly what happened
when `postLogin`'s MFA union return type shipped: unrelated tests failed
for a reason nobody could see from their own diff.

The gate does not migrate the 51 mocks already written this way (30
`@/lib/api`, 21 `@/lib/projectsApi`); the sanctioned fix,
`importOriginal`/`vi.importActual`, already covers 63 call sites and is
documented at `tests/unit/features/exportClients.test.ts:40`, but rewriting
the rest means checking each one against what its component actually
renders. What ships now is the same frozen-baseline ratchet
`token-lint.mjs` and `problem-detail-lint.mjs` already use: the current
count of full-module mocks per file is recorded in
`apps/frontend/scripts/mock-audit-baseline.json` and can only go down; a
new full mock, or a file's count rising, fails the gate. Every full mock
the audit finds is also checked against the real module's declared exports
and reported with the missing names when they exist, which today is most
of them, so the next person reaching for this pattern sees the actual
bug shape before they add another one.

- **A filter for work that looks owned and cannot move.** `?assignee=inactive`
returns findings assigned to somebody whose account has been deactivated.
Closing an account does not remove its assignments, because dropping them
Expand Down
4 changes: 3 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dev": "vite",
"build": "tsc -b tsconfig.app.json tsconfig.node.json && vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint": "eslint . && npm run mock:lint",
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest",
Expand All @@ -23,6 +23,8 @@
"token:lint:update": "node scripts/token-lint.mjs --update",
"problem:lint": "node scripts/problem-detail-lint.mjs",
"problem:lint:update": "node scripts/problem-detail-lint.mjs --update",
"mock:lint": "node scripts/mock-audit.mjs",
"mock:lint:update": "node scripts/mock-audit.mjs --update",
"test:a11y": "playwright test --config=playwright.a11y.config.ts",
"test:a11y:update": "A11Y_UPDATE_BASELINE=1 playwright test --config=playwright.a11y.config.ts",
"test:narrow": "playwright test --config=playwright.responsive.config.ts"
Expand Down
90 changes: 90 additions & 0 deletions apps/frontend/scripts/mock-audit-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"tests/unit/AnonymisationBacklogPanel.test.tsx": 1,
"tests/unit/App.test.tsx": 3,
"tests/unit/authStore.test.ts": 1,
"tests/unit/components/AppShellNavBadges.test.tsx": 1,
"tests/unit/components/AppShellOptionalNav.test.tsx": 3,
"tests/unit/components/AppShellSidebar.test.tsx": 3,
"tests/unit/components/CommandMenu.test.tsx": 2,
"tests/unit/components/DemoBanner.test.tsx": 1,
"tests/unit/components/HeaderBell.test.tsx": 1,
"tests/unit/features/admin/AdminTeamDrawer.test.tsx": 1,
"tests/unit/features/admin/AdminTeamsPage.test.tsx": 1,
"tests/unit/features/admin/AdminUserDrawer.test.tsx": 1,
"tests/unit/features/admin/AdminUserImportDialog.test.tsx": 1,
"tests/unit/features/admin/AdminUsersPage.test.tsx": 1,
"tests/unit/features/admin/backup/AdminBackupPage.test.tsx": 1,
"tests/unit/features/admin/disk/AdminDiskPage.test.tsx": 1,
"tests/unit/features/admin/health/AdminHealthPage.test.tsx": 2,
"tests/unit/features/admin/health/EolPanel.test.tsx": 1,
"tests/unit/features/admin/health/KevFeedPanel.test.tsx": 1,
"tests/unit/features/admin/health/MaliciousPanel.test.tsx": 1,
"tests/unit/features/admin/health/TrivyDBPanel.test.tsx": 1,
"tests/unit/features/admin/scans/AdminScansPage.test.tsx": 1,
"tests/unit/features/approvals/ApprovalsDrawer.test.tsx": 1,
"tests/unit/features/approvals/ApprovalsPage.test.tsx": 1,
"tests/unit/features/dashboard/ActionQueuePanel.test.tsx": 1,
"tests/unit/features/dashboard/DashboardPage.test.tsx": 4,
"tests/unit/features/dashboard/OnboardingChecklist.test.tsx": 1,
"tests/unit/features/dashboard/PortfolioGrid.test.tsx": 1,
"tests/unit/features/dashboard/TrendsPanel.test.tsx": 1,
"tests/unit/features/exportClients.test.ts": 1,
"tests/unit/features/external-package-lookup/ExternalPackageLookupPage.test.tsx": 2,
"tests/unit/features/intake/IntakeRequestsPage.test.tsx": 3,
"tests/unit/features/integrations/IntegrationsPage.test.tsx": 2,
"tests/unit/features/integrations/ServiceAccountsPanel.test.tsx": 3,
"tests/unit/features/notifications/NotificationsPage.test.tsx": 2,
"tests/unit/features/policies/GatePolicyPanel.test.tsx": 1,
"tests/unit/features/policies/PoliciesPage.test.tsx": 2,
"tests/unit/features/projects/ComparePage.test.tsx": 2,
"tests/unit/features/projects/ComplianceTab.test.tsx": 6,
"tests/unit/features/projects/ComponentDrawer.test.tsx": 1,
"tests/unit/features/projects/ComponentsEmptyState.test.tsx": 1,
"tests/unit/features/projects/ComponentsTab.test.tsx": 2,
"tests/unit/features/projects/GateResultCard.test.tsx": 1,
"tests/unit/features/projects/GovernanceBand.test.tsx": 1,
"tests/unit/features/projects/InheritedVerdictNote.test.tsx": 1,
"tests/unit/features/projects/LicenseDrawer.test.tsx": 1,
"tests/unit/features/projects/licensesApi.test.ts": 1,
"tests/unit/features/projects/LicenseWaiveAction.test.tsx": 1,
"tests/unit/features/projects/noticeApi.test.ts": 1,
"tests/unit/features/projects/ObligationDrawer.test.tsx": 1,
"tests/unit/features/projects/ObligationFulfilmentEditor.test.tsx": 1,
"tests/unit/features/projects/ObligationsTab.test.tsx": 2,
"tests/unit/features/projects/OverviewTab.test.tsx": 1,
"tests/unit/features/projects/ProjectCreatePage.test.tsx": 1,
"tests/unit/features/projects/projectDetailApi.test.ts": 1,
"tests/unit/features/projects/ProjectDetailPage.test.tsx": 4,
"tests/unit/features/projects/ProjectDetailSandbox.test.tsx": 9,
"tests/unit/features/projects/releasesApi.test.ts": 1,
"tests/unit/features/projects/ReleasesTab.test.tsx": 1,
"tests/unit/features/projects/ReleaseSwitcher.test.tsx": 1,
"tests/unit/features/projects/RemediationTab.test.tsx": 1,
"tests/unit/features/projects/ReportsTab.test.tsx": 1,
"tests/unit/features/projects/snapshotScanId.test.tsx": 3,
"tests/unit/features/projects/SourceFileViewer.test.tsx": 1,
"tests/unit/features/projects/SourceTab.test.tsx": 3,
"tests/unit/features/projects/SourceTree.test.tsx": 2,
"tests/unit/features/projects/upgradeClustersApi.test.ts": 1,
"tests/unit/features/projects/useLatestRelease.test.tsx": 1,
"tests/unit/features/projects/useLicenseWaive.test.tsx": 1,
"tests/unit/features/projects/useSourceTreeHooks.test.tsx": 1,
"tests/unit/features/projects/VulnerabilitiesTab.test.tsx": 3,
"tests/unit/features/scan/SbomIngestDialog.test.tsx": 1,
"tests/unit/features/search/SearchPage.test.tsx": 1,
"tests/unit/hooks/useDemoModeSandbox.test.tsx": 1,
"tests/unit/hooks/useTriggerScan.test.tsx": 3,
"tests/unit/lib/csvExport.test.ts": 1,
"tests/unit/lib/ingestSbom.test.ts": 1,
"tests/unit/pages/auth/AuthLayoutDemoBanner.test.tsx": 1,
"tests/unit/pages/auth/AuthLayoutGateway.test.tsx": 1,
"tests/unit/pages/auth/ForgotPasswordPage.test.tsx": 1,
"tests/unit/pages/auth/LoginPageDemoHint.test.tsx": 1,
"tests/unit/pages/auth/RegisterPageDemo.test.tsx": 1,
"tests/unit/pages/auth/ResetPasswordPage.test.tsx": 1,
"tests/unit/pages/ComponentDetailPage.test.tsx": 1,
"tests/unit/pages/VulnerabilityDetailPage.test.tsx": 2,
"tests/unit/ProjectDetailPage.test.tsx": 10,
"tests/unit/ProjectListPage.test.tsx": 4,
"tests/unit/ScanDetailPage.test.tsx": 4
}
63 changes: 63 additions & 0 deletions apps/frontend/scripts/mock-audit.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Type surface for the full-module mock ratchet (issue #390).
*
* The linter itself stays plain `.mjs` so CI can run it with bare node, with
* no build step. This declaration exists so tooling that imports it can be
* type-checked under `tsc --noEmit`.
*/

export interface MockAuditFinding {
/** Path relative to `apps/frontend`, POSIX separators. */
file: string;
/** 1-indexed line of the `vi.mock(...)` call. */
line: number;
/** The mocked module specifier, e.g. `@/lib/api`. */
module: string;
/** Whether the specifier resolved to a first-party source file. */
resolved: boolean;
/**
* Real value exports the mock factory does not declare, or `null` when the
* module could not be resolved, or the factory's shape could not be read
* statically (a computed key, or a spread of something other than the real
* module).
*/
missing: string[] | null;
}

export interface MockAuditScanResult {
/** Full-module mock count per file. Files with none are absent. */
counts: Record<string, number>;
findings: MockAuditFinding[];
}

export interface MockAuditBudgetBreach {
file: string;
count: number;
budget: number;
}

export interface MockAuditDiffResult {
/** True only when every file sits exactly on its recorded budget. */
ok: boolean;
/** Full mocks in files the baseline does not cover: new debt. */
added: Array<{ file: string; count: number }>;
/** Files whose count rose above the baseline. */
grew: MockAuditBudgetBreach[];
/** Files whose count fell; the lowered baseline must be committed. */
shrank: MockAuditBudgetBreach[];
total: number;
baselineTotal: number;
}

/** Walk `testsRoot`, counting full-module `vi.mock` calls per file relative
* to `frontendRoot`. */
export function scan(
testsRoot?: string,
frontendRoot?: string,
): MockAuditScanResult;

/** Compare a fresh scan against the recorded baseline. */
export function diff(
counts: Record<string, number>,
baseline: Record<string, number>,
): MockAuditDiffResult;
Loading
Loading