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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Adding a cross-boundary feature:
- **Source policy (Vitest registry):** file-local / small-glob invariants live in [`src/architecture/sourcePolicyRules.ts`](src/architecture/sourcePolicyRules.ts) + walker [`sourcePolicy.ts`](src/architecture/sourcePolicy.ts) — prefer adding a rule there over a new `scripts/check-*.mjs`. Suppress with `// source-policy-ok <rule-id> <reason>`. Pre-commit always appends `src/architecture/sourcePolicy.test.ts` when any TypeScript under `src/` is staged (the registry is not import-related). Keep cross-cutting always-on hygiene in existing `check:*` scanners.
- **Reticulum sidecar (Rust):** Clippy + rustfmt via `pnpm run check:reticulum-sidecar` (full-feature fmt + Clippy + test when `cargo` is on `PATH` **and** sidecar-related paths are staged) and the same feature set in `reticulum-sidecar.yaml`. Coverage threshold (`cargo llvm-cov --fail-under-lines`) is enforced only in `tests.yaml` when sidecar paths change — not in pre-commit.
- **Temp dirs in tests:** Use `mkdtempSync(path.join(os.tmpdir(), 'prefix-'))` — never write to a fixed name under `os.tmpdir()` (CodeQL + `check:insecure-temp-files`).
- Vitest worker pool sizes and shared Vite dep inline lists live in `vitest.harness.ts` — update when adding deps that need inlining.
- Vitest worker pool sizes and shared Vite dep inline lists live in `vitest.harness.mts` — update when adding deps that need inlining.
- Prefer `mockConsoleWarn` / `withMockedConsoleWarn` from `src/renderer/lib/vitestConsoleMock.ts` over ad-hoc `vi.spyOn(console, 'warn')` in renderer tests.
- Monolithic runtimes (`useMeshtasticRuntime`, `useMeshcoreRuntime`, `noble-ble-manager`) may use **source contract tests** (`sourceContractTestHelpers.ts`, `*.reconnect*.test.ts`) when full integration mocking is impractical — see [development-environment.md](docs/development-environment.md#vitest-projects-and-worker-allocation). Runtime contract tests that load `use*Runtime.ts` must use `loadRuntimeSource()` (enforced by source policy).
- Mock console before spying logged errors: `vi.spyOn(console, 'warn').mockImplementation(() => {})` in `beforeEach` when shared.
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Path alias `@/*` maps to `src/*` (see `tsconfig.json`).

**Entry points:** `src/main/index.ts`, `src/preload/index.ts`, `src/renderer/main.tsx`, `src/renderer/App.tsx`.

**Repo root (not exhaustive):** `.github/workflows/`, `scripts/check-*.mjs` (IPC, migrations, log injection, etc.), `docs/`, `resources/`, `vite.config.ts`, `electron-builder.yml`, `package.json`.
**Repo root (not exhaustive):** `.github/workflows/`, `scripts/check-*.mjs` (IPC, migrations, log injection, etc.), `docs/`, `resources/`, `vite.config.mts`, `electron-builder.yml`, `package.json`.

## Process boundaries

Expand Down
4 changes: 2 additions & 2 deletions docs/development-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,15 +519,15 @@ Not installed by pnpm (install separately when needed):

#### Vitest projects and worker allocation

`vitest.config.ts` defines three projects:
`vitest.config.mts` defines three projects:

| Project | Environment | Role |
| ---------------- | ----------- | --------------------------------------- |
| `renderer-ui` | jsdom | Component/hook tests with setup stubs |
| `renderer-logic` | node | Pure renderer unit tests (no setup) |
| `main` | node | Main, shared, preload, and script tests |

Worker counts are derived in [`vitest.harness.ts`](../vitest.harness.ts) via `computeVitestMaxWorkers(cpuCount, ratio)`: jsdom workers use `RENDERER_UI_CPU_RATIO` because they are memory-heavy; node workers use `NODE_WORKER_CPU_RATIO`. Both pools floor at `MIN_VITEST_WORKERS` (currently 2) and cap effective CPU count at `MAX_VITEST_CPU_COUNT` (32). When tuning worker allocation, change those constants in the harness (not this doc). Shared Vite dependency inline lists (`VITEST_CORE_DEPS`, `VITEST_SERVER_INLINE_DEPS`) also live there — add new deps to the harness when tests need them inlined.
Worker counts are derived in [`vitest.harness.mts`](../vitest.harness.mts) via `computeVitestMaxWorkers(cpuCount, ratio)`: jsdom workers use `RENDERER_UI_CPU_RATIO` because they are memory-heavy; node workers use `NODE_WORKER_CPU_RATIO`. Both pools floor at `MIN_VITEST_WORKERS` (currently 2) and cap effective CPU count at `MAX_VITEST_CPU_COUNT` (32). When tuning worker allocation, change those constants in the harness (not this doc). Shared Vite dependency inline lists (`VITEST_CORE_DEPS`, `VITEST_SERVER_INLINE_DEPS`) also live there — add new deps to the harness when tests need them inlined.

By default all three Vitest projects run in **parallel** (`groupOrder: 0`). On memory-constrained hosts, set `VITEST_SEQUENTIAL_PROJECTS=1` to run `renderer-ui` first, then `renderer-logic` + `main` together (legacy behavior).

Expand Down
4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default tseslint.config(
'eslint.config.mjs',
'**/*.d.ts',
'eslint.config.*',
'vitest.config.ts',
'vite.config.ts',
'vitest.config.mts',
'vite.config.mts',
'*.config.{ts,js,mjs,cjs}',
'dist-electron/**',
'coverage/**',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-security": "^4.0.1",
"eslint-plugin-simple-import-sort": "^13.0.0",
"fake-indexeddb": "^6.2.5",
"jsdom": "^29.1.1",
"leaflet": "^1.9.4",
"license-checker-rseidelsohn": "^4.4.2",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions scripts/precommit-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function isMainProjectPath(filePath) {
p.startsWith('src/architecture/') ||
p.startsWith('scripts/') ||
p === 'vitest.harness.ts' ||
p === 'vitest.harness.mts' ||
p === 'vitest.harness.test.ts'
);
}
Expand Down
4 changes: 3 additions & 1 deletion scripts/precommit-tests.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
describe('precommit-tests force-full', () => {
it('detects vitest harness and lockfile', () => {
expect(isForceFullSuitePath('vitest.harness.ts')).toBe(true);
expect(isForceFullSuitePath('vitest.harness.mts')).toBe(true);
expect(isForceFullSuitePath('vitest.config.ts')).toBe(true);
expect(isForceFullSuitePath('vitest.config.mts')).toBe(true);
expect(isForceFullSuitePath('package.json')).toBe(true);
expect(isForceFullSuitePath('pnpm-lock.yaml')).toBe(true);
expect(isForceFullSuitePath('src/renderer/vitest.setup.ts')).toBe(true);
Expand Down Expand Up @@ -159,7 +161,7 @@ describe('precommit-tests runPrecommitTests', () => {

describe('precommit-tests shouldForceFullSuite', () => {
it('is true when any force-full path is present', () => {
expect(shouldForceFullSuite(['src/main/index.ts', 'vitest.config.ts'])).toBe(true);
expect(shouldForceFullSuite(['src/main/index.ts', 'vitest.config.mts'])).toBe(true);
expect(shouldForceFullSuite(['src/main/index.ts'])).toBe(false);
});
});
7 changes: 6 additions & 1 deletion src/main/vite-config.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import { describe, expect, it } from 'vitest';

const VITE_CONFIG = readFileSync(join(__dirname, '../../vite.config.ts'), 'utf-8');
const VITE_CONFIG = readFileSync(join(__dirname, '../../vite.config.mts'), 'utf-8');

describe('vite build config', () => {
it('disables source maps for production bundles', () => {
Expand All @@ -14,4 +14,9 @@ describe('vite build config', () => {
it('sets chunk size warning limit for Electron renderer advisory threshold', () => {
expect(VITE_CONFIG).toMatch(/chunkSizeWarningLimit:\s*1000/);
});

it('uses import.meta.dirname for native ESM configLoader compatibility', () => {
expect(VITE_CONFIG).not.toMatch(/__dirname/);
expect(VITE_CONFIG).toMatch(/import\.meta\.dirname/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @vitest-environment jsdom
*
* Needs window.electronAPI; keep in renderer-logic via per-file jsdom rather than
* listing in vitest.config.ts RENDERER_LOGIC_EXCLUDE (eslint-ignored config).
* listing in vitest.config.mts RENDERER_LOGIC_EXCLUDE (eslint-ignored config).
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

Expand Down
55 changes: 55 additions & 0 deletions src/renderer/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import '@testing-library/jest-dom';
import 'vitest-axe/extend-expect';
// emoji-picker-element (ChatPanel) opens IndexedDB; jsdom has none.
import 'fake-indexeddb/auto';

import { cleanup } from '@testing-library/react';
import i18next from 'i18next';
Expand All @@ -11,6 +13,59 @@ import en from './locales/en/translation.json';
import { createElectronAPIMock, resetElectronAPIOutboxMock } from './vitest.electronApiMock';

expect.extend(matchers);

// jsdom omits structuredClone; fake-indexeddb requires it (Node engines provide it).
if (typeof globalThis.structuredClone === 'function') {
vi.stubGlobal('structuredClone', globalThis.structuredClone.bind(globalThis));
}

/** Empty DOMRect for jsdom Range / IntersectionObserver stubs. */
function emptyDomRect(): DOMRect {
return {
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON() {
return this;
},
};
}

// emoji-picker-element uses IntersectionObserver after IndexedDB opens (jsdom has none).
interface IntersectionObserverStubInstance {
root: null;
rootMargin: string;
thresholds: number[];
observe: ReturnType<typeof vi.fn>;
unobserve: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
takeRecords: ReturnType<typeof vi.fn>;
}
vi.stubGlobal(
'IntersectionObserver',
vi.fn(function IntersectionObserverStub(this: IntersectionObserverStubInstance) {
this.root = null;
this.rootMargin = '';
this.thresholds = [];
this.observe = vi.fn();
this.unobserve = vi.fn();
this.disconnect = vi.fn();
this.takeRecords = vi.fn(() => []);
}),
);

// emoji-picker-element ZWJ checks call Range#getBoundingClientRect (incomplete in jsdom).
if (typeof Range !== 'undefined' && typeof Range.prototype.getBoundingClientRect !== 'function') {
Range.prototype.getBoundingClientRect = emptyDomRect;
}
if (typeof Range !== 'undefined' && typeof Range.prototype.getClientRects !== 'function') {
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
}
afterEach(() => {
cleanup();
resetElectronAPIOutboxMock();
Expand Down
6 changes: 4 additions & 2 deletions tsconfig.main.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": [
"src/main/**/*",
"src/preload/**/*",
"src/shared/**/*",
"src/architecture/**/*",
"src/main/types/**/*",
"vitest.harness.ts",
"vitest.harness.mts",
"vitest.harness.test.ts"
],
"exclude": ["node_modules", "dist"]
Expand Down
30 changes: 15 additions & 15 deletions vite.config.ts → vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import react from '@vitejs/plugin-react';
/** Emscripten glue fetches `orlp-ed25519.wasm` relative to the page; it is not inlined by Vite. */
const ORLP_WASM_NAME = 'orlp-ed25519.wasm';
const ORLP_WASM_SRC = path.resolve(
__dirname,
import.meta.dirname,
`node_modules/@michaelhart/meshcore-decoder/lib/${ORLP_WASM_NAME}`,
);

Expand Down Expand Up @@ -37,9 +37,9 @@ function meshcoreOrlpWasmPlugin(): import('vite').Plugin {
if (!existsSync(ORLP_WASM_SRC)) return;
const buf = readFileSync(ORLP_WASM_SRC);
// Next to index.html (relative URL "orlp-ed25519.wasm" from document)
writeFileSync(path.resolve(__dirname, 'dist/renderer', ORLP_WASM_NAME), buf);
writeFileSync(path.resolve(import.meta.dirname, 'dist/renderer', ORLP_WASM_NAME), buf);
// Some Emscripten builds resolve WASM relative to the JS chunk in assets/
writeFileSync(path.resolve(__dirname, 'dist/renderer/assets', ORLP_WASM_NAME), buf);
writeFileSync(path.resolve(import.meta.dirname, 'dist/renderer/assets', ORLP_WASM_NAME), buf);
},
};
}
Expand Down Expand Up @@ -68,10 +68,10 @@ export default defineConfig({
// Electron's renderer already has process.type === "renderer"; this matches that.
'process.type': JSON.stringify('renderer'),
},
root: path.resolve(__dirname, 'src/renderer'),
root: path.resolve(import.meta.dirname, 'src/renderer'),
base: './',
build: {
outDir: path.resolve(__dirname, 'dist/renderer'),
outDir: path.resolve(import.meta.dirname, 'dist/renderer'),
emptyOutDir: true,
sourcemap: false,
// Production: omit source maps (smaller bundle). For crash symbolication use
Expand Down Expand Up @@ -109,22 +109,22 @@ export default defineConfig({
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@': path.resolve(import.meta.dirname, 'src'),
// Node built-ins imported by transitive deps (e.g. @meshtastic/core 2.6.7 via
// @meshtastic/transport-web-serial). Listing them as rollup externals emits bare
// `import "os"` etc. which the browser rejects. Redirect to renderer-safe stubs instead.
fs: path.resolve(__dirname, 'src/renderer/shims/node-fs-stub.ts'),
os: path.resolve(__dirname, 'src/renderer/shims/node-os-stub.ts'),
path: path.resolve(__dirname, 'src/renderer/shims/node-path-stub.ts'),
util: path.resolve(__dirname, 'src/renderer/shims/node-util-stub.ts'),
stream: path.resolve(__dirname, 'src/renderer/shims/node-stream-stub.ts'),
child_process: path.resolve(__dirname, 'src/renderer/shims/node-child-process-stub.ts'),
net: path.resolve(__dirname, 'src/renderer/shims/node-net-stub.ts'),
events: path.resolve(__dirname, 'src/renderer/shims/node-events-stub.ts'),
fs: path.resolve(import.meta.dirname, 'src/renderer/shims/node-fs-stub.ts'),
os: path.resolve(import.meta.dirname, 'src/renderer/shims/node-os-stub.ts'),
path: path.resolve(import.meta.dirname, 'src/renderer/shims/node-path-stub.ts'),
util: path.resolve(import.meta.dirname, 'src/renderer/shims/node-util-stub.ts'),
stream: path.resolve(import.meta.dirname, 'src/renderer/shims/node-stream-stub.ts'),
child_process: path.resolve(import.meta.dirname, 'src/renderer/shims/node-child-process-stub.ts'),
net: path.resolve(import.meta.dirname, 'src/renderer/shims/node-net-stub.ts'),
events: path.resolve(import.meta.dirname, 'src/renderer/shims/node-events-stub.ts'),
},
},
css: {
postcss: path.resolve(__dirname, 'postcss.config.cjs'),
postcss: path.resolve(import.meta.dirname, 'postcss.config.cjs'),
},
server: {
port: 5173,
Expand Down
14 changes: 6 additions & 8 deletions vitest.config.ts → vitest.config.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { readdirSync } from 'node:fs';
import os from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { join, relative, resolve } from 'node:path';

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
Expand All @@ -11,10 +10,9 @@ import {
resolveVitestProjectMaxWorkers,
VITEST_CORE_DEPS,
VITEST_SERVER_INLINE_DEPS,
} from './vitest.harness';
} from './vitest.harness.mts';

const __dirname = dirname(fileURLToPath(import.meta.url));
const srcAlias = { '@': resolve(__dirname, 'src') };
const srcAlias = { '@': resolve(import.meta.dirname, 'src') };
const cpuCount = os.cpus().length;

/** Per-project CI shards skip global thresholds; merge job enforces them on combined coverage. */
Expand Down Expand Up @@ -179,15 +177,15 @@ const RENDERER_LOGIC_LIB_UI_FALLBACK = new Set(
);

function collectRendererLibTestFiles(): string[] {
const libRoot = join(__dirname, 'src/renderer/lib');
const libRoot = join(import.meta.dirname, 'src/renderer/lib');
const results: string[] = [];
const walk = (dir: string): void => {
for (const ent of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, ent.name);
if (ent.isDirectory()) {
walk(path);
} else if (ent.name.endsWith('.test.ts')) {
results.push(relative(__dirname, path).split('\\').join('/'));
results.push(relative(import.meta.dirname, path).split('\\').join('/'));
}
}
};
Expand Down Expand Up @@ -235,7 +233,7 @@ export default defineConfig({
name: 'renderer-ui',
globals: true,
environment: 'jsdom',
setupFiles: [resolve(__dirname, 'src/renderer/vitest.setup.ts')],
setupFiles: [resolve(import.meta.dirname, 'src/renderer/vitest.setup.ts')],
include: ['src/renderer/**/*.test.{ts,tsx}'],
exclude: RENDERER_UI_EXCLUDE,
pool: 'forks',
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion vitest.harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
resolveVitestProjectMaxWorkers,
VITEST_CORE_DEPS,
VITEST_SERVER_INLINE_DEPS,
} from './vitest.harness';
} from './vitest.harness.mts';

describe('vitest.harness', () => {
it('computeVitestMaxWorkers applies ratio and MIN_VITEST_WORKERS floor', () => {
Expand Down