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
5 changes: 3 additions & 2 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ paths:
- **Floats: `toBeCloseTo(value, decimals)`** — never `toBe()`.
- **Zustand stores: reset state in `beforeEach`** via `useStore.setState(useStore.getInitialState())`.
- **i18n in tests: register your own loader** via `import.meta.glob` before `beforeAll` — never assume global registration.
- **Never `Math.random()`** in stats tests — use seeded PRNG helpers.
- **Never `Math.random()`** in tests — use `mulberry32(seed)` from `packages/core/src/__tests__/helpers/stressDataGenerator.ts` (or copy the 9-line helper if cross-package import is awkward). Seeded PRNGs make failures reproduce deterministically.
- **`fake-indexeddb/auto` is globally installed via root `test/setup.ts`.** Do NOT remove that import — any test that transitively pulls in a Dexie-backed store (e.g. `canvasViewportStore` via Canvas tests in `packages/ui`) will hang silently in jsdom without it. The hang has no stack trace — the test never starts.
- **E2E selectors: `data-testid`** only — text/role/class change with i18n + theme.
- Known flaky: `packages/hooks/src/__tests__/index.test.ts` under concurrent Turbo load — passes in isolation; retry once before treating as failure.
- **Architecture tests** (structural-absence guards): see `docs/05-technical/implementation/testing.md` "Architecture Tests (Structural-Absence Guards)" — read-once + per-name regex pattern, denylist limits, branded-type follow-up.
- **Architecture tests** (structural-absence guards): see `docs/05-technical/implementation/testing.md` "Architecture Tests (Structural-Absence Guards)" — read-once + per-name regex pattern, denylist limits, branded-type follow-up. Scope is **single-package only** (e.g. the Cpk aggregation guard scans `@variscout/core` only). State the scope honestly in the test's docstring.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Shared agent map: `docs/llms.txt`
- `pnpm test` — all packages (turbo)
- `pnpm build` — all packages + apps
- `claude --chrome` — enable the **official [Claude for Chrome extension](https://claude.com/claude-for-chrome)** for browser-assisted E2E (drives your real Chrome with your login state).
- Local test cheatsheet (`--ui`, `--changed`, `--bail`, `--reporter=verbose`): see `docs/05-technical/implementation/testing.md` § "Local TDD cheatsheet".

## Where to look

Expand Down
26 changes: 26 additions & 0 deletions docs/05-technical/implementation/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ pnpm --filter @variscout/core test -- --coverage

# Chrome Browser Testing
claude --chrome # Enable Chrome browser access
```

### Local TDD cheatsheet

Faster iteration than `pnpm test` for tight loops:

```bash
# One file, one package (substring match on path)
pnpm --filter @variscout/<pkg> test -- <filename>

# Browser UI for interactive runs at localhost:51204/__vitest__/
pnpm --filter @variscout/<pkg> test -- --ui

# Only tests affected by uncommitted changes
pnpm --filter @variscout/<pkg> test -- --changed

# Stop on first failure (pairs well with --changed)
pnpm --filter @variscout/<pkg> test -- --bail=1

# Per-file runtime — use before reaching for pool tuning
pnpm --filter @variscout/<pkg> test -- --reporter=verbose
```

Prefer `claude --chrome` over standalone Playwright for iterative UX-level debugging — devtools console + login state are immediately available.

```bash
# Then use prompts like: "Run the staged analysis verification protocol"

# Playwright E2E (automated regression)
Expand Down
20 changes: 20 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,26 @@ export default [
'variscout/no-interaction-moderator': 'error',
},
},
// Hard rule: never Math.random() in tests — tests must be deterministic.
// Use mulberry32(seed) from packages/core/src/__tests__/helpers/stressDataGenerator.ts
// (or copy the 9-line helper inline). See .claude/rules/testing.md.
{
files: [
'**/__tests__/**/*.{ts,tsx}',
'**/*.test.{ts,tsx}',
'**/*.spec.{ts,tsx}',
],
rules: {
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.object.name='Math'][callee.property.name='random']",
message:
'Tests must be deterministic — use mulberry32(seed) from packages/core/src/__tests__/helpers/stressDataGenerator.ts instead of Math.random().',
},
],
},
},
// Persistence boundary guard (F1+F2 P7.2, audit R12+R13):
// Domain stores and non-persistence app code must not import `dexie` directly.
// Persistence access is via @variscout/core HubRepository (pwaHubRepository /
Expand Down
34 changes: 22 additions & 12 deletions packages/core/src/__tests__/stackDetection.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, it, expect } from 'vitest';
import { detectColumns } from '../parser';
import type { DataRow } from '../types';
import { mulberry32 } from './helpers/stressDataGenerator';

// Per @variscout/core test discipline: never Math.random() — use a seeded PRNG
// so failures reproduce deterministically. mulberry32 is the canonical helper.

