* refactor(launcher): self-contained validate; split smokes into top-level step (#1504) Previously `validate:launcher` built cli/tui/web and ran the cross-client smokes, so the launcher's "validation" depended on every other client being built — and the launcher had no lint/format/coverage of its own. - clients/launcher: add a web-style `validate` (format:check && lint && build && test:coverage) with a Node-only eslint flat config, prettier, and a per-file coverage gate (parse-launcher-argv.ts covered; src/index.ts excluded as binary bootstrap). 100% on the gated file. - Root `validate:launcher` now just delegates: `cd clients/launcher && npm run validate` — no dependency on the other clients. - New top-level `smoke` step (smoke:launcher + smoke:cli + smoke:tui + smoke:web) with NO build commands; `validate` runs it last. Each client's own validation builds its bundle first, so nothing builds twice (cli/tui via test:coverage, launcher via its validate; smoke:web builds dist on demand). - clients/tui test:coverage now builds the TUI bundle (mirroring cli) so the build-free smoke has it available. - CI: replace the standalone cli/tui/launcher build step with a "Validate launcher" step. Docs updated (AGENTS.md, launcher README). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli,tui,root): every client self-validates; smoke separate from validate Extends the launcher self-validate work to the CLI and TUI, and makes the top-level `validate` nothing but the per-client validations — smokes are now a separate top-level target. - clients/cli, clients/tui: add a web-style `validate` with a Node flat eslint config + prettier. CLI lint/format pass as-is. TUI registers eslint-plugin-react-hooks for the classic rules only (rules-of-hooks + exhaustive-deps, which the source already references); the stricter react-hooks@7 rules are not enforced on the interim component surface (#1501). - Avoid double builds: - CLI's e2e spawns the built binary, so its `test:coverage` builds first; `validate` folds that in (`format:check && lint && test:coverage`, no separate `build`). - TUI tests don't spawn a bundle, so `validate` keeps an explicit `build` (for the smoke) and `test:coverage` is pure. - Root: `validate` = validate:web && validate:cli && validate:tui && validate:launcher (each delegates to the client's own validate). `smoke` is no longer part of `validate` — it's a separate top-level target run after a build/validate, with no build commands of its own. - CI: per-client "Validate X" steps + a single "Run cross-client smokes" (npm run smoke). Docs updated (AGENTS.md, cli/tui READMEs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(root): drop redundant test aggregator scripts Now that every client self-validates and the top-level `validate` chains the per-client validations, the root `test` / `test:web` / `test:cli` / `test:tui` / `test:coverage` aggregators are redundant — run `npm run validate` (all) or `cd clients/<name> && npm run validate` (one). `test:storybook` stays (it's the one check not part of validate). CI was already using each client's own scripts, so it's unaffected. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(web): silence happy-dom infra noise in unit runs Passing unit runs printed dozens of alarming-but-expected lines that made green runs look broken — all from happy-dom performing real I/O with no server behind it: - `<iframe src="/sandbox.html">` (MCP Apps sandbox) made happy-dom fetch and later unload that URL, emitting ~40 `DOMException [NetworkError/ AbortError]` + `AsyncTaskManager destroyed` lines. Disable child-frame navigation in the unit project (the tests only assert on the iframe element/attributes, not its document), removing the noise with no coverage loss. - App-level renders read `GET /api/config` via useSandboxUrl / useServerListWritable, 404-ing ~76x. Add a benign default `fetch` (empty 200) in the unit setup; tests that exercise fetch still override it. Negative tests that assert on errors already suppress their own console output (e.g. jsonUtils/schemaUtils); CLI, TUI, and the web integration suite were already clean. Unit suite is now noise-free; 2424 web tests pass and the coverage gate is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(scripts): make validate fast (test), split coverage into its own target Each client's `validate` now runs `test` instead of `test:coverage`, so the everyday pre-push check skips v8 instrumentation (and, for web, the slower integration project). The per-file coverage gate moves to a dedicated top-level `coverage` script: coverage = coverage:web && coverage:cli && coverage:tui && coverage:launcher coverage:<name> = cd clients/<name> && npm run test:coverage `validate` still builds each client's bundle (cli via its test `pretest`, the others via an explicit `build`), so `npm run validate && npm run smoke` works unchanged. CI now runs `validate` (fast checks + builds), then `coverage` (the gate), then `smoke` — collapsing the old per-client steps. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: drop the coverage gate from CI; keep web integration as its own step CI no longer runs `npm run coverage` — the per-file coverage gate is now a local/manual check (`npm run coverage` before pushing). To avoid silently dropping the web integration suite (it only ran via test:coverage, and `validate` runs the unit project only), CI gains a standalone `clients/web` `test:integration` step that exercises the real stdio/HTTP/OAuth/storage paths without coverage instrumentation. CI is now: validate (fast) → web integration → smoke → storybook. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(smoke): give smoke:launcher its own script like the others smoke:launcher was the odd one out — an inline chain of three `node clients/launcher/build/index.js ... --help` commands while smoke:cli / smoke:tui / smoke:web each run a `scripts/smoke-*.mjs`. Extract it to `scripts/smoke-launcher.mjs`, which runs the same `--help` / `--cli --help` / `--tui --help` dispatch checks but now also asserts each prints that mode's usage banner (proving the launcher resolved the right client build) and verifies the required builds exist up front with a clear error. Prints a `smoke:launcher OK — ...` summary in the same style as the rest. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: reorder package.json scripts for readability Move frequently-run scripts toward the top and rarely-run-by-hand ones (pretest hooks, lint/format internals, etc.) toward the end, consistently across the root and client manifests. Pure reordering — no script names or values changed, and no other manifest content is touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: drop references to removed root inspector / test:storybook scripts The reordering dropped the root `inspector` and `test:storybook` aggregators (intentional — `inspector` is redundant with `web`, and storybook is run from clients/web in CI). Update AGENTS.md so it no longer points at `npm run inspector`, and run `test:storybook` from `clients/web/`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: address PR #1505 review (format glob gap, fetch-baseline comment) - TUI format/format:check now also cover the client-root source files (index.ts, tui.tsx, dev.ts) that live outside src/ — `eslint .` already lints them, so they were linted but not format-checked. (cli/launcher have no source outside src/; their root .ts are single-quoted configs intentionally left out of prettier, matching web.) - Reword the setup.ts default-fetch comment: it overstated auto-restore (the unit project sets no restoreMocks/unstubGlobals), and now also flags that the permissive 200 default means a test asserting on a fetch FAILURE must install its own rejecting stub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove redundant root install:clients script `install:clients` ran the same `node scripts/install-clients.mjs` as `postinstall`, which fires automatically on `npm install` — so the manual alias was redundant. Drop it and the doc references (AGENTS.md, spec). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): skip smoke:tui in CI (Ink needs a real TTY) This PR made CI run the full `npm run smoke`, which surfaced that smoke:tui fails in headless CI: the Ink TUI throws "Raw mode is not supported" on mount (no TTY) and exits before rendering its first frame. The old CI never ran smoke:tui, so this was latent. smoke-tui.mjs now self-skips when `process.env.CI` is set — the boot/ render check is inherently local-only (a TUI needs a terminal). The TUI is still built and unit-tested in CI; `npm run smoke` stays the single command everywhere (full locally, tui-skipped in CI). Verified `CI=true npm run smoke` is green (launcher/cli OK, tui SKIPPED, web OK). Docs + CI comment updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
253 lines
12 KiB
TypeScript
253 lines
12 KiB
TypeScript
/// <reference types="vitest/config" />
|
|
import { defineConfig } from 'vite';
|
|
import react from '@vitejs/plugin-react';
|
|
|
|
// https://vite.dev/config/
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
|
import { playwright } from '@vitest/browser-playwright';
|
|
import { honoMiddlewarePlugin } from './server/vite-hono-plugin';
|
|
import { getViteBaseConfig } from './server/vite-base-config';
|
|
import { buildWebServerConfigFromEnv } from './server/web-server-config';
|
|
import { vitestSharedPaths } from '../../vitest.shared.mts';
|
|
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
const { repoRoot, sharedDedupe, nodeModulesAliases, projectResolve, sharedAliases } =
|
|
vitestSharedPaths(dirname);
|
|
|
|
// Integration tests live under clients/web/src/test/integration/ and run in
|
|
// the node-env vitest project below. The folder is the manifest: anything
|
|
// inside it is integration (node env, 30s timeout, real servers); anything
|
|
// outside is a unit test (happy-dom). This prevents the silent
|
|
// misclassification trap where a file's environment depended on whether
|
|
// someone remembered to add it to an enumeration (#1314).
|
|
//
|
|
// Match `{ts,tsx}` to mirror the unit project's include below — otherwise a
|
|
// stray `.test.tsx` placed inside this folder would slip past the integration
|
|
// include AND fail to be excluded from unit, silently landing under happy-dom.
|
|
const integrationGlob = 'clients/web/src/test/integration/**/*.test.{ts,tsx}';
|
|
|
|
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
|
|
export default defineConfig({
|
|
// `honoMiddlewarePlugin` is gated by `apply: 'serve'` so it only attaches
|
|
// during `vite dev` / `vite preview` — vitest projects share this config
|
|
// but never invoke `configureServer`, so the plugin stays inert there.
|
|
//
|
|
// The plugin statically imports the node-only dev backend
|
|
// (`core/mcp/remote/node/server.ts`), so Vite's config bundler (Rolldown)
|
|
// walks that chain when it loads this file and reaches node-only deps
|
|
// (`chokidar`, `atomically`, `@napi-rs/keyring`). Those resolve cleanly at
|
|
// config-bundle time because they're declared in the repo-root
|
|
// `package.json` and installed into the repo-root `node_modules`, which sits
|
|
// on `core/`'s module-resolution chain (core/ has no node_modules of its
|
|
// own). Keep them in the root manifest: drop one and Rolldown can no longer
|
|
// resolve it from core/, reviving the benign `UNRESOLVED_IMPORT` warnings
|
|
// that #1491 eliminated at the source (by removing the old stream filter and
|
|
// build-time onwarn suppressions rather than re-hiding the symptom).
|
|
plugins: [react(), honoMiddlewarePlugin(buildWebServerConfigFromEnv())],
|
|
// Shared optimizeDeps exclusions so node-only packages
|
|
// (`@modelcontextprotocol/sdk/client/stdio.js`, `cross-spawn`, `which`)
|
|
// consumed by the dev backend aren't scanned for browser pre-bundling.
|
|
// Browser code reaches the node-side stack via the Hono plugin only.
|
|
...getViteBaseConfig(),
|
|
resolve: {
|
|
// NOTE: the unit vitest project (below) overrides this — see comment there.
|
|
//
|
|
// Once App.tsx started consuming the full hook + state-manager surface
|
|
// (#1244), the browser dep graph reached bare-module subpaths in core/
|
|
// that Rolldown couldn't resolve against `core/`'s parent (it has no
|
|
// node_modules of its own). Promote the same bare-module aliases the
|
|
// vitest projects use so `vite dev` / `vite build` can resolve them
|
|
// from `clients/web/node_modules`.
|
|
alias: [
|
|
...Object.entries(sharedAliases).map(([find, replacement]) => ({
|
|
find,
|
|
replacement,
|
|
})),
|
|
...nodeModulesAliases,
|
|
],
|
|
// Source files in core/ import bare modules (react, @testing-library/react,
|
|
// etc.) that only exist in clients/web/node_modules. Dedupe ensures Vite
|
|
// resolves them from this package rather than walking up from core/'s
|
|
// location (which has no node_modules of its own yet).
|
|
dedupe: sharedDedupe,
|
|
},
|
|
// Pin the Vite dev server to the same port (and host) the Hono plugin
|
|
// configures from env, so `allowedOrigins` actually matches the browser
|
|
// origin. Without this, `vite dev` falls back to Vite's default 5173
|
|
// while the dev backend's `buildWebServerConfigFromEnv()` defaults to
|
|
// CLIENT_PORT=6274 — origin check rejects every `/api/*` request from
|
|
// the browser. CLIENT_PORT / HOST overrides flow through here too.
|
|
// `strictPort: true` so a port collision fails loudly instead of
|
|
// silently picking a different port (which would leave `allowedOrigins`
|
|
// pointing at the wrong host and break browser fetches).
|
|
server: {
|
|
port: parseInt(process.env.CLIENT_PORT ?? '6274', 10),
|
|
host: process.env.HOST ?? 'localhost',
|
|
strictPort: true,
|
|
fs: {
|
|
allow: [path.resolve(dirname, '../..')],
|
|
},
|
|
},
|
|
test: {
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: ['text', 'html', 'json-summary'],
|
|
include: [
|
|
'src/components/**/*.{ts,tsx}',
|
|
'src/utils/**/*.{ts,tsx}',
|
|
'clients/web/server/**/*.{ts,tsx}',
|
|
path.join(repoRoot, 'core/mcp/**/*.{ts,tsx}'),
|
|
path.join(repoRoot, 'core/react/**/*.{ts,tsx}'),
|
|
path.join(repoRoot, 'core/auth/**/*.{ts,tsx}'),
|
|
path.join(repoRoot, 'core/storage/**/*.{ts,tsx}'),
|
|
path.join(repoRoot, 'core/logging/**/*.{ts,tsx}'),
|
|
],
|
|
exclude: [
|
|
'**/*.stories.{ts,tsx}',
|
|
'**/*.test.{ts,tsx}',
|
|
'**/*.fixtures.{ts,tsx}',
|
|
'**/index.{ts,tsx}',
|
|
'src/components/**/types.ts',
|
|
// Dev-backend runtime glue: each file is exercised end-to-end via
|
|
// `npm run dev` (Hono plugin attaches, banner prints, /api/* serves).
|
|
// `vite-hono-plugin.ts` requires standing up a real Vite server with
|
|
// an HTTP listener to drive `configureServer`; `server.ts` is the
|
|
// production Hono entry that the v2 launcher (not yet ported, #1246)
|
|
// will invoke; `start-vite-dev-server.ts` is its dev counterpart.
|
|
// The non-glue parts are extracted to `web-server-config.ts` (fully
|
|
// tested) and `sandbox-controller.ts` (HTTP behavior tested).
|
|
'clients/web/server/vite-hono-plugin.ts',
|
|
'clients/web/server/server.ts',
|
|
'clients/web/server/start-vite-dev-server.ts',
|
|
// Pure-type modules: `interface`/`type` declarations only, no runtime
|
|
// statements. Excluding them keeps the report clean (would otherwise
|
|
// surface as misleading 0/0 rows).
|
|
path.join(repoRoot, 'core/mcp/types.ts'),
|
|
path.join(repoRoot, 'core/mcp/elicitationCreateMessage.ts'),
|
|
path.join(repoRoot, 'core/mcp/samplingCreateMessage.ts'),
|
|
path.join(repoRoot, 'core/mcp/sessionStorage.ts'),
|
|
path.join(repoRoot, 'core/mcp/inspectorClientProtocol.ts'),
|
|
path.join(repoRoot, 'core/mcp/remote/types.ts'),
|
|
// .d.ts files are declaration-only.
|
|
path.join(repoRoot, '**/*.d.ts'),
|
|
// inspectorClientEventTarget.ts is types + a single empty-body class
|
|
// (extends TypedEventTarget). v8/istanbul records 0 statements for it
|
|
// today. TODO(#1243): drop this exclusion once the class gains real
|
|
// behavior as the v1.5 InspectorClient port progresses.
|
|
path.join(repoRoot, 'core/mcp/inspectorClientEventTarget.ts'),
|
|
path.join(repoRoot, 'core/mcp/__tests__/**'),
|
|
// test-servers/ is test infrastructure (composable MCP servers and
|
|
// fixtures), not application code — its build output also lives at
|
|
// test-servers/build/, which we don't want to measure either.
|
|
path.join(repoRoot, 'test-servers/**'),
|
|
],
|
|
thresholds: {
|
|
perFile: true,
|
|
lines: 90,
|
|
statements: 85,
|
|
functions: 80,
|
|
branches: 50,
|
|
},
|
|
},
|
|
projects: [
|
|
{
|
|
extends: true,
|
|
// Vitest projects don't inherit `resolve` from the parent. The unit
|
|
// project runs from repoRoot (so vitest's coverage transformer can
|
|
// reach core/), but repoRoot has no node_modules of its own — the
|
|
// shared regex aliases redirect bare `react`/`pino`/etc. imports
|
|
// from core/ back into clients/web/node_modules.
|
|
resolve: projectResolve,
|
|
test: {
|
|
name: 'unit',
|
|
environment: 'happy-dom',
|
|
// Don't let happy-dom actually navigate child frames. Components like
|
|
// the MCP Apps sandbox render an <iframe src="/sandbox.html">; with
|
|
// navigation enabled happy-dom fetches that URL (and unloads it on
|
|
// teardown), which fails under the test server and floods the run with
|
|
// alarming-but-expected `DOMException [NetworkError/AbortError]` and
|
|
// `AsyncTaskManager destroyed` output. The component tests only assert
|
|
// on the iframe element/attributes, not its loaded document, so
|
|
// disabling frame navigation removes the noise without losing coverage.
|
|
environmentOptions: {
|
|
happyDOM: {
|
|
settings: { navigation: { disableChildFrameNavigation: true } },
|
|
},
|
|
},
|
|
// Root the unit project at the repo root so vitest's coverage
|
|
// transformer (which only processes files inside a project root)
|
|
// can reach core/ modules. Without this, untested core/ files fall
|
|
// back to raw-TS parsing in rolldown, which can't handle TS-only
|
|
// syntax (e.g. `import type`) — silently dropping them and bypassing
|
|
// the per-file gate.
|
|
root: repoRoot,
|
|
// No `globals: true` — every test file imports `describe`, `it`,
|
|
// `expect`, `vi` explicitly from "vitest". This keeps the pattern
|
|
// consistent and avoids relying on auto-cleanup tied to Vitest's
|
|
// global lifecycle hooks; cleanup is invoked manually in setup.ts.
|
|
include: ['clients/web/src/**/*.test.{ts,tsx}'],
|
|
// Integration tests run in the integration project below (node env).
|
|
exclude: [integrationGlob],
|
|
setupFiles: [path.join(dirname, 'src/test/setup.ts')],
|
|
},
|
|
},
|
|
{
|
|
extends: true,
|
|
// See note on the unit project: integration tests also run from
|
|
// repoRoot and import core/ modules, so they need the same alias
|
|
// setup. The shared bare-module aliases keep `pino`, `hono`, etc.
|
|
// resolving against clients/web/node_modules.
|
|
resolve: projectResolve,
|
|
test: {
|
|
name: 'integration',
|
|
environment: 'node',
|
|
// Same reason as the unit project: rooted at repoRoot so vitest
|
|
// can transform core/ modules and run tests against the source.
|
|
root: repoRoot,
|
|
include: [integrationGlob],
|
|
// Integration tests spawn real HTTP/stdio servers via test-servers/,
|
|
// bind sockets, run e2e OAuth flows, and exercise filesystem-backed
|
|
// storage. 30s matches the v1.5 core/vitest.config.ts.
|
|
testTimeout: 30000,
|
|
hookTimeout: 30000,
|
|
// Inline the MCP SDK so vi.mock("@modelcontextprotocol/sdk/...")
|
|
// hooks the same transformed copy that source files import.
|
|
// Externalized node_modules are loaded via Node's loader and bypass
|
|
// Vitest's mock system. The explicit alias above pins the auth.js
|
|
// subpath to one canonical ID; inlining ensures the transformer
|
|
// pipeline owns the module rather than the Node loader.
|
|
server: {
|
|
deps: {
|
|
inline: [/@modelcontextprotocol\/sdk/],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
extends: true,
|
|
plugins: [
|
|
// The plugin will run tests for the stories defined in your Storybook config
|
|
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
|
|
storybookTest({
|
|
configDir: path.join(dirname, '.storybook'),
|
|
}),
|
|
],
|
|
test: {
|
|
name: 'storybook',
|
|
browser: {
|
|
enabled: true,
|
|
headless: true,
|
|
provider: playwright({}),
|
|
instances: [
|
|
{
|
|
browser: 'chromium',
|
|
},
|
|
],
|
|
},
|
|
setupFiles: ['.storybook/vitest.setup.ts'],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|