describe('detectColumns stack suggestion', () => {
it('should suggest stacking for Finland-style tourism data (83 country columns)', () => {
Expand All @@ -17,6 +21,7 @@ describe('detectColumns stack suggestion', () => {
'Estonia',
'Norway',
];
const rng = mulberry32(1);
const data: DataRow[] = Array.from({ length: 12 }, (_, i) => {
const row: DataRow = {
Year: 2020,
Expand All @@ -26,7 +31,7 @@ describe('detectColumns stack suggestion', () => {
Total: 300000 + i * 10000,
};
for (const c of countries) {
row[c] = Math.floor(5000 + Math.random() * 50000);
row[c] = Math.floor(5000 + rng() * 50000);
}
return row;
});
Expand Down Expand Up @@ -60,11 +65,12 @@ describe('detectColumns stack suggestion', () => {
});

it('should not suggest stacking when fewer than 5 numeric columns', () => {
const rng = mulberry32(2);
const data: DataRow[] = Array.from({ length: 10 }, () => ({
A: Math.random() * 100,
B: Math.random() * 100,
C: Math.random() * 100,
D: Math.random() * 100,
A: rng() * 100,
B: rng() * 100,
C: rng() * 100,
D: rng() * 100,
Label: 'X',
}));

Expand All @@ -74,10 +80,11 @@ describe('detectColumns stack suggestion', () => {
});

it('should assign medium confidence for 5-9 columns', () => {
const rng = mulberry32(3);
const data: DataRow[] = Array.from({ length: 10 }, () => {
const row: DataRow = { ID: 'X' };
for (let i = 0; i < 7; i++) {
row[`Sensor${i}`] = Math.random() * 100;
row[`Sensor${i}`] = rng() * 100;
}
return row;
});
Expand All @@ -89,10 +96,11 @@ describe('detectColumns stack suggestion', () => {
});

it('should exclude year-like numeric columns from stack', () => {
const rng = mulberry32(4);
const data: DataRow[] = Array.from({ length: 10 }, (_, i) => {
const row: DataRow = { Year: 2015 + i };
for (let j = 0; j < 6; j++) {
row[`Country${j}`] = Math.floor(Math.random() * 10000);
row[`Country${j}`] = Math.floor(rng() * 10000);
}
return row;
});
Expand All @@ -105,13 +113,14 @@ describe('detectColumns stack suggestion', () => {

it('should separate columns with very different magnitudes into clusters', () => {
// Mix: 5 columns with values ~100 and 5 columns with values ~1,000,000
const rng = mulberry32(5);
const data: DataRow[] = Array.from({ length: 20 }, () => {
const row: DataRow = { ID: 'X' };
for (let i = 0; i < 5; i++) {
row[`Small${i}`] = Math.random() * 100;
row[`Small${i}`] = rng() * 100;
}
for (let i = 0; i < 5; i++) {
row[`Large${i}`] = Math.random() * 1000000;
row[`Large${i}`] = rng() * 1000000;
}
return row;
});
Expand All @@ -123,13 +132,14 @@ describe('detectColumns stack suggestion', () => {
});

it('should exclude columns matching strong outcome keywords', () => {
const rng = mulberry32(6);
const data: DataRow[] = Array.from({ length: 10 }, () => {
const row: DataRow = {
temperature: Math.random() * 100,
pressure: Math.random() * 10,
temperature: rng() * 100,
pressure: rng() * 10,
};
for (let i = 0; i < 6; i++) {
row[`Zone${i}`] = Math.random() * 100;
row[`Zone${i}`] = rng() * 100;
}
return row;
});
Expand Down
4 changes: 4 additions & 0 deletions packages/stores/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
pnpm --filter @variscout/stores test
```

## Testing

Per-package `src/__tests__/setup.ts` (NOT root `test/setup.ts`) — mocks `idb-keyval` with an in-memory Map for Zustand persist + clears it between tests. New Dexie-backed stores: mirror `canvasViewportStore.test.ts:1` and `import 'fake-indexeddb/auto'` at file top.

## Related

- ADR-041, ADR-064, ADR-065, ADR-078, ADR-080
Expand Down
13 changes: 7 additions & 6 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import '@testing-library/jest-dom';
// Install fake-indexeddb globally so tests that pull in Dexie-backed stores
// (e.g. canvasViewportStore via Canvas.test.tsx in packages/ui) don't hang
// waiting for an IndexedDB that jsdom doesn't provide. Previously only the
// stores package + the dedicated canvasViewportStore.test.ts registered this,
// so the @variscout/ui suite stalled on Canvas.test.tsx under concurrent
// turbo load (decision-log line 57, 2026-05-14).
// Install fake-indexeddb globally for ALL test files that load this shared
// setup. Any test that transitively imports a Dexie-backed store — most
// notably `useCanvasViewportStore` (which Canvas tests in packages/ui pull
// in) — will hang silently in jsdom without an IndexedDB implementation.
// The hang has no stack trace: the test never starts. Decision-log line 57
// (2026-05-14) and `.claude/rules/testing.md` document the trap.
// Do NOT remove this import — leaving the shim global is cheap insurance.
import 'fake-indexeddb/auto';
import { vi } from 'vitest';

Expand Down