diff --git a/CLAUDE.md b/CLAUDE.md index 0a5e65b8b..3747e91ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,9 +2,9 @@ > nice web things for the tired -`@fuzdev/zzz` — local-first AI forge: chat + files + prompts in one app. -SvelteKit frontend, Hono/Node.js backend, Svelte 5 runes, Zod schemas. -v0.0.1, no auth, no database yet. 26 cell classes, 20 action specs, 4 AI providers. +`@fuzdev/zzz` — local-first AI forge: chat + files + prompts + terminals in one app. +SvelteKit frontend, Hono/Deno backend, Svelte 5 runes, Zod schemas. +v0.0.1, no auth, no database yet. 28 cell classes, 25 action specs, 4 AI providers. For coding conventions, see [`fuz-stack`](../fuz-stack/CLAUDE.md). @@ -14,7 +14,8 @@ For coding conventions, see [`fuz-stack`](../fuz-stack/CLAUDE.md). 2. **Edit files** on disk — scoped filesystem, syntax highlighting, multi-tab editor 3. **Build prompts** — reusable content templates composed from text parts and file references 4. **Manage models** — Ollama local models + Claude/ChatGPT/Gemini via BYOK API keys -5. **Symmetric actions** — JSON-RPC 2.0 between frontend and backend, same ActionPeer on both sides +5. **Run terminals** — interactive PTY terminals via xterm.js with preset commands, contextmenu copy, and restart +6. **Symmetric actions** — JSON-RPC 2.0 between frontend and backend, same ActionPeer on both sides ## Key Principles @@ -25,34 +26,60 @@ For coding conventions, see [`fuz-stack`](../fuz-stack/CLAUDE.md). ## Development Stage -Early development, v0.0.1. Breaking changes are expected and welcome. No authentication — development use only. All state is in-memory (no database yet). The Hono/Node.js backend is a reference implementation that may be replaced by a Rust daemon (`fuzd`). +Early development, v0.0.1. Breaking changes are expected and welcome. No authentication — development use only. All state is in-memory (no database yet). The Hono/Deno backend is a reference implementation that may be replaced by a Rust daemon (`fuzd`). Deno is a shortcut — long-term the CLI and daemon migrate to Rust fuz/fuzd. See [GitHub issues](https://github.com/fuzdev/zzz/issues) for planned work. +## CLI + +zzz has a Deno-compiled CLI binary for daemon management and browser launching. +See [src/lib/zzz/CLAUDE.md](src/lib/zzz/CLAUDE.md) for full CLI architecture. + +```bash +zzz # start daemon if needed, open browser +zzz ~/dev/ # open workspace at ~/dev/ +zzz daemon start # start daemon (foreground) +zzz daemon status # show daemon info +zzz init # initialize ~/.zzz/ +``` + +The global daemon runs on port 4460 with state at `~/.zzz/`. Built via +`gro_plugin_deno_compile` (see `gro.config.ts` and `deno.json`). + ## Docs - [docs/architecture.md](docs/architecture.md) — Action system, Cell system, content model, data flow - [docs/development.md](docs/development.md) — Development workflow, extension points, patterns - [docs/providers.md](docs/providers.md) — AI provider integration, adding new providers - [src/lib/server/CLAUDE.md](src/lib/server/CLAUDE.md) — Backend server architecture, providers, security +- [src/lib/zzz/CLAUDE.md](src/lib/zzz/CLAUDE.md) — CLI architecture, commands, runtime abstraction ## Repository Structure ``` src/ ├── lib/ # Published as @fuzdev/zzz -│ ├── server/ # Backend (Hono/Node.js reference impl) +│ ├── server/ # Backend (Hono/Deno reference impl) │ │ ├── backend.ts -│ │ ├── server.ts +│ │ ├── server.ts # Deno server entry (dev + production) │ │ ├── backend_action_handlers.ts │ │ ├── backend_provider_*.ts # Ollama, Claude, ChatGPT, Gemini +│ │ ├── pty_ffi.ts # Deno FFI bindings for libfuz_pty.so +│ │ ├── backend_pty_manager.ts # PTY process management (FFI or fallback) │ │ ├── scoped_fs.ts │ │ ├── security.ts │ │ └── backend_action_types.gen.ts │ │ -│ ├── *.svelte.ts # Cell state classes (26 classes) -│ ├── action_specs.ts # All 20 action spec definitions -│ ├── action_spec.ts # ActionSpec schema +│ ├── zzz/ # CLI (Deno compiled binary) +│ │ ├── main.ts # Entry point (deno compile target) +│ │ ├── cli.ts # Arg parsing wrapper +│ │ ├── cli_config.ts # ~/.zzz/config.json +│ │ ├── runtime/ # ZzzRuntime abstraction +│ │ ├── cli/ # CLI infrastructure +│ │ └── commands/ # init, daemon, open, status +│ │ +│ ├── *.svelte.ts # Cell state classes (28 classes) +│ ├── action_specs.ts # All 25 action spec definitions │ ├── action_event.ts # Action lifecycle state machine │ ├── action_peer.ts # Symmetric send/receive │ ├── cell.svelte.ts # Base Cell class @@ -64,7 +91,7 @@ src/ │ ├── frontend_action_types.gen.ts │ └── action_metatypes.gen.ts │ -├── routes/ # SvelteKit routes (16 dirs) +├── routes/ # SvelteKit routes (17 dirs) │ ├── about/ │ ├── actions/ │ ├── bots/ @@ -80,6 +107,7 @@ src/ │ ├── repos/ │ ├── settings/ │ ├── tabs/ +│ ├── terminals/ │ └── views/ │ ├── test/ # Tests (not co-located) @@ -101,7 +129,7 @@ See [docs/architecture.md](docs/architecture.md) for detailed data flow, content ## Cell Classes -26 registered classes in `src/lib/cell_classes.ts`: +28 registered classes in `src/lib/cell_classes.ts`: | Class | Source file | Purpose | | ---------------- | ------------------------------ | ------------------------------------ | @@ -129,12 +157,14 @@ See [docs/architecture.md](docs/architecture.md) for detailed data flow, content | `Turn` | `turn.svelte.ts` | Single conversation message | | `Thread` | `thread.svelte.ts` | Linear conversation with one model | | `Threads` | `threads.svelte.ts` | Collection of threads | +| `Terminal` | `terminal.svelte.ts` | PTY terminal process state | +| `TerminalPreset` | `terminal_preset.svelte.ts` | Saved terminal command config | | `Time` | `time.svelte.ts` | Reactive time state | | `Ui` | `ui.svelte.ts` | UI state (menus, layout) | ## Action Specs -20 specs in `src/lib/action_specs.ts`: +25 specs in `src/lib/action_specs.ts`: | Method | Kind | Initiator | Purpose | | ------------------------ | --------------------- | ---------- | -------------------------------- | @@ -158,6 +188,11 @@ See [docs/architecture.md](docs/architecture.md) for detailed data flow, content | `ollama_unload` | `request_response` | `frontend` | Unload Ollama model from memory | | `provider_load_status` | `request_response` | `frontend` | Check provider availability | | `provider_update_api_key`| `request_response` | `frontend` | Update provider API key | +| `terminal_create` | `request_response` | `frontend` | Spawn PTY terminal process | +| `terminal_data_send` | `request_response` | `frontend` | Send stdin to terminal | +| `terminal_data` | `remote_notification` | `backend` | Stream stdout/stderr to frontend | +| `terminal_resize` | `request_response` | `frontend` | Update PTY dimensions | +| `terminal_close` | `request_response` | `frontend` | Kill terminal process | ## Development Workflow @@ -166,6 +201,10 @@ See [docs/architecture.md](docs/architecture.md) for detailed data flow, content ```bash cp src/lib/server/.env.development.example .env.development npm install + +# Optional: build fuz_pty for real PTY support (echo, prompts, colors, resize) +# Without this, terminals fall back to Deno.Command pipes (no interactivity) +cd ~/dev/private_fuz && cargo build -p fuz_pty --release ``` ### Daily Commands @@ -233,6 +272,7 @@ Each action is a plain object with Zod schemas for input/output: ```typescript export const diskfile_update_action_spec = { method: 'diskfile_update', + description: 'Write content to a file on disk', kind: 'request_response', initiator: 'frontend', auth: 'public', @@ -254,6 +294,71 @@ Action kinds: | `remote_notification` | WebSocket only | Backend pushes to frontend | | `local_call` | None (in-process) | Frontend-only | +### Adding an Action (End-to-End) + +Adding a new action touches up to 6 files. Here's the full workflow: + +**1. Define the spec** in `src/lib/action_specs.ts`: + +```typescript +export const my_action_spec = { + method: 'my_action', + kind: 'request_response', // or 'remote_notification', 'local_call' + initiator: 'frontend', // or 'backend', 'both' + auth: 'public', + side_effects: true, // or null for read-only + input: z.strictObject({ foo: z.string() }), + output: z.strictObject({ bar: z.number() }), + async: true, + description: 'What this action does.', +} satisfies ActionSpecUnion; +``` + +Add it to the `all_action_specs` array at the bottom of the file. + +**2. Run `gro gen`** — regenerates 4 files: +- `action_collections.ts` — `ActionInputs`/`ActionOutputs` type maps +- `action_metatypes.ts` — `ActionMethod` union, `ActionsApi` interface +- `frontend_action_types.ts` — `FrontendActionHandlers` type +- `server/backend_action_types.ts` — `BackendActionHandlers` type + +**3. Add backend handler** in `src/lib/server/backend_action_handlers.ts`: + +```typescript +my_action: { + receive_request: async ({backend, data: {input}}) => { + // input is typed from the spec's input schema + return {bar: 42}; // must match spec's output schema + }, +}, +``` + +**4. Add frontend handler** in `src/lib/frontend_action_handlers.ts`: + +```typescript +my_action: { + // For request_response: + receive_response: ({app, data: {output}}) => { /* handle success */ }, + receive_error: ({data: {error}}) => { /* handle error */ }, + // For remote_notification: + receive: ({app, data: {input}}) => { /* handle notification */ }, +}, +``` + +**5. Call from frontend** via `app.api`: + +```typescript +// Returns Result<{value: OutputType}, {error: JsonrpcError}> +const result = await app.api.my_action({foo: 'hello'}); +if (result.ok) { + console.log(result.value.bar); // 42 +} +``` + +**6. For `remote_notification` actions**, also add to `BackendActionsApi` +in `src/lib/server/backend_actions_api.ts` — follow the `terminal_data` +or `completion_progress` pattern. + ### Zod Schema Conventions - Always use `z.strictObject()` (not `z.object()`) for action specs — unknown keys are rejected @@ -285,7 +390,7 @@ The `.zzz/` directory stores app data. Configured via `PUBLIC_ZZZ_DIR`. | ------------ | -------------------------------------- | | `state/` | Persistent data (completions logs) | | `cache/` | Regenerable data, safe to delete | -| `run/` | Runtime ephemeral (server.json: PID, port) | +| `run/` | Runtime ephemeral (daemon.json: PID, port) | All filesystem access goes through `ScopedFs` — path validation, no symlinks, absolute paths only. @@ -325,13 +430,21 @@ From `src/lib/server/.env.development.example`: - **No authentication** — development use only, anyone with network access can use it - **No database** — all state is in-memory, lost on restart (pglite planned) - **No undo/history** — file edits are permanent -- **No terminal integration** — no shell access from the UI +- **PTY via FFI** — real PTY support via `fuz_pty` Rust crate loaded through Deno FFI (`forkpty()`). Requires `cargo build -p fuz_pty --release` in `~/dev/private_fuz/`. For bundled binaries, place `libfuz_pty.so` next to the `zzz` executable. Falls back to `Deno.Command` pipes (no echo, no prompt) if `.so` not found - **No git integration** — no commit/push/pull from the UI - **No MCP/A2A** — protocol support planned but not implemented - **Backend is reference impl** — may be replaced by Rust daemon (`fuzd`) ## fuz_app -zzz is the primary source for the Cell and Action patterns that will become the `fuz_app` package — a shared foundation for Fuz ecosystem apps. +zzz is the reference implementation for Cell and Action patterns. ActionSpec +types have been extracted to `@fuzdev/fuz_app` — zzz imports them from +`@fuzdev/fuz_app/actions/action_spec.js` and `@fuzdev/fuz_app/actions/action_registry.js`. +Cell patterns and the full SAES runtime (ActionEvent, ActionPeer, transports) +remain in zzz until a second consumer needs them (DA-5). + +The CLI and daemon lifecycle use `@fuzdev/fuz_app/cli/*` helpers: `DaemonInfo` +schema, `write_daemon_info`, `read_daemon_info`, `is_daemon_running`, +`stop_daemon`. The server writes `~/.zzz/run/daemon.json` (not `server.json`). -Last updated: 2026-02-10 +Last updated: 2026-03-16 diff --git a/README.md b/README.md index af323aee3..e05cf4984 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Zzz builds on a great deal of software. - I started using [Claude](https://claude.ai/) in late 2024 after making the initial prototype, and in late 2025 I started doing much of the coding with Claude Code, Opus 4.5 being the first over some threshold for me for this project - - see `⚠️ AI generated` and similar disclaimers + - see `NOTE: AI-generated` and similar disclaimers ## License 🐦 diff --git a/deno.json b/deno.json new file mode 100644 index 000000000..68f62fd50 --- /dev/null +++ b/deno.json @@ -0,0 +1,31 @@ +{ + "unstable": ["sloppy-imports"], + "exclude": ["**/*.test.ts", "**/*.svelte.ts", "**/*.gen.ts", "src/test/"], + "tasks": { + "dev:start": "NODE_ENV=development deno run --allow-all src/lib/zzz/main.ts daemon start", + "install": "gro build && mkdir -p ~/.zzz/bin && cp dist_cli/zzz ~/.zzz/bin/zzz", + "check": "deno check src/lib/zzz/**/*.ts" + }, + "imports": { + "@std/": "jsr:@std/", + "esm-env": "npm:esm-env@^1", + "hono": "npm:hono@^4", + "svelte": "npm:svelte@^5", + "zod": "npm:zod@^4", + "@electric-sql/pglite": "npm:@electric-sql/pglite@^0.3", + "@fuzdev/blake3_wasm": "npm:@fuzdev/blake3_wasm@^0.1.0", + "@fuzdev/fuz_app/": "../fuz_app/src/lib/", + "@fuzdev/fuz_util/": "npm:/@fuzdev/fuz_util@^0.53.4/", + "@fuzdev/gro/": "npm:/@fuzdev/gro@^0.197.0/", + "ollama": "npm:ollama@^0.6", + "@anthropic-ai/sdk": "npm:@anthropic-ai/sdk@^0.71.2", + "openai": "npm:openai@^6.10.0", + "@google/generative-ai": "npm:@google/generative-ai@^0.24.1" + }, + "fmt": { + "useTabs": true, + "lineWidth": 100, + "indentWidth": 2, + "singleQuote": true + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 000000000..e35148c4f --- /dev/null +++ b/deno.lock @@ -0,0 +1,2222 @@ +{ + "version": "5", + "specifiers": { + "npm:@anthropic-ai/sdk@~0.71.2": "0.71.2_zod@4.3.6", + "npm:@changesets/changelog-git@~0.2.1": "0.2.1", + "npm:@electric-sql/pglite@0.3": "0.3.16", + "npm:@fuzdev/fuz_code@~0.45.1": "0.45.1_@fuzdev+fuz_css@0.56.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@fuzdev+gro@0.197.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___esbuild@0.27.4___svelte@5.53.12___typescript@5.9.3___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+acorn-typescript@1.0.9___acorn@8.16.0__@webref+css@8.4.1___css-tree@3.2.1__zimmerframe@1.1.4__zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__acorn@8.16.0__css-tree@3.2.1__esbuild@0.27.4__esm-env@1.2.2__jsdom@27.4.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_esm-env@1.2.2_magic-string@0.30.21_svelte@5.53.12_zimmerframe@1.1.4_@fuzdev+blake3_wasm@0.1.0_@fuzdev+gro@0.197.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__esbuild@0.27.4__svelte@5.53.12__typescript@5.9.3__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+acorn-typescript@1.0.9__acorn@8.16.0_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_@webref+css@8.4.1__css-tree@3.2.1_acorn@8.16.0_css-tree@3.2.1_esbuild@0.27.4_jsdom@27.4.0_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_zod@4.3.6", + "npm:@fuzdev/fuz_css@0.56": "0.56.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@fuzdev+gro@0.197.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__esbuild@0.27.4__svelte@5.53.12__typescript@5.9.3__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+acorn-typescript@1.0.9__acorn@8.16.0_@webref+css@8.4.1__css-tree@3.2.1_zimmerframe@1.1.4_zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_acorn@8.16.0_css-tree@3.2.1_esbuild@0.27.4_esm-env@1.2.2_jsdom@27.4.0_svelte@5.53.12_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0", + "npm:@fuzdev/fuz_ui@0.190": "0.190.0_@fuzdev+fuz_code@0.45.1__@fuzdev+fuz_css@0.56.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@fuzdev+gro@0.197.0____@fuzdev+blake3_wasm@0.1.0____@fuzdev+fuz_util@0.54.0_____@fuzdev+blake3_wasm@0.1.0_____@types+estree@1.0.8_____@types+node@24.12.0_____esm-env@1.2.2_____svelte@5.53.12_____zod@4.3.6____@sveltejs+kit@2.55.0_____@sveltejs+vite-plugin-svelte@6.2.4______svelte@5.53.12______vite@7.3.1_______@types+node@24.12.0______@types+node@24.12.0_____svelte@5.53.12_____typescript@5.9.3_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____esbuild@0.27.4____svelte@5.53.12____typescript@5.9.3____vitest@4.1.0_____@types+node@24.12.0_____jsdom@27.4.0_____vite@7.3.1______@types+node@24.12.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____@types+estree@1.0.8____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+acorn-typescript@1.0.9____acorn@8.16.0___@webref+css@8.4.1____css-tree@3.2.1___zimmerframe@1.1.4___zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___acorn@8.16.0___css-tree@3.2.1___esbuild@0.27.4___esm-env@1.2.2___jsdom@27.4.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__esm-env@1.2.2__magic-string@0.30.21__svelte@5.53.12__zimmerframe@1.1.4__@fuzdev+blake3_wasm@0.1.0__@fuzdev+gro@0.197.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___esbuild@0.27.4___svelte@5.53.12___typescript@5.9.3___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+acorn-typescript@1.0.9___acorn@8.16.0__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__@webref+css@8.4.1___css-tree@3.2.1__acorn@8.16.0__css-tree@3.2.1__esbuild@0.27.4__jsdom@27.4.0__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__zod@4.3.6_@fuzdev+fuz_css@0.56.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@fuzdev+gro@0.197.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___esbuild@0.27.4___svelte@5.53.12___typescript@5.9.3___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+acorn-typescript@1.0.9___acorn@8.16.0__@webref+css@8.4.1___css-tree@3.2.1__zimmerframe@1.1.4__zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__acorn@8.16.0__css-tree@3.2.1__esbuild@0.27.4__esm-env@1.2.2__jsdom@27.4.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@fuzdev+gro@0.197.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__esbuild@0.27.4__svelte@5.53.12__typescript@5.9.3__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@jridgewell+trace-mapping@0.3.31_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_esm-env@1.2.2_svelte@5.53.12_svelte2tsx@0.7.52__svelte@5.53.12__typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_zod@4.3.6_@fuzdev+blake3_wasm@0.1.0_@sveltejs+acorn-typescript@1.0.9__acorn@8.16.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+node@24.12.0_@webref+css@8.4.1__css-tree@3.2.1_acorn@8.16.0_css-tree@3.2.1_esbuild@0.27.4_jsdom@27.4.0_magic-string@0.30.21_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_zimmerframe@1.1.4", + "npm:@fuzdev/fuz_util@0.54": "0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "npm:@fuzdev/fuz_util@~0.53.4": "0.53.4_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "npm:@fuzdev/gro@0.197": "0.197.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0", + "npm:@google/generative-ai@~0.24.1": "0.24.1", + "npm:@jridgewell/trace-mapping@~0.3.31": "0.3.31", + "npm:@ryanatkn/eslint-config@~0.10.1": "0.10.1_@eslint+js@9.39.4_eslint@9.39.4_eslint-plugin-svelte@3.15.2__eslint@9.39.4__svelte@5.53.12_svelte@5.53.12_typescript@5.9.3_typescript-eslint@8.57.1__eslint@9.39.4__typescript@5.9.3", + "npm:@sveltejs/acorn-typescript@^1.0.9": "1.0.9_acorn@8.16.0", + "npm:@sveltejs/adapter-static@^3.0.10": "3.0.10_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+node@24.12.0_svelte@5.53.12_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0", + "npm:@sveltejs/kit@^2.55.0": "2.55.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_svelte@5.53.12_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0", + "npm:@sveltejs/vite-plugin-svelte@^6.2.4": "6.2.4_svelte@5.53.12_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0", + "npm:@types/deno@^2.5.0": "2.5.0", + "npm:@types/estree@^1.0.8": "1.0.8", + "npm:@types/node@^24.10.1": "24.12.0", + "npm:@webref/css@^8.2.0": "8.4.1_css-tree@3.2.1", + "npm:@xterm/xterm@6": "6.0.0", + "npm:date-fns@^4.1.0": "4.1.0", + "npm:eslint-plugin-svelte@^3.13.1": "3.15.2_eslint@9.39.4_svelte@5.53.12", + "npm:eslint@^9.39.1": "9.39.4", + "npm:esm-env@1": "1.2.2", + "npm:esm-env@^1.2.2": "1.2.2", + "npm:hono@4": "4.12.8", + "npm:hono@^4.12.7": "4.12.8", + "npm:jsdom@^27.2.0": "27.4.0", + "npm:magic-string@~0.30.21": "0.30.21", + "npm:ollama@0.6": "0.6.3", + "npm:ollama@~0.6.3": "0.6.3", + "npm:openai@^6.10.0": "6.29.0_zod@4.3.6", + "npm:prettier-plugin-svelte@^3.4.1": "3.5.1_prettier@3.8.1_svelte@5.53.12", + "npm:prettier@^3.7.4": "3.8.1", + "npm:svelte-check@^4.3.5": "4.4.5_svelte@5.53.12_typescript@5.9.3", + "npm:svelte2tsx@~0.7.52": "0.7.52_svelte@5.53.12_typescript@5.9.3", + "npm:svelte@5": "5.53.12", + "npm:svelte@^5.53.12": "5.53.12", + "npm:tslib@^2.8.1": "2.8.1", + "npm:typescript-eslint@^8.48.1": "8.57.1_eslint@9.39.4_typescript@5.9.3", + "npm:typescript@^5.9.3": "5.9.3", + "npm:vite@^7.3.1": "7.3.1_@types+node@24.12.0", + "npm:vitest@^4.0.15": "4.1.0_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0", + "npm:zimmerframe@^1.1.4": "1.1.4", + "npm:zod@4": "4.3.6", + "npm:zod@^4.3.6": "4.3.6" + }, + "npm": { + "@acemir/cssom@0.9.31": { + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==" + }, + "@anthropic-ai/sdk@0.71.2_zod@4.3.6": { + "integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==", + "dependencies": [ + "json-schema-to-ts", + "zod" + ], + "optionalPeers": [ + "zod" + ], + "bin": true + }, + "@asamuzakjp/css-color@4.1.2": { + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dependencies": [ + "@csstools/css-calc", + "@csstools/css-color-parser", + "@csstools/css-parser-algorithms", + "@csstools/css-tokenizer", + "lru-cache" + ] + }, + "@asamuzakjp/dom-selector@6.8.1": { + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dependencies": [ + "@asamuzakjp/nwsapi", + "bidi-js", + "css-tree", + "is-potential-custom-element-name", + "lru-cache" + ] + }, + "@asamuzakjp/nwsapi@2.3.9": { + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==" + }, + "@babel/runtime@7.28.6": { + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==" + }, + "@changesets/changelog-git@0.2.1": { + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "dependencies": [ + "@changesets/types" + ] + }, + "@changesets/types@6.1.0": { + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==" + }, + "@csstools/color-helpers@6.0.2": { + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==" + }, + "@csstools/css-calc@3.1.1_@csstools+css-parser-algorithms@4.0.0__@csstools+css-tokenizer@4.0.0_@csstools+css-tokenizer@4.0.0": { + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dependencies": [ + "@csstools/css-parser-algorithms", + "@csstools/css-tokenizer" + ] + }, + "@csstools/css-color-parser@4.0.2_@csstools+css-parser-algorithms@4.0.0__@csstools+css-tokenizer@4.0.0_@csstools+css-tokenizer@4.0.0": { + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dependencies": [ + "@csstools/color-helpers", + "@csstools/css-calc", + "@csstools/css-parser-algorithms", + "@csstools/css-tokenizer" + ] + }, + "@csstools/css-parser-algorithms@4.0.0_@csstools+css-tokenizer@4.0.0": { + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dependencies": [ + "@csstools/css-tokenizer" + ] + }, + "@csstools/css-syntax-patches-for-csstree@1.1.1_css-tree@3.2.1": { + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dependencies": [ + "css-tree" + ], + "optionalPeers": [ + "css-tree" + ] + }, + "@csstools/css-tokenizer@4.0.0": { + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==" + }, + "@electric-sql/pglite@0.3.16": { + "integrity": "sha512-mZkZfOd9OqTMHsK+1cje8OSzfAQcpD7JmILXTl5ahdempjUDdmg4euf1biDex5/LfQIDJ3gvCu6qDgdnDxfJmA==" + }, + "@emnapi/core@1.9.0": { + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dependencies": [ + "@emnapi/wasi-threads", + "tslib" + ] + }, + "@emnapi/runtime@1.9.0": { + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dependencies": [ + "tslib" + ] + }, + "@emnapi/wasi-threads@1.2.0": { + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dependencies": [ + "tslib" + ] + }, + "@esbuild/aix-ppc64@0.27.4": { + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.27.4": { + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.27.4": { + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.27.4": { + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.27.4": { + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.27.4": { + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.27.4": { + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.27.4": { + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.27.4": { + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.27.4": { + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.27.4": { + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.27.4": { + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.27.4": { + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.27.4": { + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.27.4": { + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.27.4": { + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.27.4": { + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.27.4": { + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.27.4": { + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.27.4": { + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.27.4": { + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openharmony-arm64@0.27.4": { + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.27.4": { + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.27.4": { + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.27.4": { + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.27.4": { + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@eslint-community/eslint-utils@4.9.1_eslint@9.39.4": { + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dependencies": [ + "eslint", + "eslint-visitor-keys@3.4.3" + ] + }, + "@eslint-community/regexpp@4.12.2": { + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==" + }, + "@eslint/config-array@0.21.2": { + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dependencies": [ + "@eslint/object-schema", + "debug", + "minimatch@3.1.5" + ] + }, + "@eslint/config-helpers@0.4.2": { + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dependencies": [ + "@eslint/core" + ] + }, + "@eslint/core@0.17.0": { + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dependencies": [ + "@types/json-schema" + ] + }, + "@eslint/eslintrc@3.3.5": { + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dependencies": [ + "ajv", + "debug", + "espree", + "globals@14.0.0", + "ignore@5.3.2", + "import-fresh", + "js-yaml", + "minimatch@3.1.5", + "strip-json-comments" + ] + }, + "@eslint/js@9.39.4": { + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==" + }, + "@eslint/object-schema@2.1.7": { + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==" + }, + "@eslint/plugin-kit@0.4.1": { + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dependencies": [ + "@eslint/core", + "levn" + ] + }, + "@exodus/bytes@1.15.0": { + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==" + }, + "@fuzdev/blake3_wasm@0.1.0": { + "integrity": "sha512-EU5uUcSX55Li3IXi1NiBDoVlxCN8ip9wqAhVZlMBEUa+cFQtLL6Z8GpYjlWy0KosLmxy2Z9WQv49PAkiAzFppg==" + }, + "@fuzdev/fuz_code@0.45.1_@fuzdev+fuz_css@0.56.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@fuzdev+gro@0.197.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___esbuild@0.27.4___svelte@5.53.12___typescript@5.9.3___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+acorn-typescript@1.0.9___acorn@8.16.0__@webref+css@8.4.1___css-tree@3.2.1__zimmerframe@1.1.4__zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__acorn@8.16.0__css-tree@3.2.1__esbuild@0.27.4__esm-env@1.2.2__jsdom@27.4.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_esm-env@1.2.2_magic-string@0.30.21_svelte@5.53.12_zimmerframe@1.1.4_@fuzdev+blake3_wasm@0.1.0_@fuzdev+gro@0.197.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__esbuild@0.27.4__svelte@5.53.12__typescript@5.9.3__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+acorn-typescript@1.0.9__acorn@8.16.0_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_@webref+css@8.4.1__css-tree@3.2.1_acorn@8.16.0_css-tree@3.2.1_esbuild@0.27.4_jsdom@27.4.0_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_zod@4.3.6": { + "integrity": "sha512-aVWWJHJ3U/bV9ZqooBuZ1XQrFgKdbSgRgs4NQOXDHl20JmmoR0jf7BkxQM/lxhtT/WU5kFJhiaGFYZCSmSgUuw==", + "dependencies": [ + "@fuzdev/fuz_css", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "esm-env", + "magic-string", + "svelte", + "zimmerframe" + ], + "optionalPeers": [ + "@fuzdev/fuz_css", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "magic-string", + "svelte", + "zimmerframe" + ] + }, + "@fuzdev/fuz_css@0.56.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@fuzdev+gro@0.197.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__esbuild@0.27.4__svelte@5.53.12__typescript@5.9.3__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+acorn-typescript@1.0.9__acorn@8.16.0_@webref+css@8.4.1__css-tree@3.2.1_zimmerframe@1.1.4_zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_acorn@8.16.0_css-tree@3.2.1_esbuild@0.27.4_esm-env@1.2.2_jsdom@27.4.0_svelte@5.53.12_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0": { + "integrity": "sha512-x5/Sx0/n+nQUJ1gnnAq51bNd0zlQGRGXFilnDat/5RYCpSD6Zr4W4u0s7F4IrZ9sxnyMA1wRkoi78OnUfsg/tw==", + "dependencies": [ + "@fuzdev/blake3_wasm", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "@fuzdev/gro@0.197.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0", + "@sveltejs/acorn-typescript", + "@webref/css", + "zimmerframe", + "zod" + ], + "optionalPeers": [ + "@fuzdev/blake3_wasm", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "@fuzdev/gro@0.197.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0", + "@sveltejs/acorn-typescript", + "@webref/css", + "zimmerframe", + "zod" + ] + }, + "@fuzdev/fuz_ui@0.190.0_@fuzdev+fuz_code@0.45.1__@fuzdev+fuz_css@0.56.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@fuzdev+gro@0.197.0____@fuzdev+blake3_wasm@0.1.0____@fuzdev+fuz_util@0.54.0_____@fuzdev+blake3_wasm@0.1.0_____@types+estree@1.0.8_____@types+node@24.12.0_____esm-env@1.2.2_____svelte@5.53.12_____zod@4.3.6____@sveltejs+kit@2.55.0_____@sveltejs+vite-plugin-svelte@6.2.4______svelte@5.53.12______vite@7.3.1_______@types+node@24.12.0______@types+node@24.12.0_____svelte@5.53.12_____typescript@5.9.3_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____esbuild@0.27.4____svelte@5.53.12____typescript@5.9.3____vitest@4.1.0_____@types+node@24.12.0_____jsdom@27.4.0_____vite@7.3.1______@types+node@24.12.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____@types+estree@1.0.8____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+acorn-typescript@1.0.9____acorn@8.16.0___@webref+css@8.4.1____css-tree@3.2.1___zimmerframe@1.1.4___zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___acorn@8.16.0___css-tree@3.2.1___esbuild@0.27.4___esm-env@1.2.2___jsdom@27.4.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__esm-env@1.2.2__magic-string@0.30.21__svelte@5.53.12__zimmerframe@1.1.4__@fuzdev+blake3_wasm@0.1.0__@fuzdev+gro@0.197.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___esbuild@0.27.4___svelte@5.53.12___typescript@5.9.3___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+acorn-typescript@1.0.9___acorn@8.16.0__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__@webref+css@8.4.1___css-tree@3.2.1__acorn@8.16.0__css-tree@3.2.1__esbuild@0.27.4__jsdom@27.4.0__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__zod@4.3.6_@fuzdev+fuz_css@0.56.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@fuzdev+gro@0.197.0___@fuzdev+blake3_wasm@0.1.0___@fuzdev+fuz_util@0.54.0____@fuzdev+blake3_wasm@0.1.0____@types+estree@1.0.8____@types+node@24.12.0____esm-env@1.2.2____svelte@5.53.12____zod@4.3.6___@sveltejs+kit@2.55.0____@sveltejs+vite-plugin-svelte@6.2.4_____svelte@5.53.12_____vite@7.3.1______@types+node@24.12.0_____@types+node@24.12.0____svelte@5.53.12____typescript@5.9.3____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___esbuild@0.27.4___svelte@5.53.12___typescript@5.9.3___vitest@4.1.0____@types+node@24.12.0____jsdom@27.4.0____vite@7.3.1_____@types+node@24.12.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___@types+estree@1.0.8___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+acorn-typescript@1.0.9___acorn@8.16.0__@webref+css@8.4.1___css-tree@3.2.1__zimmerframe@1.1.4__zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__acorn@8.16.0__css-tree@3.2.1__esbuild@0.27.4__esm-env@1.2.2__jsdom@27.4.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@fuzdev+gro@0.197.0__@fuzdev+blake3_wasm@0.1.0__@fuzdev+fuz_util@0.54.0___@fuzdev+blake3_wasm@0.1.0___@types+estree@1.0.8___@types+node@24.12.0___esm-env@1.2.2___svelte@5.53.12___zod@4.3.6__@sveltejs+kit@2.55.0___@sveltejs+vite-plugin-svelte@6.2.4____svelte@5.53.12____vite@7.3.1_____@types+node@24.12.0____@types+node@24.12.0___svelte@5.53.12___typescript@5.9.3___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__esbuild@0.27.4__svelte@5.53.12__typescript@5.9.3__vitest@4.1.0___@types+node@24.12.0___jsdom@27.4.0___vite@7.3.1____@types+node@24.12.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__@types+estree@1.0.8__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@jridgewell+trace-mapping@0.3.31_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_esm-env@1.2.2_svelte@5.53.12_svelte2tsx@0.7.52__svelte@5.53.12__typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_zod@4.3.6_@fuzdev+blake3_wasm@0.1.0_@sveltejs+acorn-typescript@1.0.9__acorn@8.16.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+node@24.12.0_@webref+css@8.4.1__css-tree@3.2.1_acorn@8.16.0_css-tree@3.2.1_esbuild@0.27.4_jsdom@27.4.0_magic-string@0.30.21_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_zimmerframe@1.1.4": { + "integrity": "sha512-JKqV13xciHIU3X7WxoUhWVdKNMHTsyeSQMKoP/EgOYgXiaYooqchvZbJTDBWUCYiRWRMSGb1oj/LetTq/lxV9Q==", + "dependencies": [ + "@fuzdev/fuz_code", + "@fuzdev/fuz_css", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "@fuzdev/gro@0.197.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0", + "@jridgewell/trace-mapping", + "@sveltejs/kit", + "@types/estree", + "esm-env", + "svelte", + "svelte2tsx", + "vite", + "zod" + ], + "optionalPeers": [ + "@fuzdev/fuz_code", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "@fuzdev/gro@0.197.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0", + "@jridgewell/trace-mapping", + "@types/estree", + "esm-env", + "svelte2tsx", + "vite", + "zod" + ] + }, + "@fuzdev/fuz_util@0.53.4_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6": { + "integrity": "sha512-pkb0vsEviCnVv3Oc9ZPYUkkRxtg6CInFvm4S3AcXTdJv6T8efzwkQh7pqPAEVxVT3ZM1Tse2Ej7MEbxK477j2Q==", + "dependencies": [ + "@fuzdev/blake3_wasm", + "@types/estree", + "@types/node", + "esm-env", + "svelte", + "zod" + ], + "optionalPeers": [ + "@fuzdev/blake3_wasm", + "@types/estree", + "@types/node", + "esm-env", + "svelte", + "zod" + ] + }, + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6": { + "integrity": "sha512-FTO9g0p6hVb0UoqRkxbnnUvxA6MKeMl7wVlfyvluC2p9y0QVwa/c8mLmgEHWlauDF3pUuhznXcovVJj+QElZLA==", + "dependencies": [ + "@fuzdev/blake3_wasm", + "@types/estree", + "@types/node", + "esm-env", + "svelte", + "zod" + ], + "optionalPeers": [ + "@fuzdev/blake3_wasm", + "@types/estree", + "@types/node", + "esm-env", + "svelte", + "zod" + ] + }, + "@fuzdev/gro@0.195.2_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@fuzdev+blake3_wasm@0.1.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0": { + "integrity": "sha512-hxxu4M2xLzJbr8bfwVUq/7io9Yzb1woTvnm5w7YUO6yHB6wcoqcVNfq23lnSlZY/8zEC899dynfjyEHfcbZUwA==", + "dependencies": [ + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "@sveltejs/kit", + "chokidar@5.0.0", + "dotenv", + "esbuild", + "esm-env", + "oxc-parser", + "prettier", + "prettier-plugin-svelte", + "svelte", + "ts-blank-space", + "tslib", + "typescript", + "zod" + ], + "optionalDependencies": [ + "vitest" + ], + "optionalPeers": [ + "@sveltejs/kit", + "vitest" + ], + "bin": true + }, + "@fuzdev/gro@0.197.0_@fuzdev+blake3_wasm@0.1.0_@fuzdev+fuz_util@0.54.0__@fuzdev+blake3_wasm@0.1.0__@types+estree@1.0.8__@types+node@24.12.0__esm-env@1.2.2__svelte@5.53.12__zod@4.3.6_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_esbuild@0.27.4_svelte@5.53.12_typescript@5.9.3_vitest@4.1.0__@types+node@24.12.0__jsdom@27.4.0__vite@7.3.1___@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+estree@1.0.8_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0": { + "integrity": "sha512-8su6n1a7UCOJUWIwRPIr88hmLlP1lKwEdQJieP0T8lVLKdrP/wMA7TrkMUPOxxKzfKcONx6eWO6hrCNrPrVPKA==", + "dependencies": [ + "@fuzdev/blake3_wasm", + "@fuzdev/fuz_util@0.54.0_@fuzdev+blake3_wasm@0.1.0_@types+estree@1.0.8_@types+node@24.12.0_esm-env@1.2.2_svelte@5.53.12_zod@4.3.6", + "@sveltejs/kit", + "chokidar@5.0.0", + "dotenv", + "esbuild", + "esm-env", + "oxc-parser", + "prettier", + "prettier-plugin-svelte", + "svelte", + "ts-blank-space", + "tslib", + "typescript", + "zod" + ], + "optionalDependencies": [ + "vitest" + ], + "optionalPeers": [ + "@sveltejs/kit", + "vitest" + ], + "bin": true + }, + "@google/generative-ai@0.24.1": { + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==" + }, + "@humanfs/core@0.19.1": { + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" + }, + "@humanfs/node@0.16.7": { + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dependencies": [ + "@humanfs/core", + "@humanwhocodes/retry" + ] + }, + "@humanwhocodes/module-importer@1.0.1": { + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + }, + "@humanwhocodes/retry@0.4.3": { + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@napi-rs/wasm-runtime@1.1.1": { + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dependencies": [ + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util" + ] + }, + "@oxc-parser/binding-android-arm64@0.99.0": { + "integrity": "sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@oxc-parser/binding-darwin-arm64@0.99.0": { + "integrity": "sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@oxc-parser/binding-darwin-x64@0.99.0": { + "integrity": "sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@oxc-parser/binding-freebsd-x64@0.99.0": { + "integrity": "sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@oxc-parser/binding-linux-arm-gnueabihf@0.99.0": { + "integrity": "sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@oxc-parser/binding-linux-arm-musleabihf@0.99.0": { + "integrity": "sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@oxc-parser/binding-linux-arm64-gnu@0.99.0": { + "integrity": "sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@oxc-parser/binding-linux-arm64-musl@0.99.0": { + "integrity": "sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@oxc-parser/binding-linux-riscv64-gnu@0.99.0": { + "integrity": "sha512-zxn/xkf519f12FKkpL5XwJipsylfSSnm36h6c1zBDTz4fbIDMGyIhHfWfwM7uUmHo9Aqw1pLxFpY39Etv398+Q==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@oxc-parser/binding-linux-s390x-gnu@0.99.0": { + "integrity": "sha512-Y1eSDKDS5E4IVC7Oxw+NbYAKRmJPMJTIjW+9xOWwteDHkFqpocKe0USxog+Q1uhzalD9M0p9eXWEWdGQCMDBMQ==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@oxc-parser/binding-linux-x64-gnu@0.99.0": { + "integrity": "sha512-YVJMfk5cFWB8i2/nIrbk6n15bFkMHqWnMIWkVx7r2KwpTxHyFMfu2IpeVKo1ITDSmt5nBrGdLHD36QRlu2nDLg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@oxc-parser/binding-linux-x64-musl@0.99.0": { + "integrity": "sha512-2+SDPrie5f90A1b9EirtVggOgsqtsYU5raZwkDYKyS1uvJzjqHCDhG/f4TwQxHmIc5YkczdQfwvN91lwmjsKYQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@oxc-parser/binding-wasm32-wasi@0.99.0": { + "integrity": "sha512-DKA4j0QerUWSMADziLM5sAyM7V53Fj95CV9SjP77bPfEfT7MnvFKnneaRMqPK1cpzjAGiQF52OBUIKyk0dwOQA==", + "dependencies": [ + "@napi-rs/wasm-runtime" + ], + "cpu": ["wasm32"] + }, + "@oxc-parser/binding-win32-arm64-msvc@0.99.0": { + "integrity": "sha512-EaB3AvsxqdNUhh9FOoAxRZ2L4PCRwDlDb//QXItwyOJrX7XS+uGK9B1KEUV4FZ/7rDhHsWieLt5e07wl2Ti5AQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@oxc-parser/binding-win32-x64-msvc@0.99.0": { + "integrity": "sha512-sJN1Q8h7ggFOyDn0zsHaXbP/MklAVUvhrbq0LA46Qum686P3SZQHjbATqJn9yaVEvaSKXCshgl0vQ1gWkGgpcQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@oxc-project/types@0.99.0": { + "integrity": "sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==" + }, + "@polka/url@1.0.0-next.29": { + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==" + }, + "@rollup/rollup-android-arm-eabi@4.59.0": { + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "os": ["android"], + "cpu": ["arm"] + }, + "@rollup/rollup-android-arm64@4.59.0": { + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-arm64@4.59.0": { + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-x64@4.59.0": { + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rollup/rollup-freebsd-arm64@4.59.0": { + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@rollup/rollup-freebsd-x64@4.59.0": { + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-arm-gnueabihf@4.59.0": { + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm-musleabihf@4.59.0": { + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm64-gnu@4.59.0": { + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-arm64-musl@4.59.0": { + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-loong64-gnu@4.59.0": { + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-loong64-musl@4.59.0": { + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-ppc64-gnu@4.59.0": { + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-ppc64-musl@4.59.0": { + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-riscv64-gnu@4.59.0": { + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-riscv64-musl@4.59.0": { + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-s390x-gnu@4.59.0": { + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rollup/rollup-linux-x64-gnu@4.59.0": { + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-x64-musl@4.59.0": { + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-openbsd-x64@4.59.0": { + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-openharmony-arm64@4.59.0": { + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.59.0": { + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-ia32-msvc@4.59.0": { + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@rollup/rollup-win32-x64-gnu@4.59.0": { + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@rollup/rollup-win32-x64-msvc@4.59.0": { + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@ryanatkn/eslint-config@0.10.1_@eslint+js@9.39.4_eslint@9.39.4_eslint-plugin-svelte@3.15.2__eslint@9.39.4__svelte@5.53.12_svelte@5.53.12_typescript@5.9.3_typescript-eslint@8.57.1__eslint@9.39.4__typescript@5.9.3": { + "integrity": "sha512-fHQ5PyFriflVj/fiF9m4SoUnipyK/Of522HL3+YA5TD2lKdJueA5c4wxucxkuFanuZ1FvsCBjGN/wMHO94HNHA==", + "dependencies": [ + "@eslint/js", + "eslint", + "eslint-plugin-svelte", + "globals@16.5.0", + "svelte", + "typescript", + "typescript-eslint" + ] + }, + "@standard-schema/spec@1.1.0": { + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" + }, + "@sveltejs/acorn-typescript@1.0.9_acorn@8.16.0": { + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dependencies": [ + "acorn" + ] + }, + "@sveltejs/adapter-static@3.0.10_@sveltejs+kit@2.55.0__@sveltejs+vite-plugin-svelte@6.2.4___svelte@5.53.12___vite@7.3.1____@types+node@24.12.0___@types+node@24.12.0__svelte@5.53.12__typescript@5.9.3__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_@types+node@24.12.0_svelte@5.53.12_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0": { + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dependencies": [ + "@sveltejs/kit" + ] + }, + "@sveltejs/kit@2.55.0_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_svelte@5.53.12_typescript@5.9.3_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0": { + "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", + "dependencies": [ + "@standard-schema/spec", + "@sveltejs/acorn-typescript", + "@sveltejs/vite-plugin-svelte", + "@types/cookie", + "acorn", + "cookie", + "devalue", + "esm-env", + "kleur", + "magic-string", + "mrmime", + "set-cookie-parser", + "sirv", + "svelte", + "typescript", + "vite" + ], + "optionalPeers": [ + "typescript" + ], + "bin": true + }, + "@sveltejs/vite-plugin-svelte-inspector@5.0.2_@sveltejs+vite-plugin-svelte@6.2.4__svelte@5.53.12__vite@7.3.1___@types+node@24.12.0__@types+node@24.12.0_svelte@5.53.12_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0": { + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte", + "obug", + "svelte", + "vite" + ] + }, + "@sveltejs/vite-plugin-svelte@6.2.4_svelte@5.53.12_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0": { + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte-inspector", + "deepmerge", + "magic-string", + "obug", + "svelte", + "vite", + "vitefu" + ] + }, + "@tybys/wasm-util@0.10.1": { + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dependencies": [ + "tslib" + ] + }, + "@types/chai@5.2.3": { + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dependencies": [ + "@types/deep-eql", + "assertion-error" + ] + }, + "@types/cookie@0.6.0": { + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "@types/deep-eql@4.0.2": { + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" + }, + "@types/deno@2.5.0": { + "integrity": "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw==" + }, + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "@types/node@24.12.0": { + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dependencies": [ + "undici-types" + ] + }, + "@types/trusted-types@2.0.7": { + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "@typescript-eslint/eslint-plugin@8.57.1_@typescript-eslint+parser@8.57.1__eslint@9.39.4__typescript@5.9.3_eslint@9.39.4_typescript@5.9.3": { + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dependencies": [ + "@eslint-community/regexpp", + "@typescript-eslint/parser", + "@typescript-eslint/scope-manager", + "@typescript-eslint/type-utils", + "@typescript-eslint/utils", + "@typescript-eslint/visitor-keys", + "eslint", + "ignore@7.0.5", + "natural-compare", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/parser@8.57.1_eslint@9.39.4_typescript@5.9.3": { + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dependencies": [ + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/visitor-keys", + "debug", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/project-service@8.57.1_typescript@5.9.3": { + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dependencies": [ + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "debug", + "typescript" + ] + }, + "@typescript-eslint/scope-manager@8.57.1": { + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys" + ] + }, + "@typescript-eslint/tsconfig-utils@8.57.1_typescript@5.9.3": { + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dependencies": [ + "typescript" + ] + }, + "@typescript-eslint/type-utils@8.57.1_eslint@9.39.4_typescript@5.9.3": { + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/utils", + "debug", + "eslint", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/types@8.57.1": { + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==" + }, + "@typescript-eslint/typescript-estree@8.57.1_typescript@5.9.3": { + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dependencies": [ + "@typescript-eslint/project-service", + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys", + "debug", + "minimatch@10.2.4", + "semver", + "tinyglobby", + "ts-api-utils", + "typescript" + ] + }, + "@typescript-eslint/utils@8.57.1_eslint@9.39.4_typescript@5.9.3": { + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "eslint", + "typescript" + ] + }, + "@typescript-eslint/visitor-keys@8.57.1": { + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dependencies": [ + "@typescript-eslint/types", + "eslint-visitor-keys@5.0.1" + ] + }, + "@vitest/expect@4.1.0": { + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dependencies": [ + "@standard-schema/spec", + "@types/chai", + "@vitest/spy", + "@vitest/utils", + "chai", + "tinyrainbow" + ] + }, + "@vitest/mocker@4.1.0_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0": { + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dependencies": [ + "@vitest/spy", + "estree-walker", + "magic-string", + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "@vitest/pretty-format@4.1.0": { + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dependencies": [ + "tinyrainbow" + ] + }, + "@vitest/runner@4.1.0": { + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dependencies": [ + "@vitest/utils", + "pathe" + ] + }, + "@vitest/snapshot@4.1.0": { + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dependencies": [ + "@vitest/pretty-format", + "@vitest/utils", + "magic-string", + "pathe" + ] + }, + "@vitest/spy@4.1.0": { + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==" + }, + "@vitest/utils@4.1.0": { + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dependencies": [ + "@vitest/pretty-format", + "convert-source-map", + "tinyrainbow" + ] + }, + "@webref/css@8.4.1_css-tree@3.2.1": { + "integrity": "sha512-8DTncc0dhWJ4lVbi9rhLVyMNm+YEYrsFLRbdjgMxPupjNHcAdXiT1s4ZWJXzN4ckUvYQKTjLJKtZWc6tsR4FIQ==", + "dependencies": [ + "css-tree" + ] + }, + "@xterm/xterm@6.0.0": { + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==" + }, + "acorn-jsx@5.3.2_acorn@8.16.0": { + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dependencies": [ + "acorn" + ] + }, + "acorn@8.16.0": { + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "bin": true + }, + "agent-base@7.1.4": { + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" + }, + "ajv@6.14.0": { + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse", + "uri-js" + ] + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "aria-query@5.3.1": { + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==" + }, + "assertion-error@2.0.1": { + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==" + }, + "axobject-query@4.1.0": { + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" + }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "balanced-match@4.0.4": { + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + }, + "bidi-js@1.0.3": { + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dependencies": [ + "require-from-string" + ] + }, + "brace-expansion@1.1.12": { + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": [ + "balanced-match@1.0.2", + "concat-map" + ] + }, + "brace-expansion@5.0.4": { + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dependencies": [ + "balanced-match@4.0.4" + ] + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "chai@6.2.2": { + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==" + }, + "chalk@4.1.2": { + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": [ + "ansi-styles", + "supports-color" + ] + }, + "chokidar@4.0.3": { + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": [ + "readdirp@4.1.2" + ] + }, + "chokidar@5.0.0": { + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dependencies": [ + "readdirp@5.0.0" + ] + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "convert-source-map@2.0.0": { + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "cookie@0.6.0": { + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + "css-tree@3.2.1": { + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dependencies": [ + "mdn-data", + "source-map-js" + ] + }, + "cssesc@3.0.0": { + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": true + }, + "cssstyle@5.3.7": { + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dependencies": [ + "@asamuzakjp/css-color", + "@csstools/css-syntax-patches-for-csstree", + "css-tree", + "lru-cache" + ] + }, + "data-urls@6.0.1": { + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dependencies": [ + "whatwg-mimetype@5.0.0", + "whatwg-url" + ] + }, + "date-fns@4.1.0": { + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "decimal.js@10.6.0": { + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" + }, + "dedent-js@1.0.1": { + "integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==" + }, + "deep-is@0.1.4": { + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, + "devalue@5.6.4": { + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==" + }, + "dotenv@17.3.1": { + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==" + }, + "entities@6.0.1": { + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==" + }, + "es-module-lexer@2.0.0": { + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==" + }, + "esbuild@0.27.4": { + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "escape-string-regexp@4.0.0": { + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "eslint-plugin-svelte@3.15.2_eslint@9.39.4_svelte@5.53.12": { + "integrity": "sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@jridgewell/sourcemap-codec", + "eslint", + "esutils", + "globals@16.5.0", + "known-css-properties", + "postcss", + "postcss-load-config", + "postcss-safe-parser", + "semver", + "svelte", + "svelte-eslint-parser" + ], + "optionalPeers": [ + "svelte" + ] + }, + "eslint-scope@8.4.0": { + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dependencies": [ + "esrecurse", + "estraverse" + ] + }, + "eslint-visitor-keys@3.4.3": { + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + }, + "eslint-visitor-keys@4.2.1": { + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==" + }, + "eslint-visitor-keys@5.0.1": { + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==" + }, + "eslint@9.39.4": { + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/config-helpers", + "@eslint/core", + "@eslint/eslintrc", + "@eslint/js", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry", + "@types/estree", + "ajv", + "chalk", + "cross-spawn", + "debug", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up", + "glob-parent", + "ignore@5.3.2", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "lodash.merge", + "minimatch@3.1.5", + "natural-compare", + "optionator" + ], + "bin": true + }, + "esm-env@1.2.2": { + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" + }, + "espree@10.4.0": { + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dependencies": [ + "acorn", + "acorn-jsx", + "eslint-visitor-keys@4.2.1" + ] + }, + "esquery@1.7.0": { + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dependencies": [ + "estraverse" + ] + }, + "esrap@2.2.4": { + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@typescript-eslint/types" + ] + }, + "esrecurse@4.3.0": { + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": [ + "estraverse" + ] + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "estree-walker@3.0.3": { + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": [ + "@types/estree" + ] + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "expect-type@1.3.0": { + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify@2.1.0": { + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein@2.0.6": { + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "fdir@6.5.0_picomatch@4.0.3": { + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dependencies": [ + "picomatch" + ], + "optionalPeers": [ + "picomatch" + ] + }, + "file-entry-cache@8.0.0": { + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dependencies": [ + "flat-cache" + ] + }, + "find-up@5.0.0": { + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": [ + "locate-path", + "path-exists" + ] + }, + "flat-cache@4.0.1": { + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dependencies": [ + "flatted", + "keyv" + ] + }, + "flatted@3.4.1": { + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==" + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "glob-parent@6.0.2": { + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": [ + "is-glob" + ] + }, + "globals@14.0.0": { + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, + "globals@16.5.0": { + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==" + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "hono@4.12.8": { + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==" + }, + "html-encoding-sniffer@6.0.0": { + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dependencies": [ + "@exodus/bytes" + ] + }, + "http-proxy-agent@7.0.2": { + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": [ + "agent-base", + "debug" + ] + }, + "https-proxy-agent@7.0.6": { + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": [ + "agent-base", + "debug" + ] + }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, + "ignore@7.0.5": { + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==" + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "imurmurhash@0.1.4": { + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-potential-custom-element-name@1.0.1": { + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "is-reference@3.0.3": { + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dependencies": [ + "@types/estree" + ] + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "js-yaml@4.1.1": { + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, + "jsdom@27.4.0": { + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dependencies": [ + "@acemir/cssom", + "@asamuzakjp/dom-selector", + "@exodus/bytes", + "cssstyle", + "data-urls", + "decimal.js", + "html-encoding-sniffer", + "http-proxy-agent", + "https-proxy-agent", + "is-potential-custom-element-name", + "parse5", + "saxes", + "symbol-tree", + "tough-cookie", + "w3c-xmlserializer", + "webidl-conversions", + "whatwg-mimetype@4.0.0", + "whatwg-url", + "ws", + "xml-name-validator" + ] + }, + "json-buffer@3.0.1": { + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "json-schema-to-ts@3.1.1": { + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dependencies": [ + "@babel/runtime", + "ts-algebra" + ] + }, + "json-schema-traverse@0.4.1": { + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify@1.0.1": { + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "keyv@4.5.4": { + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": [ + "json-buffer" + ] + }, + "kleur@4.1.5": { + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + }, + "known-css-properties@0.37.0": { + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==" + }, + "levn@0.4.1": { + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, + "lilconfig@2.1.0": { + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + }, + "locate-character@3.0.0": { + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "locate-path@6.0.0": { + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": [ + "p-locate" + ] + }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lru-cache@11.2.7": { + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==" + }, + "magic-string@0.30.21": { + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "mdn-data@2.27.1": { + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==" + }, + "minimatch@10.2.4": { + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dependencies": [ + "brace-expansion@5.0.4" + ] + }, + "minimatch@3.1.5": { + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dependencies": [ + "brace-expansion@1.1.12" + ] + }, + "mri@1.2.0": { + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, + "mrmime@2.0.1": { + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "bin": true + }, + "natural-compare@1.4.0": { + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "obug@2.1.1": { + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==" + }, + "ollama@0.6.3": { + "integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==", + "dependencies": [ + "whatwg-fetch" + ] + }, + "openai@6.29.0_zod@4.3.6": { + "integrity": "sha512-YxoArl2BItucdO89/sN6edksV0x47WUTgkgVfCgX7EuEMhbirENsgYe5oO4LTjBL9PtdKtk2WqND1gSLcTd2yw==", + "dependencies": [ + "zod" + ], + "optionalPeers": [ + "zod" + ], + "bin": true + }, + "optionator@0.9.4": { + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "word-wrap" + ] + }, + "oxc-parser@0.99.0": { + "integrity": "sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ==", + "dependencies": [ + "@oxc-project/types" + ], + "optionalDependencies": [ + "@oxc-parser/binding-android-arm64", + "@oxc-parser/binding-darwin-arm64", + "@oxc-parser/binding-darwin-x64", + "@oxc-parser/binding-freebsd-x64", + "@oxc-parser/binding-linux-arm-gnueabihf", + "@oxc-parser/binding-linux-arm-musleabihf", + "@oxc-parser/binding-linux-arm64-gnu", + "@oxc-parser/binding-linux-arm64-musl", + "@oxc-parser/binding-linux-riscv64-gnu", + "@oxc-parser/binding-linux-s390x-gnu", + "@oxc-parser/binding-linux-x64-gnu", + "@oxc-parser/binding-linux-x64-musl", + "@oxc-parser/binding-wasm32-wasi", + "@oxc-parser/binding-win32-arm64-msvc", + "@oxc-parser/binding-win32-x64-msvc" + ] + }, + "p-limit@3.1.0": { + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": [ + "yocto-queue" + ] + }, + "p-locate@5.0.0": { + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": [ + "p-limit" + ] + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "parse5@8.0.0": { + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dependencies": [ + "entities" + ] + }, + "path-exists@4.0.0": { + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "pathe@2.0.3": { + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@4.0.3": { + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" + }, + "postcss-load-config@3.1.4_postcss@8.5.8": { + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dependencies": [ + "lilconfig", + "postcss", + "yaml" + ], + "optionalPeers": [ + "postcss" + ] + }, + "postcss-safe-parser@7.0.1_postcss@8.5.8": { + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dependencies": [ + "postcss" + ] + }, + "postcss-scss@4.0.9_postcss@8.5.8": { + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dependencies": [ + "postcss" + ] + }, + "postcss-selector-parser@7.1.1": { + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss@8.5.8": { + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prelude-ls@1.2.1": { + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + }, + "prettier-plugin-svelte@3.5.1_prettier@3.8.1_svelte@5.53.12": { + "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "dependencies": [ + "prettier", + "svelte" + ] + }, + "prettier@3.8.1": { + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "bin": true + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "readdirp@4.1.2": { + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, + "readdirp@5.0.0": { + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==" + }, + "require-from-string@2.0.2": { + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "rollup@4.59.0": { + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dependencies": [ + "@types/estree" + ], + "optionalDependencies": [ + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loong64-gnu", + "@rollup/rollup-linux-loong64-musl", + "@rollup/rollup-linux-ppc64-gnu", + "@rollup/rollup-linux-ppc64-musl", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-riscv64-musl", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-openbsd-x64", + "@rollup/rollup-openharmony-arm64", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-gnu", + "@rollup/rollup-win32-x64-msvc", + "fsevents" + ], + "bin": true + }, + "sade@1.8.1": { + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": [ + "mri" + ] + }, + "saxes@6.0.0": { + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": [ + "xmlchars" + ] + }, + "scule@1.3.0": { + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==" + }, + "semver@7.7.4": { + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": true + }, + "set-cookie-parser@3.0.1": { + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==" + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "siginfo@2.0.0": { + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" + }, + "sirv@3.0.2": { + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dependencies": [ + "@polka/url", + "mrmime", + "totalist" + ] + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "stackback@0.0.2": { + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" + }, + "std-env@4.0.0": { + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==" + }, + "strip-json-comments@3.1.1": { + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag" + ] + }, + "svelte-check@4.4.5_svelte@5.53.12_typescript@5.9.3": { + "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", + "dependencies": [ + "@jridgewell/trace-mapping", + "chokidar@4.0.3", + "fdir", + "picocolors", + "sade", + "svelte", + "typescript" + ], + "bin": true + }, + "svelte-eslint-parser@1.6.0_svelte@5.53.12": { + "integrity": "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==", + "dependencies": [ + "eslint-scope", + "eslint-visitor-keys@4.2.1", + "espree", + "postcss", + "postcss-scss", + "postcss-selector-parser", + "semver", + "svelte" + ], + "optionalPeers": [ + "svelte" + ] + }, + "svelte2tsx@0.7.52_svelte@5.53.12_typescript@5.9.3": { + "integrity": "sha512-svdT1FTrCLpvlU62evO5YdJt/kQ7nxgQxII/9BpQUvKr+GJRVdAXNVw8UWOt0fhoe5uWKyU0WsUTMRVAtRbMQg==", + "dependencies": [ + "dedent-js", + "scule", + "svelte", + "typescript" + ] + }, + "svelte@5.53.12": { + "integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==", + "dependencies": [ + "@jridgewell/remapping", + "@jridgewell/sourcemap-codec", + "@sveltejs/acorn-typescript", + "@types/estree", + "@types/trusted-types", + "acorn", + "aria-query", + "axobject-query", + "clsx", + "devalue", + "esm-env", + "esrap", + "is-reference", + "locate-character", + "magic-string", + "zimmerframe" + ] + }, + "symbol-tree@3.2.4": { + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "tinybench@2.9.0": { + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==" + }, + "tinyexec@1.0.4": { + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==" + }, + "tinyglobby@0.2.15": { + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dependencies": [ + "fdir", + "picomatch" + ] + }, + "tinyrainbow@3.1.0": { + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==" + }, + "tldts-core@7.0.26": { + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==" + }, + "tldts@7.0.26": { + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", + "dependencies": [ + "tldts-core" + ], + "bin": true + }, + "totalist@3.0.1": { + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==" + }, + "tough-cookie@6.0.1": { + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dependencies": [ + "tldts" + ] + }, + "tr46@6.0.0": { + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dependencies": [ + "punycode" + ] + }, + "ts-algebra@2.0.0": { + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + }, + "ts-api-utils@2.4.0_typescript@5.9.3": { + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dependencies": [ + "typescript" + ] + }, + "ts-blank-space@0.6.2": { + "integrity": "sha512-hZjcHdHrveEKI67v8OzI90a1bizgoDkY7ekE4fH89qJhZgxvmjfBOv98aibCU7OpKbvV3R9p/qd3DrzZqT1cFQ==", + "dependencies": [ + "typescript" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "type-check@0.4.0": { + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": [ + "prelude-ls" + ] + }, + "typescript-eslint@8.57.1_eslint@9.39.4_typescript@5.9.3": { + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dependencies": [ + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/utils", + "eslint", + "typescript" + ] + }, + "typescript@5.9.3": { + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": true + }, + "undici-types@7.16.0": { + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + }, + "uri-js@4.4.1": { + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": [ + "punycode" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "vite@7.3.1_@types+node@24.12.0": { + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dependencies": [ + "@types/node", + "esbuild", + "fdir", + "picomatch", + "postcss", + "rollup", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "optionalPeers": [ + "@types/node" + ], + "bin": true + }, + "vitefu@1.1.2_vite@7.3.1__@types+node@24.12.0_@types+node@24.12.0": { + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "dependencies": [ + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "vitest@4.1.0_@types+node@24.12.0_jsdom@27.4.0_vite@7.3.1__@types+node@24.12.0": { + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dependencies": [ + "@types/node", + "@vitest/expect", + "@vitest/mocker", + "@vitest/pretty-format", + "@vitest/runner", + "@vitest/snapshot", + "@vitest/spy", + "@vitest/utils", + "es-module-lexer", + "expect-type", + "jsdom", + "magic-string", + "obug", + "pathe", + "picomatch", + "std-env", + "tinybench", + "tinyexec", + "tinyglobby", + "tinyrainbow", + "vite", + "why-is-node-running" + ], + "optionalPeers": [ + "@types/node", + "jsdom" + ], + "bin": true + }, + "w3c-xmlserializer@5.0.0": { + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": [ + "xml-name-validator" + ] + }, + "webidl-conversions@8.0.1": { + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==" + }, + "whatwg-fetch@3.6.20": { + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "whatwg-mimetype@4.0.0": { + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" + }, + "whatwg-mimetype@5.0.0": { + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==" + }, + "whatwg-url@15.1.0": { + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dependencies": [ + "tr46", + "webidl-conversions" + ] + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe" + ], + "bin": true + }, + "why-is-node-running@2.3.0": { + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dependencies": [ + "siginfo", + "stackback" + ], + "bin": true + }, + "word-wrap@1.2.5": { + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, + "ws@8.19.0": { + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==" + }, + "xml-name-validator@5.0.0": { + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==" + }, + "xmlchars@2.2.0": { + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "yaml@1.10.2": { + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yocto-queue@0.1.0": { + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zimmerframe@1.1.4": { + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==" + }, + "zod@4.3.6": { + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==" + } + }, + "workspace": { + "dependencies": [ + "npm:@anthropic-ai/sdk@~0.71.2", + "npm:@electric-sql/pglite@0.3", + "npm:@fuzdev/blake3_wasm@0.1", + "npm:@fuzdev/fuz_util@~0.53.4", + "npm:@fuzdev/gro@0.197", + "npm:@google/generative-ai@~0.24.1", + "npm:esm-env@1", + "npm:hono@4", + "npm:ollama@0.6", + "npm:openai@^6.10.0", + "npm:svelte@5", + "npm:zod@4" + ], + "packageJson": { + "dependencies": [ + "npm:@anthropic-ai/sdk@~0.71.2", + "npm:@changesets/changelog-git@~0.2.1", + "npm:@fuzdev/blake3_wasm@0.1", + "npm:@fuzdev/fuz_code@~0.45.1", + "npm:@fuzdev/fuz_css@0.56", + "npm:@fuzdev/fuz_ui@0.190", + "npm:@fuzdev/fuz_util@0.54", + "npm:@fuzdev/gro@0.197", + "npm:@google/generative-ai@~0.24.1", + "npm:@jridgewell/trace-mapping@~0.3.31", + "npm:@ryanatkn/eslint-config@~0.10.1", + "npm:@sveltejs/acorn-typescript@^1.0.9", + "npm:@sveltejs/adapter-static@^3.0.10", + "npm:@sveltejs/kit@^2.55.0", + "npm:@sveltejs/vite-plugin-svelte@^6.2.4", + "npm:@types/deno@^2.5.0", + "npm:@types/estree@^1.0.8", + "npm:@types/node@^24.10.1", + "npm:@webref/css@^8.2.0", + "npm:@xterm/xterm@6", + "npm:date-fns@^4.1.0", + "npm:eslint-plugin-svelte@^3.13.1", + "npm:eslint@^9.39.1", + "npm:esm-env@^1.2.2", + "npm:hono@^4.12.7", + "npm:jsdom@^27.2.0", + "npm:magic-string@~0.30.21", + "npm:ollama@~0.6.3", + "npm:openai@^6.10.0", + "npm:prettier-plugin-svelte@^3.4.1", + "npm:prettier@^3.7.4", + "npm:svelte-check@^4.3.5", + "npm:svelte2tsx@~0.7.52", + "npm:svelte@^5.53.12", + "npm:tslib@^2.8.1", + "npm:typescript-eslint@^8.48.1", + "npm:typescript@^5.9.3", + "npm:vite@^7.3.1", + "npm:vitest@^4.0.15", + "npm:zimmerframe@^1.1.4", + "npm:zod@^4.3.6" + ] + } + } +} diff --git a/docs/architecture.md b/docs/architecture.md index 9a3b059a6..86d348092 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,52 +12,52 @@ Every action is a plain object with Zod schemas. Defined in `src/lib/action_spec ```typescript export const completion_create_action_spec = { - method: 'completion_create', - kind: 'request_response', - initiator: 'frontend', - auth: 'public', - side_effects: true, - input: z.strictObject({ - completion_request: CompletionRequest, - _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), - }), - output: z.strictObject({ - completion_response: CompletionResponse, - _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), - }), - async: true, + method: 'completion_create', + kind: 'request_response', + initiator: 'frontend', + auth: 'public', + side_effects: true, + input: z.strictObject({ + completion_request: CompletionRequest, + _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), + }), + output: z.strictObject({ + completion_response: CompletionResponse, + _meta: z.looseObject({progressToken: Uuid.optional()}).optional(), + }), + async: true, } satisfies ActionSpecUnion; ``` ### Action Kinds -| Kind | Phases | Transport | Use | -|------|--------|-----------|-----| -| `request_response` | `send_request` → `receive_request` → `send_response` → `receive_response` | HTTP or WebSocket | Standard RPC | -| `remote_notification` | `send` → `receive` | WebSocket only | Streaming progress (backend → frontend) | -| `local_call` | `execute` | None | Frontend-only UI actions | +| Kind | Phases | Transport | Use | +| --------------------- | ------------------------------------------------------------------------- | ----------------- | --------------------------------------- | +| `request_response` | `send_request` → `receive_request` → `send_response` → `receive_response` | HTTP or WebSocket | Standard RPC | +| `remote_notification` | `send` → `receive` | WebSocket only | Streaming progress (backend → frontend) | +| `local_call` | `execute` | None | Frontend-only UI actions | ### Action Spec Fields -| Field | Type | Values | -|-------|------|--------| -| `method` | `string` | Action name (e.g. `'completion_create'`) | -| `kind` | `ActionKind` | `'request_response'` \| `'remote_notification'` \| `'local_call'` | -| `initiator` | `ActionInitiator` | `'frontend'` \| `'backend'` \| `'both'` | -| `auth` | `ActionAuth \| null` | `'public'` \| `'authorize'` \| `null` | -| `side_effects` | `boolean \| null` | Whether action mutates state | -| `input` | `z.ZodType` | Zod schema for request params | -| `output` | `z.ZodType` | Zod schema for response | -| `async` | `boolean` | Whether handler is async | +| Field | Type | Values | +| -------------- | -------------------- | ----------------------------------------------------------------- | +| `method` | `string` | Action name (e.g. `'completion_create'`) | +| `kind` | `ActionKind` | `'request_response'` \| `'remote_notification'` \| `'local_call'` | +| `initiator` | `ActionInitiator` | `'frontend'` \| `'backend'` \| `'both'` | +| `auth` | `ActionAuth \| null` | `'public'` \| `'authenticate'` \| `null` | +| `side_effects` | `boolean \| null` | Whether action mutates state | +| `input` | `z.ZodType` | Zod schema for request params | +| `output` | `z.ZodType` | Zod schema for response | +| `async` | `boolean` | Whether handler is async | ### Core Components -| Component | File | Purpose | -|-----------|------|---------| -| `ActionSpec` | `action_spec.ts` | Action metadata schema | -| `ActionEvent` | `action_event.ts` | Lifecycle state machine (initial → parsed → handling → handled/failed) | -| `ActionPeer` | `action_peer.ts` | Send/receive on both sides | -| `ActionRegistry` | `action_registry.ts` | Type-safe action lookup | +| Component | File | Purpose | +| ---------------- | -------------------- | ---------------------------------------------------------------------- | +| `ActionSpec` | `action_spec.ts` | Action metadata schema | +| `ActionEvent` | `action_event.ts` | Lifecycle state machine (initial → parsed → handling → handled/failed) | +| `ActionPeer` | `action_peer.ts` | Send/receive on both sides | +| `ActionRegistry` | `action_registry.ts` | Type-safe action lookup | ### Action Event Lifecycle @@ -77,37 +77,43 @@ Frontend and backend register handlers per action per phase: ```typescript // Frontend (frontend_action_handlers.ts) export const frontend_action_handlers: FrontendActionHandlers = { - completion_create: { - send_request: ({data: {input}}) => { - console.log('sending prompt:', input.completion_request.prompt); - }, - receive_response: ({app, data: {input, output}}) => { - const progress_token = input._meta?.progressToken; - if (progress_token) { - const turn = app.cell_registry.all.get(progress_token); - if (turn instanceof Turn) { - turn.content = to_completion_response_text(output.completion_response) || ''; - turn.response = output.completion_response; - } - } - }, - receive_error: ({data: {error}}) => { - console.error('completion failed:', error); - }, - }, + completion_create: { + send_request: ({data: {input}}) => { + console.log('sending prompt:', input.completion_request.prompt); + }, + receive_response: ({app, data: {input, output}}) => { + const progress_token = input._meta?.progressToken; + if (progress_token) { + const turn = app.cell_registry.all.get(progress_token); + if (turn instanceof Turn) { + turn.content = to_completion_response_text(output.completion_response) || ''; + turn.response = output.completion_response; + } + } + }, + receive_error: ({data: {error}}) => { + console.error('completion failed:', error); + }, + }, }; // Backend (server/backend_action_handlers.ts) export const backend_action_handlers: BackendActionHandlers = { - completion_create: { - receive_request: async ({backend, data: {input}}) => { - const {prompt, provider_name, model, completion_messages} = input.completion_request; - const progress_token = input._meta?.progressToken; - const provider = backend.lookup_provider(provider_name); - const handler = provider.get_handler(!!progress_token); - return await handler({model, prompt, completion_messages, completion_options, progress_token}); - }, - }, + completion_create: { + receive_request: async ({backend, data: {input}}) => { + const {prompt, provider_name, model, completion_messages} = input.completion_request; + const progress_token = input._meta?.progressToken; + const provider = backend.lookup_provider(provider_name); + const handler = provider.get_handler(!!progress_token); + return await handler({ + model, + prompt, + completion_messages, + completion_options, + progress_token, + }); + }, + }, }; ``` @@ -117,10 +123,10 @@ Actions are transport-agnostic via the `Transport` interface (`transports.ts`): ```typescript interface Transport { - transport_name: TransportName; - send(message: JsonrpcRequest): Promise; - send(message: JsonrpcNotification): Promise; - is_ready: () => boolean; + transport_name: TransportName; + send(message: JsonrpcRequest): Promise; + send(message: JsonrpcNotification): Promise; + is_ready: () => boolean; } ``` @@ -139,28 +145,28 @@ MCP-compatible subset, no batching: ### All 20 Actions -| Method | Kind | Initiator | Purpose | -|--------|------|-----------|---------| -| `ping` | `request_response` | `both` | Health check | -| `session_load` | `request_response` | `frontend` | Load initial session data | -| `filer_change` | `remote_notification` | `backend` | File system change notification | -| `diskfile_update` | `request_response` | `frontend` | Write file content | -| `diskfile_delete` | `request_response` | `frontend` | Delete a file | -| `directory_create` | `request_response` | `frontend` | Create a directory | -| `completion_create` | `request_response` | `frontend` | Start AI completion | -| `completion_progress` | `remote_notification` | `backend` | Stream completion chunks | -| `ollama_progress` | `remote_notification` | `backend` | Model operation progress | -| `toggle_main_menu` | `local_call` | `frontend` | Toggle main menu UI | -| `ollama_list` | `request_response` | `frontend` | List local models | -| `ollama_ps` | `request_response` | `frontend` | List running models | -| `ollama_show` | `request_response` | `frontend` | Show model details | -| `ollama_pull` | `request_response` | `frontend` | Pull model | -| `ollama_delete` | `request_response` | `frontend` | Delete model | -| `ollama_copy` | `request_response` | `frontend` | Copy model | -| `ollama_create` | `request_response` | `frontend` | Create model | -| `ollama_unload` | `request_response` | `frontend` | Unload model from memory | -| `provider_load_status` | `request_response` | `frontend` | Check provider availability | -| `provider_update_api_key` | `request_response` | `frontend` | Update provider API key | +| Method | Kind | Initiator | Purpose | +| ------------------------- | --------------------- | ---------- | ------------------------------- | +| `ping` | `request_response` | `both` | Health check | +| `session_load` | `request_response` | `frontend` | Load initial session data | +| `filer_change` | `remote_notification` | `backend` | File system change notification | +| `diskfile_update` | `request_response` | `frontend` | Write file content | +| `diskfile_delete` | `request_response` | `frontend` | Delete a file | +| `directory_create` | `request_response` | `frontend` | Create a directory | +| `completion_create` | `request_response` | `frontend` | Start AI completion | +| `completion_progress` | `remote_notification` | `backend` | Stream completion chunks | +| `ollama_progress` | `remote_notification` | `backend` | Model operation progress | +| `toggle_main_menu` | `local_call` | `frontend` | Toggle main menu UI | +| `ollama_list` | `request_response` | `frontend` | List local models | +| `ollama_ps` | `request_response` | `frontend` | List running models | +| `ollama_show` | `request_response` | `frontend` | Show model details | +| `ollama_pull` | `request_response` | `frontend` | Pull model | +| `ollama_delete` | `request_response` | `frontend` | Delete model | +| `ollama_copy` | `request_response` | `frontend` | Copy model | +| `ollama_create` | `request_response` | `frontend` | Create model | +| `ollama_unload` | `request_response` | `frontend` | Unload model from memory | +| `provider_load_status` | `request_response` | `frontend` | Check provider availability | +| `provider_update_api_key` | `request_response` | `frontend` | Update provider API key | ## Cell System @@ -201,8 +207,8 @@ export abstract class Cell implements Cel ```typescript interface CellOptions { - app: Frontend; // Root app state reference - json?: z.input; // Initial JSON data (parsed by schema) + app: Frontend; // Root app state reference + json?: z.input; // Initial JSON data (parsed by schema) } ``` @@ -213,36 +219,36 @@ Real example from `chat.svelte.ts`: ```typescript // 1. Schema with CellJson base — every field has .default() export const ChatJson = CellJson.extend({ - name: z.string().default(''), - thread_ids: z.array(Uuid).default(() => []), - main_input: z.string().default(''), - view_mode: z.enum(['simple', 'multi']).default('simple'), - selected_thread_id: Uuid.nullable().default(null), + name: z.string().default(''), + thread_ids: z.array(Uuid).default(() => []), + main_input: z.string().default(''), + view_mode: z.enum(['simple', 'multi']).default('simple'), + selected_thread_id: Uuid.nullable().default(null), }).meta({cell_class_name: 'Chat'}); // 2. Class: $state for schema fields, $derived for computed export class Chat extends Cell { - name: string = $state()!; - thread_ids: Array = $state()!; - main_input: string = $state()!; - view_mode: ChatViewMode = $state()!; - selected_thread_id: Uuid | null = $state()!; - - readonly threads: Array = $derived.by(() => { - const result: Array = []; - for (const id of this.thread_ids) { - const thread = this.app.threads.items.by_id.get(id); - if (thread) result.push(thread); - } - return result; - }); - - readonly enabled_threads = $derived(this.threads.filter((t) => t.enabled)); - - constructor(options: ChatOptions) { - super(ChatJson, options); - this.init(); // Must call at end - } + name: string = $state()!; + thread_ids: Array = $state()!; + main_input: string = $state()!; + view_mode: ChatViewMode = $state()!; + selected_thread_id: Uuid | null = $state()!; + + readonly threads: Array = $derived.by(() => { + const result: Array = []; + for (const id of this.thread_ids) { + const thread = this.app.threads.items.by_id.get(id); + if (thread) result.push(thread); + } + return result; + }); + + readonly enabled_threads = $derived(this.threads.filter((t) => t.enabled)); + + constructor(options: ChatOptions) { + super(ChatJson, options); + this.init(); // Must call at end + } } ``` @@ -277,12 +283,17 @@ All cell classes are registered in `cell_classes.ts`. Frontend iterates and regi ```typescript // cell_classes.ts — add new classes here export const cell_classes = { - Parts, Chat, Chats, Thread, Threads, Turn, /* ... 26 total */ + Parts, + Chat, + Chats, + Thread, + Threads, + Turn /* ... 26 total */, } satisfies Record>; // frontend.svelte.ts — auto-registers all classes for (const constructor of Object.values(cell_classes)) { - this.cell_registry.register(constructor); + this.cell_registry.register(constructor); } // Lookup by ID at runtime @@ -303,9 +314,9 @@ Prompt → parts: Array (reusable content templates) ### Parts -| Type | Class | Content source | -|------|-------|----------------| -| Text | `TextPart` | `content: string` stored directly | +| Type | Class | Content source | +| -------- | -------------- | ------------------------------------------------------ | +| Text | `TextPart` | `content: string` stored directly | | Diskfile | `DiskfilePart` | `path: DiskfilePath` → reads from disk or editor state | ### Turns @@ -314,17 +325,20 @@ Conversation messages with role: ```typescript class Turn extends Cell { - part_ids: Array = $state()!; - role: CompletionRole = $state()!; // 'user' | 'assistant' | 'system' - request: CompletionRequest | undefined = $state.raw(); - response: CompletionResponse | undefined = $state.raw(); - - readonly content: string = $derived( - this.parts.map(p => p.content).filter(Boolean).join('\n\n') - ); - readonly pending: boolean = $derived( - this.role === 'assistant' && this.is_content_empty && !this.response - ); + part_ids: Array = $state()!; + role: CompletionRole = $state()!; // 'user' | 'assistant' | 'system' + request: CompletionRequest | undefined = $state.raw(); + response: CompletionResponse | undefined = $state.raw(); + + readonly content: string = $derived( + this.parts + .map((p) => p.content) + .filter(Boolean) + .join('\n\n'), + ); + readonly pending: boolean = $derived( + this.role === 'assistant' && this.is_content_empty && !this.response, + ); } ``` @@ -404,31 +418,31 @@ Queryable reactive collections with multiple index types. From `indexed_collecti ```typescript class IndexedCollection { - readonly by_id: SvelteMap = new SvelteMap(); - readonly values: Array = $derived(Array.from(this.by_id.values())); - readonly size: number = $derived(this.by_id.size); + readonly by_id: SvelteMap = new SvelteMap(); + readonly values: Array = $derived(Array.from(this.by_id.values())); + readonly size: number = $derived(this.by_id.size); } ``` ### Index Types -| Type | Cardinality | Example | -|------|-------------|---------| -| `single` | One key → one item | `by('name', 'gpt-5')` | -| `multi` | One key → many items | `where('provider_name', 'ollama')` | +| Type | Cardinality | Example | +| --------- | --------------------- | ---------------------------------- | +| `single` | One key → one item | `by('name', 'gpt-5')` | +| `multi` | One key → many items | `where('provider_name', 'ollama')` | | `derived` | Computed sorted array | `derived_index('ordered_by_name')` | -| `dynamic` | Runtime-computed | Custom queries | +| `dynamic` | Runtime-computed | Custom queries | ### Index Definition ```typescript interface IndexDefinition { - key: string; - type?: 'single' | 'multi' | 'derived' | 'dynamic'; - extractor?: (item: T) => any; - compute: (collection: IndexedCollection) => TResult; - onadd?: (result: TResult, item: T, collection: IndexedCollection) => TResult; - onremove?: (result: TResult, item: T, collection: IndexedCollection) => TResult; + key: string; + type?: 'single' | 'multi' | 'derived' | 'dynamic'; + extractor?: (item: T) => any; + compute: (collection: IndexedCollection) => TResult; + onadd?: (result: TResult, item: T, collection: IndexedCollection) => TResult; + onremove?: (result: TResult, item: T, collection: IndexedCollection) => TResult; } ``` @@ -437,27 +451,27 @@ interface IndexDefinition { ```typescript // Create with indexes const items = new IndexedCollection({ - indexes: [ - create_single_index({key: 'name', extractor: m => m.name}), - create_multi_index({key: 'provider_name', extractor: m => m.provider_name}), - create_derived_index({key: 'ordered_by_name', sort: (a, b) => a.name.localeCompare(b.name)}), - ], + indexes: [ + create_single_index({key: 'name', extractor: (m) => m.name}), + create_multi_index({key: 'provider_name', extractor: (m) => m.provider_name}), + create_derived_index({key: 'ordered_by_name', sort: (a, b) => a.name.localeCompare(b.name)}), + ], }); // Query -items.by('name', 'gpt-5'); // single → Model | undefined -items.where('provider_name', 'ollama'); // multi → Array -items.derived_index('ordered_by_name'); // derived → Array +items.by('name', 'gpt-5'); // single → Model | undefined +items.where('provider_name', 'ollama'); // multi → Array +items.derived_index('ordered_by_name'); // derived → Array ``` ## Filesystem Two separate concerns: -| Concern | Env Var | Purpose | -|---------|---------|---------| -| App directory | `PUBLIC_ZZZ_DIR` | Zzz's own data (`.zzz/state/`, `.zzz/cache/`, `.zzz/run/`) | -| Scoped dirs | `PUBLIC_ZZZ_SCOPED_DIRS` | User file access (comma-separated paths) | +| Concern | Env Var | Purpose | +| ------------- | ------------------------ | ---------------------------------------------------------- | +| App directory | `PUBLIC_ZZZ_DIR` | Zzz's own data (`.zzz/state/`, `.zzz/cache/`, `.zzz/run/`) | +| Scoped dirs | `PUBLIC_ZZZ_SCOPED_DIRS` | User file access (comma-separated paths) | ### ScopedFs @@ -467,6 +481,6 @@ All filesystem operations go through `ScopedFs` (`server/scoped_fs.ts`). Securit Each scoped directory gets a `Filer` watcher. File changes are broadcast to clients via `filer_change` notifications over WebSocket. -### Server Info +### Daemon Info -`run/server.json` tracks the running server (PID, port, version). Written atomically on startup, removed on clean shutdown (SIGINT/SIGTERM). Stale detection via `process.kill(pid, 0)`. +`run/daemon.json` tracks the running server (PID, port, version). Written atomically on startup via `@fuzdev/fuz_app/cli/daemon.js`, removed on clean shutdown (SIGINT/SIGTERM). Stale detection via `kill -0`. diff --git a/docs/development.md b/docs/development.md index d5493e104..42fb24bc9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -12,6 +12,24 @@ npm install Optionally add API keys to `.env.development` for remote providers (Anthropic, OpenAI, Google). Ollama requires no key. +### PTY support (optional) + +Terminal integration uses a Rust shared library (`fuz_pty`) for real PTY +support via Deno FFI. Without it, terminals fall back to `Deno.Command` pipes +(commands run but no echo, no prompt, no interactivity). + +```bash +cd ~/dev/private_fuz && cargo build -p fuz_pty --release +``` + +This produces `target/release/libfuz_pty.so`, which zzz loads at runtime via +`Deno.dlopen()`. The dev server needs `--allow-ffi` (already set in +`gro.config.ts`). The compiled binary also has `--allow-ffi`. + +For bundled/compiled binaries, place `libfuz_pty.so` next to the `zzz` +executable. The library lookup checks exe-relative path first, then falls back +to the dev path (`~/dev/private_fuz/target/release/`). + ## Commands | Command | Purpose | @@ -65,6 +83,7 @@ Components use `PascalCase` with domain prefixes: | `Ollama` | Ollama-specific | `OllamaManager`, `OllamaPullModel` | | `Part` | Content parts | `PartView`, `PartEditorForText` | | `Prompt` | Prompts | `PromptList`, `PromptPickerDialog` | +| `Terminal` | Terminals | `TerminalRunner`, `TerminalView`, `TerminalContextmenu` | | `Thread` | Threads | `ThreadList`, `ThreadContextmenu` | | `Turn` | Turns | `TurnView`, `TurnListitem` | diff --git a/gro.config.ts b/gro.config.ts new file mode 100644 index 000000000..3aa8cf455 --- /dev/null +++ b/gro.config.ts @@ -0,0 +1,45 @@ +import type {CreateGroConfig} from '@fuzdev/gro'; +import {gro_plugin_deno_compile} from '@fuzdev/gro/gro_plugin_deno_compile.js'; +import {gro_plugin_deno_server} from '@fuzdev/gro/gro_plugin_deno_server.js'; + +// eslint-disable-next-line @typescript-eslint/require-await +const config: CreateGroConfig = async (base_config) => { + const base_plugins = base_config.plugins; + base_config.plugins = async (ctx) => { + const plugins = (await base_plugins(ctx)).filter((p) => p.name !== 'gro_plugin_server'); + plugins.push( + gro_plugin_deno_server({ + entry: 'src/lib/server/server.ts', + port: 8999, + permissions: [ + '--allow-net', + '--allow-read', + '--allow-write', + '--allow-env', + '--allow-run', + '--allow-ffi', + '--allow-sys', + ], + flags: ['--no-check', '--sloppy-imports'], + }), + ); + return [ + ...plugins, + gro_plugin_deno_compile({ + entry: 'src/lib/zzz/main.ts', + output_name: 'zzz', + flags: [ + '--no-check', + '--sloppy-imports', + '--allow-ffi', + '--include', + 'node_modules/@fuzdev/blake3_wasm', + ], + }), + ]; + }; + + return base_config; +}; + +export default config; diff --git a/package-lock.json b/package-lock.json index 6bc070077..00729c9c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,28 +10,30 @@ "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.71.2", - "@fuzdev/gro": "^0.197.1", + "@fuzdev/blake3_wasm": "^0.1.0", + "@fuzdev/gro": "^0.197.2", "@google/generative-ai": "^0.24.1", - "@hono/node-server": "^1.19.6", - "@hono/node-ws": "^1.2.0", + "@xterm/xterm": "^6.0.0", "date-fns": "^4.1.0", "esm-env": "^1.2.2", - "hono": "^4.10.7", + "hono": "^4.12.7", "openai": "^6.10.0", "zod": "^4.3.6" }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_app": "^0.2.1", "@fuzdev/fuz_code": "^0.45.1", - "@fuzdev/fuz_css": "^0.57.0", - "@fuzdev/fuz_ui": "^0.191.1", + "@fuzdev/fuz_css": "^0.58.0", + "@fuzdev/fuz_ui": "^0.191.3", "@fuzdev/fuz_util": "^0.55.0", "@jridgewell/trace-mapping": "^0.3.31", - "@ryanatkn/eslint-config": "^0.9.0", - "@sveltejs/adapter-node": "^5.4.0", + "@ryanatkn/eslint-config": "^0.10.1", + "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/deno": "^2.5.0", "@types/estree": "^1.0.8", "@types/node": "^24.10.1", "@webref/css": "^8.2.0", @@ -42,12 +44,13 @@ "ollama": "^0.6.3", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.54.0", + "svelte": "^5.55.0", "svelte-check": "^4.4.5", "svelte2tsx": "^0.7.52", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", + "vite": "^7.3.1", "vitest": "^4.0.15", "zimmerframe": "^1.1.4" }, @@ -62,10 +65,77 @@ "svelte": "^5" } }, + "../fuz_app": { + "name": "@fuzdev/fuz_app", + "version": "0.1.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@electric-sql/pglite": "^0.3.16", + "@fuzdev/blake3_wasm": "^0.1.0", + "@fuzdev/fuz_code": "^0.45.1", + "@fuzdev/fuz_css": "^0.57.0", + "@fuzdev/fuz_ui": "^0.191.1", + "@fuzdev/fuz_util": "^0.55.0", + "@fuzdev/gro": "^0.197.1", + "@jridgewell/trace-mapping": "^0.3.31", + "@node-rs/argon2": "^2.0.2", + "@ryanatkn/eslint-config": "^0.10.1", + "@sveltejs/acorn-typescript": "^1.0.9", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.55.0", + "@sveltejs/package": "^2.5.7", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/estree": "^1.0.8", + "@types/pg": "^8.18.0", + "@webref/css": "^8.2.0", + "eslint": "^9.39.4", + "eslint-plugin-svelte": "^3.15.1", + "esm-env": "^1.2.2", + "hono": "^4.12.7", + "jsdom": "^28.1.0", + "magic-string": "^0.30.21", + "pg": "^8.20.0", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.5.1", + "svelte": "^5.54.1", + "svelte-check": "^4.4.5", + "svelte2tsx": "^0.7.52", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^7.3.1", + "vitest": "^4.0.18", + "zimmerframe": "^1.1.4", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=22.15" + }, + "peerDependencies": { + "@electric-sql/pglite": ">=0.3", + "@fuzdev/blake3_wasm": ">=0.1.0", + "@fuzdev/fuz_util": ">=0.53.4", + "@node-rs/argon2": ">=2", + "@sveltejs/kit": "^2", + "hono": ">=4", + "pg": ">=8", + "svelte": "^5", + "zod": ">=4" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "pg": { + "optional": true + } + } + }, "node_modules/@acemir/cssom": { - "version": "0.9.24", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.24.tgz", - "integrity": "sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==", + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", "devOptional": true, "license": "MIT" }, @@ -90,23 +160,23 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", - "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", "devOptional": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.2" + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", - "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -114,7 +184,7 @@ "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.2" + "lru-cache": "^11.2.6" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -125,9 +195,9 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -151,9 +221,9 @@ "license": "MIT" }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "devOptional": true, "funding": [ { @@ -167,13 +237,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "devOptional": true, "funding": [ { @@ -187,17 +257,17 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "devOptional": true, "funding": [ { @@ -211,21 +281,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "devOptional": true, "funding": [ { @@ -239,16 +309,16 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.17.tgz", - "integrity": "sha512-LCC++2h8pLUSPY+EsZmrrJ1EOUu+5iClpEiDhhdw3zRJpPbABML/N5lmRuBHjxtKm9VnRcsUzioyD0sekFMF0A==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", "devOptional": true, "funding": [ { @@ -261,14 +331,19 @@ } ], "license": "MIT-0", - "engines": { - "node": ">=18" + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "devOptional": true, "funding": [ { @@ -282,24 +357,24 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "license": "MIT", "optional": true, "dependencies": { @@ -307,9 +382,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "license": "MIT", "optional": true, "dependencies": { @@ -317,9 +392,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -328,15 +403,14 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -345,15 +419,14 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -362,15 +435,14 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -379,15 +451,14 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -396,15 +467,14 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -413,15 +483,14 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -430,15 +499,14 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -447,15 +515,14 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -464,15 +531,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -481,15 +547,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -498,15 +563,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -515,15 +579,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -532,15 +595,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -549,15 +611,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -566,15 +627,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -583,15 +643,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -600,15 +659,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -617,15 +675,14 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -634,15 +691,14 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -651,15 +707,14 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -668,15 +723,14 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -685,15 +739,14 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -702,15 +755,14 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -719,15 +771,14 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -736,15 +787,14 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -753,15 +803,14 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -791,9 +840,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -801,15 +850,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -842,20 +891,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -879,9 +928,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -915,12 +964,29 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fuzdev/blake3_wasm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@fuzdev/blake3_wasm/-/blake3_wasm-0.1.0.tgz", "integrity": "sha512-EU5uUcSX55Li3IXi1NiBDoVlxCN8ip9wqAhVZlMBEUa+cFQtLL6Z8GpYjlWy0KosLmxy2Z9WQv49PAkiAzFppg==", "license": "MIT", - "peer": true, "engines": { "node": ">=20" }, @@ -928,6 +994,35 @@ "url": "https://www.ryanatkn.com/funding" } }, + "node_modules/@fuzdev/fuz_app": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_app/-/fuz_app-0.2.1.tgz", + "integrity": "sha512-/MYFHS3w4QGzHaakXj9yATq0HEM/Xz/emkbaYDQ4YvoIoHUyx7XbtrWfKiwapd6FmZdkBB4uoMKrdaCBliVjeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.15" + }, + "peerDependencies": { + "@electric-sql/pglite": ">=0.3", + "@fuzdev/blake3_wasm": ">=0.1.0", + "@fuzdev/fuz_util": ">=0.53.4", + "@node-rs/argon2": ">=2", + "@sveltejs/kit": "^2", + "hono": ">=4", + "pg": ">=8", + "svelte": "^5", + "zod": ">=4" + }, + "peerDependenciesMeta": { + "@electric-sql/pglite": { + "optional": true + }, + "pg": { + "optional": true + } + } + }, "node_modules/@fuzdev/fuz_code": { "version": "0.45.1", "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.45.1.tgz", @@ -967,9 +1062,9 @@ } }, "node_modules/@fuzdev/fuz_css": { - "version": "0.57.0", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.57.0.tgz", - "integrity": "sha512-2UGLAG4tfvLEOqTWTdV1j5raitAJ5YzyiM2luXaJfddSCfs0lZGlVP4M8i2DtIfcCBJDJfanjZuu2WSNm8MYjQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_css/-/fuz_css-0.58.0.tgz", + "integrity": "sha512-+66ZulIMyZj6xdh61kpTznZaSucpMtUggJOQjEmzdLYAY74GUFsda6801J6YgQhlqJ3QX7wzP8z7+lw8NMcMUQ==", "dev": true, "license": "MIT", "engines": { @@ -1016,9 +1111,9 @@ } }, "node_modules/@fuzdev/fuz_ui": { - "version": "0.191.1", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.191.1.tgz", - "integrity": "sha512-AXrlIcx+ijB98+z6RejsVIyQXeHfr7VfYsXOjS6GNALU6kFbrU1QArUbjv57TciqONEIh5bVywX3tf4fMOL0zQ==", + "version": "0.191.3", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_ui/-/fuz_ui-0.191.3.tgz", + "integrity": "sha512-VKKguEpQnItnlvPfO3bwayra1APf6IaUrKC3kGxoXsU1w3xWCr0FZEfbj0x2pc/PSFuZH7qbA0FY+hBV3gIqMA==", "dev": true, "license": "MIT", "engines": { @@ -1112,9 +1207,9 @@ } }, "node_modules/@fuzdev/gro": { - "version": "0.197.1", - "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.197.1.tgz", - "integrity": "sha512-FAiMQ4Pngbc9+CsF1WFjwD3Q+L+AVMyRXswnjl0/AczTCcYt+kBPQYEsZ4K9E6QcYT9WPuRs+WI1CX9YVGofyw==", + "version": "0.197.2", + "resolved": "https://registry.npmjs.org/@fuzdev/gro/-/gro-0.197.2.tgz", + "integrity": "sha512-YwMXDcegJAxMv1Ftm0lAQNnFUsPe64fEeW09xeNc4nl0B4GbDaeO4NZwVU+kKa6GHnL9AmpIZPnvsa5UYJwSng==", "license": "MIT", "dependencies": { "chokidar": "^5.0.0", @@ -1157,34 +1252,6 @@ } } }, - "node_modules/@fuzdev/gro/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@fuzdev/gro/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@google/generative-ai": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", @@ -1194,34 +1261,6 @@ "node": ">=18.0.0" } }, - "node_modules/@hono/node-server": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", - "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@hono/node-ws": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hono/node-ws/-/node-ws-1.2.0.tgz", - "integrity": "sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ==", - "license": "MIT", - "dependencies": { - "ws": "^8.17.0" - }, - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "@hono/node-server": "^1.11.1", - "hono": "^4.6.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1335,122 +1374,415 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.99.0.tgz", - "integrity": "sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==", + "node_modules/@node-rs/argon2": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz", + "integrity": "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@node-rs/argon2-android-arm-eabi": "2.0.2", + "@node-rs/argon2-android-arm64": "2.0.2", + "@node-rs/argon2-darwin-arm64": "2.0.2", + "@node-rs/argon2-darwin-x64": "2.0.2", + "@node-rs/argon2-freebsd-x64": "2.0.2", + "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", + "@node-rs/argon2-linux-arm64-gnu": "2.0.2", + "@node-rs/argon2-linux-arm64-musl": "2.0.2", + "@node-rs/argon2-linux-x64-gnu": "2.0.2", + "@node-rs/argon2-linux-x64-musl": "2.0.2", + "@node-rs/argon2-wasm32-wasi": "2.0.2", + "@node-rs/argon2-win32-arm64-msvc": "2.0.2", + "@node-rs/argon2-win32-ia32-msvc": "2.0.2", + "@node-rs/argon2-win32-x64-msvc": "2.0.2" + } + }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", "cpu": [ - "arm64" + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-darwin-arm64": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.99.0.tgz", - "integrity": "sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==", + "node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.99.0.tgz", - "integrity": "sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==", + "node_modules/@node-rs/argon2-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==", "cpu": [ - "x64" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.99.0.tgz", - "integrity": "sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==", + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.99.0.tgz", - "integrity": "sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==", + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", "cpu": [ - "arm" + "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.99.0.tgz", - "integrity": "sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==", + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.99.0.tgz", - "integrity": "sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==", + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10" } }, - "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.99.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.99.0.tgz", - "integrity": "sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==", + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", + "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", + "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.99.0.tgz", + "integrity": "sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.99.0.tgz", + "integrity": "sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.99.0.tgz", + "integrity": "sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.99.0.tgz", + "integrity": "sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.99.0.tgz", + "integrity": "sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.99.0.tgz", + "integrity": "sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.99.0.tgz", + "integrity": "sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.99.0.tgz", + "integrity": "sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==", "cpu": [ "arm64" ], @@ -1591,146 +1923,26 @@ "devOptional": true, "license": "MIT" }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz", - "integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==", - "dev": true, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", - "cpu": [ - "arm64" - ], + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], "license": "MIT", "optional": true, "os": [ @@ -1738,9 +1950,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1751,9 +1963,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1764,9 +1976,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1777,9 +1989,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1790,9 +2002,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1803,9 +2015,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1816,9 +2028,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1829,9 +2041,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1841,10 +2053,23 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1855,9 +2080,22 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1868,9 +2106,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1881,9 +2119,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1894,9 +2132,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1907,9 +2145,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1920,9 +2158,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1932,10 +2170,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1946,9 +2197,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1959,9 +2210,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1971,10 +2222,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1985,9 +2249,9 @@ ] }, "node_modules/@ryanatkn/eslint-config": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.9.0.tgz", - "integrity": "sha512-RF42tZfJo2CYE4E3clQRBm9bVHMpL5ErR3HfWaxbiuL1aGraehegsiXMsr1L4BiKpSP55ZO8vvCr1ibUaSRIrQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@ryanatkn/eslint-config/-/eslint-config-0.10.1.tgz", + "integrity": "sha512-fHQ5PyFriflVj/fiF9m4SoUnipyK/Of522HL3+YA5TD2lKdJueA5c4wxucxkuFanuZ1FvsCBjGN/wMHO94HNHA==", "dev": true, "license": "Unlicense", "dependencies": { @@ -2006,37 +2270,21 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "devOptional": true, "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" } }, - "node_modules/@sveltejs/adapter-node": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.4.0.tgz", - "integrity": "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "rollup": "^4.9.5" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.4.0" - } - }, "node_modules/@sveltejs/adapter-static": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", @@ -2111,13 +2359,13 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", - "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", "devOptional": true, "license": "MIT", "dependencies": { - "debug": "^4.4.1" + "obug": "^2.1.0" }, "engines": { "node": "^20.19 || ^22.12 || >=24" @@ -2163,6 +2411,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/deno": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/deno/-/deno-2.5.0.tgz", + "integrity": "sha512-g8JS38vmc0S87jKsFzre+0ZyMOUDHPVokEJymSCRlL57h6f/FdKPWBXgdFh3Z8Ees9sz11qt9VWELU9Y9ZkiVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2177,22 +2432,15 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2200,21 +2448,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2224,8 +2471,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -2240,17 +2487,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2260,20 +2507,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2287,14 +2534,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2305,9 +2552,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", "dev": true, "license": "MIT", "engines": { @@ -2322,17 +2569,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2342,15 +2589,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", - "dev": true, + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2361,21 +2607,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2388,43 +2634,56 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2434,19 +2693,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2456,18 +2715,31 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/expect": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz", - "integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "devOptional": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.15", - "@vitest/utils": "4.0.15", - "chai": "^6.2.1", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2475,13 +2747,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz", - "integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.15", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2490,7 +2762,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -2502,9 +2774,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", - "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2515,13 +2787,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz", - "integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.15", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -2529,13 +2801,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz", - "integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.15", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2544,9 +2817,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz", - "integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "devOptional": true, "license": "MIT", "funding": { @@ -2554,13 +2827,14 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz", - "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.15", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2568,19 +2842,28 @@ } }, "node_modules/@webref/css": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@webref/css/-/css-8.2.0.tgz", - "integrity": "sha512-BSTwlyJwR2LotmT6GTmO5WIPPORr+4lU39vDBWNVEFnLo9w3XYCuHU4lmmd8OY5Zj9ykadg6pfJ/1cFHxzyr3w==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@webref/css/-/css-8.4.1.tgz", + "integrity": "sha512-8DTncc0dhWJ4lVbi9rhLVyMNm+YEYrsFLRbdjgMxPupjNHcAdXiT1s4ZWJXzN4ckUvYQKTjLJKtZWc6tsR4FIQ==", "dev": true, "license": "MIT", "peerDependencies": { - "css-tree": "^3.1.0" + "css-tree": "^3.2.1" } }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2610,9 +2893,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2716,9 +2999,9 @@ } }, "node_modules/chai": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "devOptional": true, "license": "MIT", "engines": { @@ -2743,16 +3026,15 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -2787,13 +3069,6 @@ "dev": true, "license": "MIT" }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2801,6 +3076,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -2827,14 +3109,14 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "devOptional": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -2854,34 +3136,45 @@ } }, "node_modules/cssstyle": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", - "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.0.3", - "@csstools/css-syntax-patches-for-csstree": "^1.0.14", - "css-tree": "^3.1.0" + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" }, "engines": { "node": ">=20" } }, "node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", "devOptional": true, "license": "MIT", "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" }, "engines": { "node": ">=20" } }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -2948,9 +3241,9 @@ "license": "MIT" }, "node_modules/dotenv": { - "version": "17.2.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", - "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2973,19 +3266,18 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "devOptional": true, "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2993,32 +3285,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escape-string-regexp": { @@ -3035,25 +3327,25 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -3072,7 +3364,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3095,9 +3387,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.13.1.tgz", - "integrity": "sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.15.2.tgz", + "integrity": "sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -3119,7 +3411,7 @@ "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "eslint": "^8.57.1 || ^9.0.0", + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -3183,9 +3475,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3196,12 +3488,13 @@ } }, "node_modules/esrap": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz", - "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" } }, "node_modules/esrecurse": { @@ -3248,9 +3541,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "devOptional": true, "license": "Apache-2.0", "engines": { @@ -3341,9 +3634,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -3361,16 +3654,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3385,9 +3668,9 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -3397,13 +3680,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3414,39 +3690,26 @@ "node": ">=8" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/hono": { - "version": "4.10.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.7.tgz", - "integrity": "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw==", + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", "license": "MIT", "engines": { "node": ">=16.9.0" } }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "devOptional": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/http-proxy-agent": { @@ -3477,19 +3740,6 @@ "node": ">= 14" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3527,22 +3777,6 @@ "node": ">=0.8.19" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3566,13 +3800,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3597,9 +3824,9 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3610,18 +3837,19 @@ } }, "node_modules/jsdom": { - "version": "27.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", - "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "devOptional": true, "license": "MIT", "dependencies": { - "@acemir/cssom": "^0.9.23", - "@asamuzakjp/dom-selector": "^6.7.4", - "cssstyle": "^5.3.3", + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^4.0.0", + "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", @@ -3631,7 +3859,6 @@ "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", - "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", @@ -3764,11 +3991,11 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "devOptional": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -3783,16 +4010,16 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "devOptional": true, "license": "CC0-1.0" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3877,9 +4104,9 @@ } }, "node_modules/openai": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.10.0.tgz", - "integrity": "sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.29.0.tgz", + "integrity": "sha512-YxoArl2BItucdO89/sN6edksV0x47WUTgkgVfCgX7EuEMhbirENsgYe5oO4LTjBL9PtdKtk2WqND1gSLcTd2yw==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" @@ -4025,13 +4252,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4060,9 +4280,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "devOptional": true, "funding": [ { @@ -4183,9 +4403,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -4207,9 +4427,9 @@ } }, "node_modules/prettier": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", - "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -4242,13 +4462,12 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -4265,31 +4484,10 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -4297,9 +4495,9 @@ } }, "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4313,27 +4511,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -4350,13 +4552,6 @@ "node": ">=6" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -4378,9 +4573,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4460,9 +4655,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "devOptional": true, "license": "MIT" }, @@ -4492,23 +4687,10 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/svelte": { - "version": "5.54.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.0.tgz", - "integrity": "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==", + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz", + "integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -4556,10 +4738,40 @@ "typescript": ">=5.0.0" } }, + "node_modules/svelte-check/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/svelte-check/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/svelte-eslint-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.0.tgz", - "integrity": "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz", + "integrity": "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -4568,11 +4780,12 @@ "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", - "postcss-selector-parser": "^7.0.0" + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.18.3" + "pnpm": "10.30.3" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -4616,9 +4829,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "devOptional": true, "license": "MIT", "engines": { @@ -4643,9 +4856,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "devOptional": true, "license": "MIT", "engines": { @@ -4653,709 +4866,251 @@ } }, "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", "devOptional": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.19" + "tldts-core": "^7.0.26" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", "devOptional": true, "license": "MIT" }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "devOptional": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-blank-space": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/ts-blank-space/-/ts-blank-space-0.6.2.tgz", - "integrity": "sha512-hZjcHdHrveEKI67v8OzI90a1bizgoDkY7ekE4fH89qJhZgxvmjfBOv98aibCU7OpKbvV3R9p/qd3DrzZqT1cFQ==", - "license": "Apache-2.0", - "dependencies": { - "typescript": "5.1.6 - 5.9.x" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", - "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.1", - "@typescript-eslint/parser": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "punycode": "^2.3.1" + }, "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "node_modules/ts-blank-space": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ts-blank-space/-/ts-blank-space-0.6.2.tgz", + "integrity": "sha512-hZjcHdHrveEKI67v8OzI90a1bizgoDkY7ekE4fH89qJhZgxvmjfBOv98aibCU7OpKbvV3R9p/qd3DrzZqT1cFQ==", + "license": "Apache-2.0", + "dependencies": { + "typescript": "5.1.6 - 5.9.x" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "prelude-ls": "^1.2.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=18" + "node": ">=14.17" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "devOptional": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, "bin": { - "esbuild": "bin/esbuild" + "vite": "bin/vite.js" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", "devOptional": true, "license": "MIT", "workspaces": [ @@ -5364,7 +5119,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { @@ -5373,31 +5128,31 @@ } }, "node_modules/vitest": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz", - "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "devOptional": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.15", - "@vitest/mocker": "4.0.15", - "@vitest/pretty-format": "4.0.15", - "@vitest/runner": "4.0.15", - "@vitest/snapshot": "4.0.15", - "@vitest/spy": "4.0.15", - "@vitest/utils": "4.0.15", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -5413,12 +5168,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.15", - "@vitest/browser-preview": "4.0.15", - "@vitest/browser-webdriverio": "4.0.15", - "@vitest/ui": "4.0.15", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -5447,6 +5203,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -5464,28 +5223,15 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=20" } }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -5561,9 +5307,10 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 13cb758c9..3f19c9e6a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@fuzdev/zzz", "version": "0.0.1", "description": "local-first forge for power users and devs", - "motto": "nice web things for the tired", + "tagline": "nice web things for the tired", "glyph": "💤", "logo": "logo.svg", "logo_alt": "three sleepy z's", @@ -25,7 +25,7 @@ "test": "gro test", "preview": "vite preview", "deploy": "gro deploy", - "serve": "gro build && npm run preview & node dist_server/server/server.js" + "serve": "gro build && npm run preview" }, "type": "module", "engines": { @@ -37,16 +37,18 @@ }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/fuz_app": "^0.2.1", "@fuzdev/fuz_code": "^0.45.1", - "@fuzdev/fuz_css": "^0.57.0", - "@fuzdev/fuz_ui": "^0.191.1", + "@fuzdev/fuz_css": "^0.58.0", + "@fuzdev/fuz_ui": "^0.191.3", "@fuzdev/fuz_util": "^0.55.0", "@jridgewell/trace-mapping": "^0.3.31", - "@ryanatkn/eslint-config": "^0.9.0", - "@sveltejs/adapter-node": "^5.4.0", + "@ryanatkn/eslint-config": "^0.10.1", + "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@types/deno": "^2.5.0", "@types/estree": "^1.0.8", "@types/node": "^24.10.1", "@webref/css": "^8.2.0", @@ -57,24 +59,25 @@ "ollama": "^0.6.3", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.54.0", + "svelte": "^5.55.0", "svelte-check": "^4.4.5", "svelte2tsx": "^0.7.52", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", + "vite": "^7.3.1", "vitest": "^4.0.15", "zimmerframe": "^1.1.4" }, "dependencies": { "@anthropic-ai/sdk": "^0.71.2", - "@fuzdev/gro": "^0.197.1", + "@fuzdev/blake3_wasm": "^0.1.0", + "@fuzdev/gro": "^0.197.2", "@google/generative-ai": "^0.24.1", - "@hono/node-server": "^1.19.6", - "@hono/node-ws": "^1.2.0", + "@xterm/xterm": "^6.0.0", "date-fns": "^4.1.0", "esm-env": "^1.2.2", - "hono": "^4.10.7", + "hono": "^4.12.7", "openai": "^6.10.0", "zod": "^4.3.6" }, diff --git a/src/lib/ActionListitem.svelte b/src/lib/ActionListitem.svelte index 0faeb5564..57edad652 100644 --- a/src/lib/ActionListitem.svelte +++ b/src/lib/ActionListitem.svelte @@ -22,7 +22,7 @@ - {/if} - {/snippet} - - -{#snippet children_default(popover: Popover)} - {#if children} - {@render children(popover, () => confirm(popover))} - {:else} - - {/if} -{/snippet} - - diff --git a/src/lib/Dashboard.svelte b/src/lib/Dashboard.svelte index f0021eddb..69bffd360 100644 --- a/src/lib/Dashboard.svelte +++ b/src/lib/Dashboard.svelte @@ -10,9 +10,16 @@ import {logo_zzz} from './logos.js'; import NavLink from './NavLink.svelte'; import Glyph from './Glyph.svelte'; - import {GLYPH_ARROW_LEFT, GLYPH_ARROW_RIGHT, GLYPH_PROJECT, GLYPH_TAB} from './glyphs.js'; + import { + GLYPH_ARROW_LEFT, + GLYPH_ARROW_RIGHT, + GLYPH_DESK, + GLYPH_PROJECT, + GLYPH_TAB, + } from './glyphs.js'; import {frontend_context} from './frontend.svelte.js'; import {main_nav_items_default, to_nav_link_href} from './nav.js'; + import {DESK_WIDTH} from './DeskMenu.svelte'; // TODO dashboard should be mounted with Markdown @@ -26,6 +33,7 @@ const SIDEBAR_WIDTH_MAX = 180; const sidebar_width = $derived(app.ui.show_sidebar ? SIDEBAR_WIDTH_MAX : 0); + const desk_width = $derived(app.ui.show_desk_menu && app.ui.desk_pinned ? DESK_WIDTH : 0); let futureclicks = $state(0); const FUTURECLICKS = 3; @@ -75,7 +83,18 @@ { - if (e.key === '`' && !is_editable(e.target)) { + if ( + e.key === 'Escape' && + app.ui.show_desk_menu && + !app.ui.desk_pinned && + !is_editable(e.target) + ) { + app.ui.toggle_desk_menu(false); + swallow(e); + } else if (e.key === '~' && !is_editable(e.target)) { + app.ui.toggle_desk_menu(); + swallow(e); + } else if (e.key === '`' && !is_editable(e.target)) { app.ui.toggle_sidebar(); swallow(e); } @@ -83,10 +102,15 @@ /> -
+
{@render children()}
@@ -163,11 +187,24 @@ + + + {#if !app.ui.show_desk_menu} + + {/if}
diff --git a/src/lib/DashboardPrompts.svelte b/src/lib/DashboardPrompts.svelte index 7971b26a3..714891d47 100644 --- a/src/lib/DashboardPrompts.svelte +++ b/src/lib/DashboardPrompts.svelte @@ -2,8 +2,8 @@ import {fade} from 'svelte/transition'; import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; import {random_item} from '@fuzdev/fuz_util/random.js'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; - import ConfirmButton from './ConfirmButton.svelte'; import Glyph from './Glyph.svelte'; import PartView from './PartView.svelte'; import { diff --git a/src/lib/DeskMenu.svelte b/src/lib/DeskMenu.svelte new file mode 100644 index 000000000..c7578a3d6 --- /dev/null +++ b/src/lib/DeskMenu.svelte @@ -0,0 +1,132 @@ + + + + +{#if app.ui.show_desk_menu} + +{/if} + + diff --git a/src/lib/DiskfileActions.svelte b/src/lib/DiskfileActions.svelte index 91e7f8509..3f7d7c6c9 100644 --- a/src/lib/DiskfileActions.svelte +++ b/src/lib/DiskfileActions.svelte @@ -2,8 +2,8 @@ import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; import PasteFromClipboard from '@fuzdev/fuz_ui/PasteFromClipboard.svelte'; import {slide} from 'svelte/transition'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; - import ConfirmButton from './ConfirmButton.svelte'; import {frontend_context} from './frontend.svelte.js'; import type {Diskfile} from './diskfile.svelte.js'; import ClearRestoreButton from './ClearRestoreButton.svelte'; diff --git a/src/lib/DiskfileEditorView.svelte b/src/lib/DiskfileEditorView.svelte index 4a0062488..29291458a 100644 --- a/src/lib/DiskfileEditorView.svelte +++ b/src/lib/DiskfileEditorView.svelte @@ -68,7 +68,7 @@ token_count={editor_state.current_token_count} placeholder={GLYPH_PLACEHOLDER + ' ' + diskfile.path_relative} readonly={false} - attrs={{class: 'height:100% border_radius_0'}} + attrs={{class: 'height:100% border-radius:0'}} onsave={async (value) => { await app.diskfiles.update(diskfile.path, value); }} diff --git a/src/lib/DiskfileHistoryView.svelte b/src/lib/DiskfileHistoryView.svelte index 3d967cb16..0fe028f00 100644 --- a/src/lib/DiskfileHistoryView.svelte +++ b/src/lib/DiskfileHistoryView.svelte @@ -1,8 +1,8 @@ +
diff --git a/src/lib/OllamaModelDetail.svelte b/src/lib/OllamaModelDetail.svelte index cf90762dc..6242433da 100644 --- a/src/lib/OllamaModelDetail.svelte +++ b/src/lib/OllamaModelDetail.svelte @@ -4,6 +4,7 @@ import PendingAnimation from '@fuzdev/fuz_ui/PendingAnimation.svelte'; import Details from '@fuzdev/fuz_ui/Details.svelte'; import type {Snippet} from 'svelte'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; import Glyph from './Glyph.svelte'; import { @@ -15,7 +16,6 @@ GLYPH_DISCONNECT, } from './glyphs.js'; import type {Model} from './model.svelte.js'; - import ConfirmButton from './ConfirmButton.svelte'; import ModelContextmenu from './ModelContextmenu.svelte'; import ModelLink from './ModelLink.svelte'; import {format_short_date} from './time_helpers.js'; diff --git a/src/lib/PartRemoveButton.svelte b/src/lib/PartRemoveButton.svelte index 213ef18cd..5a1fca589 100644 --- a/src/lib/PartRemoveButton.svelte +++ b/src/lib/PartRemoveButton.svelte @@ -1,11 +1,11 @@ - - -
- {#if button} - {@render button(popover)} - {:else} - - {/if} - - {#if popover.visible} -
- {@render popover_content(popover)} -
- {/if} -
diff --git a/src/lib/SocketMessageQueue.svelte b/src/lib/SocketMessageQueue.svelte index 58d8592ca..b0f4282e5 100644 --- a/src/lib/SocketMessageQueue.svelte +++ b/src/lib/SocketMessageQueue.svelte @@ -3,12 +3,12 @@ import {format} from 'date-fns'; import {SvelteMap} from 'svelte/reactivity'; import CopyToClipboard from '@fuzdev/fuz_ui/CopyToClipboard.svelte'; + import ConfirmButton from '@fuzdev/fuz_app/ui/ConfirmButton.svelte'; + import PopoverButton from '@fuzdev/fuz_app/ui/PopoverButton.svelte'; import type {Socket, QueuedMessage, FailedMessage} from './socket.svelte.js'; import Glyph from './Glyph.svelte'; import {GLYPH_RETRY, GLYPH_REMOVE, GLYPH_INFO} from './glyphs.js'; - import ConfirmButton from './ConfirmButton.svelte'; - import PopoverButton from './PopoverButton.svelte'; import {format_timestamp} from './time_helpers.js'; import {DURATION_SM} from './helpers.js'; diff --git a/src/lib/TerminalCommandInput.svelte b/src/lib/TerminalCommandInput.svelte new file mode 100644 index 000000000..0ea98c84a --- /dev/null +++ b/src/lib/TerminalCommandInput.svelte @@ -0,0 +1,42 @@ + + +
+ + +
+ + diff --git a/src/lib/TerminalContextmenu.svelte b/src/lib/TerminalContextmenu.svelte new file mode 100644 index 000000000..a1feb4c08 --- /dev/null +++ b/src/lib/TerminalContextmenu.svelte @@ -0,0 +1,24 @@ + + + + +{#snippet entries()} + {#if get_terminal_text} + + {/if} + +{/snippet} diff --git a/src/lib/TerminalPresetBar.svelte b/src/lib/TerminalPresetBar.svelte new file mode 100644 index 000000000..7c7350a13 --- /dev/null +++ b/src/lib/TerminalPresetBar.svelte @@ -0,0 +1,121 @@ + + +
+ {#each presets as preset (preset.id)} + + + {#if ondelete} + + {/if} + + {/each} + + {#if oncreate} + {#if adding} + + + + + + + {:else} + + {/if} + {/if} +
+ + diff --git a/src/lib/TerminalRunItem.svelte b/src/lib/TerminalRunItem.svelte new file mode 100644 index 000000000..55c254ab2 --- /dev/null +++ b/src/lib/TerminalRunItem.svelte @@ -0,0 +1,96 @@ + + + +
+
+ $ {display_command} + + {#if exited} + + exited {exit_code ?? '?'} + + {:else} + running + {/if} + + {#if onrestart} + + {/if} +
+
+ +
+
+
+ + diff --git a/src/lib/TerminalRunner.svelte b/src/lib/TerminalRunner.svelte new file mode 100644 index 000000000..99860693d --- /dev/null +++ b/src/lib/TerminalRunner.svelte @@ -0,0 +1,167 @@ + + +
+
+
+ {#each runs as run (run.terminal_id)} +
+ +
+ {/each} +
+
+ + {#if runs.length === 0} +

no commands run yet — use a preset or type a command below

+ {/if} + + {#if error_message} +

{error_message}

+ {/if} + +
+ + +
+
+ + diff --git a/src/lib/TerminalView.svelte b/src/lib/TerminalView.svelte new file mode 100644 index 000000000..1d18086ff --- /dev/null +++ b/src/lib/TerminalView.svelte @@ -0,0 +1,189 @@ + + +
+
+ terminal {terminal_id.slice(0, 8)} +
+ + +
+
+
+
+ + diff --git a/src/lib/ThreadListitem.svelte b/src/lib/ThreadListitem.svelte index 7d6b08d49..759983de2 100644 --- a/src/lib/ThreadListitem.svelte +++ b/src/lib/ThreadListitem.svelte @@ -1,8 +1,9 @@ + +
+
+

Terminals

+
+ + +
+ + + + diff --git a/src/test/action_event.test.ts b/src/test/action_event.test.ts index f398b1389..5b95dc4c4 100644 --- a/src/test/action_event.test.ts +++ b/src/test/action_event.test.ts @@ -1,12 +1,10 @@ -// @slop Claude Opus 4 - // @vitest-environment jsdom -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {create_action_event, create_action_event_from_json} from '$lib/action_event.js'; -import type {ActionEventEnvironment} from '$lib/action_event_types.js'; -import type {ActionSpecUnion} from '$lib/action_spec.js'; +import type {ActionEventEnvironment, ActionExecutor} from '$lib/action_event_types.js'; +import type {ActionSpecUnion} from '@fuzdev/fuz_app/actions/action_spec.js'; import { ping_action_spec, filer_change_action_spec, @@ -14,7 +12,6 @@ import { completion_create_action_spec, } from '$lib/action_specs.js'; import {create_uuid} from '$lib/zod_helpers.js'; -import type {ActionExecutor} from '$lib/action_types.js'; // Mock environment for testing class TestEnvironment implements ActionEventEnvironment { @@ -51,17 +48,17 @@ describe('ActionEvent', () => { const env = new TestEnvironment([ping_action_spec]); const event = create_action_event(env, ping_action_spec, undefined); - expect(event.data.kind).toBe('request_response'); - expect(event.data.phase).toBe('send_request'); - expect(event.data.step).toBe('initial'); - expect(event.data.method).toBe('ping'); - expect(event.data.executor).toBe('frontend'); - expect(event.data.input).toBeUndefined(); - expect(event.data.output).toBe(null); - expect(event.data.error).toBe(null); - expect(event.data.request).toBe(null); - expect(event.data.response).toBe(null); - expect(event.data.notification).toBe(null); + assert.strictEqual(event.data.kind, 'request_response'); + assert.strictEqual(event.data.phase, 'send_request'); + assert.strictEqual(event.data.step, 'initial'); + assert.strictEqual(event.data.method, 'ping'); + assert.strictEqual(event.data.executor, 'frontend'); + assert.ok(event.data.input === undefined); + assert.isNull(event.data.output); + assert.isNull(event.data.error); + assert.isNull(event.data.request); + assert.isNull(event.data.response); + assert.isNull(event.data.notification); }); test('creates event with input data', () => { @@ -78,14 +75,14 @@ describe('ActionEvent', () => { const event = create_action_event(env, completion_create_action_spec, input); - expect(event.data.input).toEqual(input); + assert.deepEqual(event.data.input, input); }); test('creates event with specified initial phase', () => { const env = new TestEnvironment([ping_action_spec]); const event = create_action_event(env, ping_action_spec, undefined, 'receive_request'); - expect(event.data.phase).toBe('receive_request'); + assert.strictEqual(event.data.phase, 'receive_request'); }); test('throws for invalid executor/initiator combination', () => { @@ -93,8 +90,9 @@ describe('ActionEvent', () => { env.executor = 'frontend'; // filer_change has initiator: 'backend', so frontend can't initiate send - expect(() => create_action_event(env, filer_change_action_spec, {})).toThrow( - "executor 'frontend' cannot initiate action 'filer_change'", + assert.throws( + () => create_action_event(env, filer_change_action_spec, {}), + /executor 'frontend' cannot initiate action 'filer_change'/, ); }); }); @@ -106,9 +104,9 @@ describe('ActionEvent', () => { event.parse(); - expect(event.data.step).toBe('parsed'); + assert.strictEqual(event.data.step, 'parsed'); // ping has void input, so it should remain undefined - expect(event.data.input).toBeUndefined(); + assert.ok(event.data.input === undefined); }); test('parses complex input with validation', () => { @@ -126,8 +124,8 @@ describe('ActionEvent', () => { const event = create_action_event(env, completion_create_action_spec, input); event.parse(); - expect(event.data.step).toBe('parsed'); - expect(event.data.input).toEqual(input); + assert.strictEqual(event.data.step, 'parsed'); + assert.deepEqual(event.data.input, input); }); test('fails on invalid input', () => { @@ -142,10 +140,10 @@ describe('ActionEvent', () => { const event = create_action_event(env, completion_create_action_spec, invalid_input); event.parse(); - expect(event.data.step).toBe('failed'); - expect(event.data.error).toBeDefined(); - expect(event.data.error?.code).toBe(-32602); - expect(event.data.error?.message).toContain('failed to parse input'); + assert.strictEqual(event.data.step, 'failed'); + assert.isDefined(event.data.error); + assert.strictEqual(event.data.error?.code, -32602); + assert.include(event.data.error?.message, 'failed to parse input'); }); test('throws when not in initial step', () => { @@ -155,7 +153,7 @@ describe('ActionEvent', () => { event.parse(); // First parse succeeds // Second parse should throw - expect(() => event.parse()).toThrow("cannot parse from step 'parsed' - must be 'initial'"); + assert.throws(() => event.parse(), /cannot parse from step 'parsed' - must be 'initial'/); }); }); @@ -172,12 +170,12 @@ describe('ActionEvent', () => { await event.handle_async(); - expect(event.data.step).toBe('handled'); + assert.strictEqual(event.data.step, 'handled'); // send_request doesn't produce output - expect(event.data.output).toBe(null); + assert.isNull(event.data.output); // But it should have created a request - expect(event.data.request).toBeDefined(); - expect(event.data.request?.method).toBe('ping'); + assert.isDefined(event.data.request); + assert.strictEqual(event.data.request?.method, 'ping'); }); test('handles missing handler gracefully', async () => { @@ -189,7 +187,7 @@ describe('ActionEvent', () => { await event.handle_async(); - expect(event.data.step).toBe('handled'); + assert.strictEqual(event.data.step, 'handled'); }); test('captures handler errors', async () => { @@ -205,11 +203,11 @@ describe('ActionEvent', () => { await event.handle_async(); // Handler errors transition to error phase, not directly to failed - expect(event.data.step).toBe('parsed'); - expect(event.data.phase).toBe('send_error'); - expect(event.data.error).toBeDefined(); - expect(event.data.error?.code).toBe(-32603); - expect(event.data.error?.message).toContain('unknown error'); + assert.strictEqual(event.data.step, 'parsed'); + assert.strictEqual(event.data.phase, 'send_error'); + assert.isDefined(event.data.error); + assert.strictEqual(event.data.error?.code, -32603); + assert.include(event.data.error?.message, 'unknown error'); }); test('send_error handler can handle errors gracefully', async () => { @@ -224,8 +222,8 @@ describe('ActionEvent', () => { // Error handler logs and completes successfully env.add_handler('ping', 'send_error', (event) => { error_logged = true; - expect(event.data.error).toBeDefined(); - expect(event.data.error?.message).toContain('primary handler error'); + assert.isDefined(event.data.error); + assert.include(event.data.error?.message, 'primary handler error'); // Error handler completes without throwing }); @@ -234,17 +232,17 @@ describe('ActionEvent', () => { await event.handle_async(); // First error transitions to send_error - expect(event.data.phase).toBe('send_error'); - expect(event.data.step).toBe('parsed'); + assert.strictEqual(event.data.phase, 'send_error'); + assert.strictEqual(event.data.step, 'parsed'); // Handle error phase await event.handle_async(); // Error handler completed successfully - expect(error_logged).toBe(true); - expect(event.data.step).toBe('failed'); - expect(event.data.phase).toBe('send_error'); - expect(event.is_complete()).toBe(true); + assert.ok(error_logged); + assert.strictEqual(event.data.step, 'failed'); + assert.strictEqual(event.data.phase, 'send_error'); + assert.ok(event.is_complete()); }); test('receive_error handler can handle errors gracefully', async () => { @@ -254,8 +252,8 @@ describe('ActionEvent', () => { // Error handler can inspect and handle the error env.add_handler('ping', 'receive_error', (event) => { error_handled = true; - expect(event.data.error).toBeDefined(); - expect(event.data.error?.code).toBe(-32603); + assert.isDefined(event.data.error); + assert.strictEqual(event.data.error?.code, -32603); // Could implement retry logic, fallback, logging, etc. }); @@ -285,16 +283,16 @@ describe('ActionEvent', () => { event.parse(); // Should be in receive_error phase - expect(event.data.phase).toBe('receive_error'); - expect(event.data.step).toBe('parsed'); + assert.strictEqual(event.data.phase, 'receive_error'); + assert.strictEqual(event.data.step, 'parsed'); // Handle error phase await event.handle_async(); // Error handler completed successfully - expect(error_handled).toBe(true); - expect(event.data.step).toBe('handled'); - expect(event.is_complete()).toBe(true); + assert.ok(error_handled); + assert.strictEqual(event.data.step, 'handled'); + assert.ok(event.is_complete()); }); test('validates output for phases that expect it', async () => { @@ -310,9 +308,9 @@ describe('ActionEvent', () => { await event.handle_async(); - expect(event.data.step).toBe('handled'); - expect(event.data.output).toBeDefined(); - expect(event.data.output).toHaveProperty('ping_id'); + assert.strictEqual(event.data.step, 'handled'); + assert.isDefined(event.data.output); + assert.ok(Object.hasOwn(event.data.output as any, 'ping_id')); }); test('throws when not in parsed step', async () => { @@ -320,9 +318,12 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); // Not parsed yet - await expect(event.handle_async()).rejects.toThrow( - "cannot handle from step 'initial' - must be 'parsed'", - ); + try { + await event.handle_async(); + assert.fail('expected handle_async to throw'); + } catch (e: any) { + assert.include(e.message, "cannot handle from step 'initial' - must be 'parsed'"); + } }); test('is no-op when already failed', async () => { @@ -338,15 +339,15 @@ describe('ActionEvent', () => { event.parse(); // Should be failed after parsing invalid input - expect(event.data.step).toBe('failed'); + assert.strictEqual(event.data.step, 'failed'); const original_error = event.data.error; // handle_async should be no-op await event.handle_async(); // State should remain unchanged - expect(event.data.step).toBe('failed'); - expect(event.data.error).toBe(original_error); + assert.strictEqual(event.data.step, 'failed'); + assert.strictEqual(event.data.error, original_error); }); }); @@ -362,8 +363,8 @@ describe('ActionEvent', () => { event.handle_sync(); - expect(event.data.step).toBe('handled'); - expect(event.data.output).toEqual(output); + assert.strictEqual(event.data.step, 'handled'); + assert.deepEqual(event.data.output, output); }); test('throws for async actions', () => { @@ -371,8 +372,9 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); event.parse(); - expect(() => event.handle_sync()).toThrow( - 'handle_sync can only be used with synchronous local_call actions', + assert.throws( + () => event.handle_sync(), + /handle_sync can only be used with synchronous local_call actions/, ); }); @@ -384,15 +386,15 @@ describe('ActionEvent', () => { event.parse(); // Should be failed after parsing invalid input - expect(event.data.step).toBe('failed'); + assert.strictEqual(event.data.step, 'failed'); const original_error = event.data.error; // handle_sync should be no-op event.handle_sync(); // State should remain unchanged - expect(event.data.step).toBe('failed'); - expect(event.data.error).toBe(original_error); + assert.strictEqual(event.data.step, 'failed'); + assert.strictEqual(event.data.error, original_error); }); }); @@ -405,16 +407,16 @@ describe('ActionEvent', () => { event.parse(); await event.handle_async(); - expect(event.data.phase).toBe('send_request'); - expect(event.data.step).toBe('handled'); + assert.strictEqual(event.data.phase, 'send_request'); + assert.strictEqual(event.data.step, 'handled'); // Transition to receive_response event.transition('receive_response'); - expect(event.data.phase).toBe('receive_response'); - expect(event.data.step).toBe('initial'); + assert.strictEqual(event.data.phase, 'receive_response'); + assert.strictEqual(event.data.step, 'initial'); // Request should be preserved - expect(event.data.request).toBeDefined(); + assert.isDefined(event.data.request); }); test('throws for invalid phase transition', async () => { @@ -425,8 +427,9 @@ describe('ActionEvent', () => { await event.handle_async(); // Can't go from send_request to send_response - expect(() => event.transition('send_response')).toThrow( - "Invalid phase transition from 'send_request' to 'send_response'", + assert.throws( + () => event.transition('send_response'), + /Invalid phase transition from 'send_request' to 'send_response'/, ); }); @@ -435,8 +438,9 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); // Still in initial step - expect(() => event.transition('receive_response')).toThrow( - "cannot transition from step 'initial' - must be 'handled'", + assert.throws( + () => event.transition('receive_response'), + /cannot transition from step 'initial' - must be 'handled'/, ); }); @@ -460,11 +464,11 @@ describe('ActionEvent', () => { // Transition to send_response event.transition('send_response'); - expect(event.data.phase).toBe('send_response'); - expect(event.data.request).toEqual(request); - expect(event.data.output).toEqual({ping_id: request.id}); - expect(event.data.response).toBeDefined(); - expect(event.data.response).toHaveProperty('result'); + assert.strictEqual(event.data.phase, 'send_response'); + assert.deepEqual(event.data.request, request); + assert.deepEqual(event.data.output, {ping_id: request.id}); + assert.isDefined(event.data.response); + assert.ok(Object.hasOwn(event.data.response as any, 'result')); }); test('is no-op when already failed', async () => { @@ -485,14 +489,14 @@ describe('ActionEvent', () => { await event.handle_async(); // First error transitions to send_error - expect(event.data.step).toBe('parsed'); - expect(event.data.phase).toBe('send_error'); + assert.strictEqual(event.data.step, 'parsed'); + assert.strictEqual(event.data.phase, 'send_error'); // Handle error phase - this will throw and transition to failed await event.handle_async(); // Now should be failed after error handler error - expect(event.data.step).toBe('failed'); + assert.strictEqual(event.data.step, 'failed'); const original_error = event.data.error; const original_phase = event.data.phase; @@ -500,9 +504,9 @@ describe('ActionEvent', () => { event.transition('receive_response'); // State should remain unchanged - expect(event.data.step).toBe('failed'); - expect(event.data.phase).toBe(original_phase); - expect(event.data.error).toBe(original_error); + assert.strictEqual(event.data.step, 'failed'); + assert.strictEqual(event.data.phase, original_phase); + assert.strictEqual(event.data.error, original_error); }); }); @@ -520,7 +524,7 @@ describe('ActionEvent', () => { event.set_request(request); - expect(event.data.request).toEqual(request); + assert.deepEqual(event.data.request, request); }); test('set_response() sets response and extracts output', () => { @@ -549,8 +553,8 @@ describe('ActionEvent', () => { event.set_response(response); - expect(event.data.response).toEqual(response); - expect(event.data.output).toEqual(response.result); + assert.deepEqual(event.data.response, response); + assert.deepEqual(event.data.output, response.result); }); test('error response transitions to receive_error phase on parse', () => { @@ -586,11 +590,11 @@ describe('ActionEvent', () => { // Parse should detect the error and transition to receive_error phase event.parse(); - expect(event.data.step).toBe('parsed'); - expect(event.data.phase).toBe('receive_error'); - expect(event.data.error).toEqual(errorResponse.error); - expect(event.data.response).toEqual(errorResponse); - expect(event.data.output).toBe(null); + assert.strictEqual(event.data.step, 'parsed'); + assert.strictEqual(event.data.phase, 'receive_error'); + assert.deepEqual(event.data.error, errorResponse.error); + assert.deepEqual(event.data.response, errorResponse); + assert.isNull(event.data.output); }); test('set_notification() sets notification data', () => { @@ -609,19 +613,21 @@ describe('ActionEvent', () => { event.set_notification(notification); - expect(event.data.notification).toEqual(notification); + assert.deepEqual(event.data.notification, notification); }); test('setters throw for wrong phase/kind', () => { const env = new TestEnvironment([ping_action_spec]); const event = create_action_event(env, ping_action_spec, undefined); - expect(() => event.set_request({} as any)).toThrow( - 'can only set request in receive_request phase', + assert.throws( + () => event.set_request({} as any), + /can only set request in receive_request phase/, ); - expect(() => event.set_notification({} as any)).toThrow( - 'can only set notification in receive phase', + assert.throws( + () => event.set_notification({} as any), + /can only set notification in receive phase/, ); }); }); @@ -633,7 +639,7 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); // Not complete in initial state - expect(event.is_complete()).toBe(false); + assert.ok(!event.is_complete()); // Handle through to receive_response event.parse(); @@ -648,7 +654,7 @@ describe('ActionEvent', () => { await event.handle_async(); // receive_response is terminal for request_response - expect(event.is_complete()).toBe(true); + assert.ok(event.is_complete()); }); test('returns true for failed state', () => { @@ -657,8 +663,8 @@ describe('ActionEvent', () => { event.parse(); // Will fail due to invalid input - expect(event.data.step).toBe('failed'); - expect(event.is_complete()).toBe(true); + assert.strictEqual(event.data.step, 'failed'); + assert.ok(event.is_complete()); }); test('returns false for non-terminal phases', () => { @@ -668,7 +674,7 @@ describe('ActionEvent', () => { event.parse(); // Parsed but not handled - expect(event.is_complete()).toBe(false); + assert.ok(!event.is_complete()); }); }); @@ -688,8 +694,8 @@ describe('ActionEvent', () => { event.parse(); - expect(changes).toHaveLength(1); - expect(changes[0]).toEqual({ + assert.strictEqual(changes.length, 1); + assert.deepEqual(changes[0], { old_step: 'initial', new_step: 'parsed', }); @@ -705,12 +711,12 @@ describe('ActionEvent', () => { }); event.parse(); - expect(call_count).toBe(1); + assert.strictEqual(call_count, 1); cleanup(); await event.handle_async(); - expect(call_count).toBe(1); // No additional calls + assert.strictEqual(call_count, 1); // No additional calls }); test('multiple listeners work independently', () => { @@ -730,8 +736,8 @@ describe('ActionEvent', () => { event.parse(); - expect(listener1_calls).toEqual(['parsed']); - expect(listener2_calls).toEqual(['parsed']); + assert.deepEqual(listener1_calls, ['parsed']); + assert.deepEqual(listener2_calls, ['parsed']); }); }); @@ -745,15 +751,15 @@ describe('ActionEvent', () => { const json = event.toJSON(); - expect(json.kind).toBe('request_response'); - expect(json.phase).toBe('send_request'); - expect(json.step).toBe('handled'); - expect(json.request).toBeDefined(); + assert.strictEqual(json.kind, 'request_response'); + assert.strictEqual(json.phase, 'send_request'); + assert.strictEqual(json.step, 'handled'); + assert.isDefined(json.request); // Reconstruct from JSON const restored = create_action_event_from_json(json, env); - expect(restored.data).toEqual(event.data); + assert.deepEqual(restored.data, event.data); }); test('throws when spec not found for deserialization', () => { @@ -773,8 +779,9 @@ describe('ActionEvent', () => { notification: null, }; - expect(() => create_action_event_from_json(json as any, env)).toThrow( - "no spec found for method 'unknown_method'", + assert.throws( + () => create_action_event_from_json(json as any, env), + /no spec found for method 'unknown_method'/, ); }); }); @@ -786,7 +793,7 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); - expect(event.app).toBe(env); + assert.strictEqual(event.app, env); }); test('backend getter works for backend environment', () => { @@ -795,7 +802,7 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); - expect(event.backend).toBe(env); + assert.strictEqual(event.backend, env); }); test('app getter throws for backend environment', () => { @@ -804,8 +811,9 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); - expect(() => event.app).toThrow( - '`action_event.app` can only be accessed in frontend environments', + assert.throws( + () => event.app, + /action_event\.app.*can only be accessed in frontend environments/, ); }); @@ -815,8 +823,9 @@ describe('ActionEvent', () => { const event = create_action_event(env, ping_action_spec, undefined); - expect(() => event.backend).toThrow( - '`action_event.backend` can only be accessed in backend environments', + assert.throws( + () => event.backend, + /action_event\.backend.*can only be accessed in backend environments/, ); }); }); @@ -835,14 +844,14 @@ describe('ActionEvent', () => { event.parse(); // Should fail during parsing - expect(event.data.step).toBe('failed'); - expect(event.data.error).toBeDefined(); - expect(event.data.error?.code).toBe(-32602); - expect(event.data.error?.message).toContain('failed to parse input'); + assert.strictEqual(event.data.step, 'failed'); + assert.isDefined(event.data.error); + assert.strictEqual(event.data.error?.code, -32602); + assert.include(event.data.error?.message, 'failed to parse input'); // Should be a no-op when handling after parse failure await event.handle_async(); - expect(event.data.step).toBe('failed'); // Still failed, no change + assert.strictEqual(event.data.step, 'failed'); // Still failed, no change }); test('remote_notification creates notification in send phase', async () => { @@ -866,9 +875,9 @@ describe('ActionEvent', () => { event.parse(); await event.handle_async(); - expect(event.data.notification).toBeDefined(); - expect(event.data.notification?.method).toBe('filer_change'); - expect(event.data.notification?.params).toEqual(input); + assert.isDefined(event.data.notification); + assert.strictEqual(event.data.notification?.method, 'filer_change'); + assert.deepEqual(event.data.notification?.params, input); }); test('local_call completes in single phase', () => { @@ -880,10 +889,10 @@ describe('ActionEvent', () => { event.parse(); event.handle_sync(); - expect(event.data.phase).toBe('execute'); - expect(event.data.step).toBe('handled'); - expect(event.data.output).toEqual({show: false}); - expect(event.is_complete()).toBe(true); + assert.strictEqual(event.data.phase, 'execute'); + assert.strictEqual(event.data.step, 'handled'); + assert.deepEqual(event.data.output, {show: false}); + assert.ok(event.is_complete()); }); }); }); diff --git a/src/test/cell.svelte.base.test.ts b/src/test/cell.svelte.base.test.ts index 259cf8881..9dbf56f71 100644 --- a/src/test/cell.svelte.base.test.ts +++ b/src/test/cell.svelte.base.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, vi, beforeEach, describe} from 'vitest'; +import {test, vi, beforeEach, describe, assert} from 'vitest'; import {z} from 'zod'; import {Cell, type CellOptions} from '$lib/cell.svelte.js'; @@ -59,16 +57,16 @@ describe('Cell initialization', () => { }); // Verify basic properties - expect(test_cell.id).toBe(TEST_ID); - expect(test_cell.created).toBe(TEST_DATETIME); - expect(test_cell.updated).toBe(test_cell.created); - expect(test_cell.text).toBe('Sample'); - expect(test_cell.number).toBe(42); - expect(test_cell.items).toEqual(['item1', 'item2']); + assert.strictEqual(test_cell.id, TEST_ID); + assert.strictEqual(test_cell.created, TEST_DATETIME); + assert.strictEqual(test_cell.updated, test_cell.created); + assert.strictEqual(test_cell.text, 'Sample'); + assert.strictEqual(test_cell.number, 42); + assert.deepEqual(test_cell.items, ['item1', 'item2']); // Verify cell was registered - expect(app.cell_registry.all.has(TEST_ID)).toBe(true); - expect(app.cell_registry.all.get(TEST_ID)).toBe(test_cell); + assert.ok(app.cell_registry.all.has(TEST_ID)); + assert.ok(app.cell_registry.all.get(TEST_ID) === (test_cell as any)); }); test('uses default values when json is empty', () => { @@ -77,13 +75,13 @@ describe('Cell initialization', () => { }); // Should use schema defaults - expect(test_cell.id).toBeDefined(); - expect(test_cell.created).toBeDefined(); - expect(test_cell.updated).toBe(test_cell.created); - expect(test_cell.text).toBe(''); - expect(test_cell.number).toBe(0); - expect(test_cell.items).toEqual([]); - expect(test_cell.flag).toBe(true); + assert.isDefined(test_cell.id); + assert.isDefined(test_cell.created); + assert.strictEqual(test_cell.updated, test_cell.created); + assert.strictEqual(test_cell.text, ''); + assert.strictEqual(test_cell.number, 0); + assert.deepEqual(test_cell.items, []); + assert.ok(test_cell.flag); }); test('derived schema properties are correctly calculated', () => { @@ -95,25 +93,25 @@ describe('Cell initialization', () => { }); // Check if schema keys contain expected fields - expect(test_cell.schema_keys).toContain('id'); - expect(test_cell.schema_keys).toContain('text'); - expect(test_cell.schema_keys).toContain('number'); - expect(test_cell.schema_keys).toContain('items'); + assert.include(test_cell.schema_keys, 'id'); + assert.include(test_cell.schema_keys, 'text'); + assert.include(test_cell.schema_keys, 'number'); + assert.include(test_cell.schema_keys, 'items'); // Check if field schemas are correctly mapped - expect(test_cell.field_schemas.size).toBeGreaterThan(0); - expect(test_cell.field_schemas.has('text')).toBe(true); - expect(test_cell.field_schemas.has('number')).toBe(true); + assert.ok(test_cell.field_schemas.size > 0); + assert.ok(test_cell.field_schemas.has('text')); + assert.ok(test_cell.field_schemas.has('number')); // Test schema info for an array type const items_info = test_cell.field_schema_info.get('items'); - expect(items_info?.is_array).toBe(true); - expect(items_info?.type).toBe('ZodArray'); + assert.ok(items_info?.is_array); + assert.strictEqual(items_info.type, 'ZodArray'); // Test schema info for a scalar type const text_info = test_cell.field_schema_info.get('text'); - expect(text_info?.is_array).toBe(false); - expect(text_info?.type).toBe('ZodString'); + assert.ok(!text_info?.is_array); + assert.strictEqual(text_info?.type, 'ZodString'); }); }); @@ -130,8 +128,8 @@ describe('Cell registry lifecycle', () => { }); // Cell should be registered automatically in init() - expect(app.cell_registry.all.has(cell_id)).toBe(true); - expect(app.cell_registry.all.get(cell_id)).toBe(test_cell); + assert.ok(app.cell_registry.all.has(cell_id)); + assert.ok(app.cell_registry.all.get(cell_id) === (test_cell as any)); }); test('dispose removes from registry', () => { @@ -146,13 +144,13 @@ describe('Cell registry lifecycle', () => { }); // Verify initial registration - expect(app.cell_registry.all.has(cell_id)).toBe(true); + assert.ok(app.cell_registry.all.has(cell_id)); // Dispose cell test_cell.dispose(); // Should be removed from registry - expect(app.cell_registry.all.has(cell_id)).toBe(false); + assert.ok(!app.cell_registry.all.has(cell_id)); }); test('dispose is safe to call multiple times', () => { @@ -167,10 +165,10 @@ describe('Cell registry lifecycle', () => { // First dispose test_cell.dispose(); - expect(app.cell_registry.all.has(cell_id)).toBe(false); + assert.ok(!app.cell_registry.all.has(cell_id)); // Second dispose should not throw - expect(() => test_cell.dispose()).not.toThrow(); + assert.doesNotThrow(() => test_cell.dispose()); }); }); @@ -201,11 +199,11 @@ describe('Cell id handling', () => { const initial_id = cell.id; // Verify initial state - expect(cell.id).toBe(initial_id); + assert.strictEqual(cell.id, initial_id); // Create a new id to set const new_id = create_uuid(); - expect(new_id).not.toBe(initial_id); + assert.notStrictEqual(new_id, initial_id); // Set new id through set_json cell.set_json({ @@ -216,8 +214,8 @@ describe('Cell id handling', () => { }); // Verify id was changed to the new value - expect(cell.id).toBe(new_id); - expect(cell.id).not.toBe(initial_id); + assert.strictEqual(cell.id, new_id); + assert.notStrictEqual(cell.id, initial_id); }); test('set_json_partial updates id when included in partial update', () => { @@ -235,11 +233,11 @@ describe('Cell id handling', () => { }); // Verify id was updated and other properties preserved - expect(cell.id).toBe(new_id); - expect(cell.id).not.toBe(initial_id); - expect(cell.type).toBe('test'); - expect(cell.content).toBe(''); - expect(cell.version).toBe(3); + assert.strictEqual(cell.id, new_id); + assert.notStrictEqual(cell.id, initial_id); + assert.strictEqual(cell.type, 'test'); + assert.strictEqual(cell.content, ''); + assert.strictEqual(cell.version, 3); }); test('set_json_partial preserves id when not included in partial update', () => { @@ -254,9 +252,9 @@ describe('Cell id handling', () => { }); // Verify id preserved and content updated - expect(cell.id).toBe(initial_id); - expect(cell.content).toBe('Partial update content'); - expect(cell.content).not.toBe(initial_content); + assert.strictEqual(cell.id, initial_id); + assert.strictEqual(cell.content, 'Partial update content'); + assert.notStrictEqual(cell.content, initial_content); }); test('schema validation rejects invalid id formats', () => { @@ -264,11 +262,11 @@ describe('Cell id handling', () => { const cell = new IdTestCell({app}); // Attempt to set invalid id - expect(() => { + assert.throws(() => { cell.set_json_partial({ id: 'not-a-valid-uuid' as any, }); - }).toThrow(); + }); }); test('clone creates a new id instead of copying the original', () => { @@ -287,13 +285,13 @@ describe('Cell id handling', () => { const cloned_cell = cell.clone(); // Verify clone has new id but same content - expect(cloned_cell.id).not.toBe(original_id); - expect(cloned_cell.content).toBe('Original content'); - expect(cloned_cell.version).toBe(1); + assert.notStrictEqual(cloned_cell.id, original_id); + assert.strictEqual(cloned_cell.content, 'Original content'); + assert.strictEqual(cloned_cell.version, 1); // Verify changing clone doesn't affect original cloned_cell.content = 'Changed in clone'; - expect(cell.content).toBe('Original content'); + assert.strictEqual(cell.content, 'Original content'); }); }); @@ -312,11 +310,11 @@ describe('Cell serialization', () => { const json = test_cell.to_json(); - expect(json.id).toBe(TEST_ID); - expect(json.created).toBe(TEST_DATETIME); - expect(json.text).toBe('JSON Test'); - expect(json.number).toBe(100); - expect(json.items).toEqual(['value1', 'value2']); + assert.strictEqual(json.id, TEST_ID); + assert.strictEqual(json.created, TEST_DATETIME); + assert.strictEqual(json.text, 'JSON Test'); + assert.strictEqual(json.number, 100); + assert.deepEqual(json.items, ['value1', 'value2']); }); test('toJSON method works with JSON.stringify', () => { @@ -331,8 +329,8 @@ describe('Cell serialization', () => { const stringified = JSON.stringify(test_cell); const parsed = JSON.parse(stringified); - expect(parsed.id).toBe(TEST_ID); - expect(parsed.text).toBe('Stringify Test'); + assert.strictEqual(parsed.id, TEST_ID); + assert.strictEqual(parsed.text, 'Stringify Test'); }); test('derived json properties update when cell changes', () => { @@ -346,21 +344,21 @@ describe('Cell serialization', () => { }); // Check initial values - expect(test_cell.json.text).toBe('Initial'); - expect(test_cell.json.number).toBe(10); + assert.strictEqual(test_cell.json.text, 'Initial'); + assert.strictEqual(test_cell.json.number, 10); // Update values test_cell.text = 'Updated'; test_cell.number = 20; // Check derived properties updated - expect(test_cell.json.text).toBe('Updated'); - expect(test_cell.json.number).toBe(20); + assert.strictEqual(test_cell.json.text, 'Updated'); + assert.strictEqual(test_cell.json.number, 20); // Check derived serialized JSON const parsed = JSON.parse(test_cell.json_serialized); - expect(parsed.text).toBe('Updated'); - expect(parsed.number).toBe(20); + assert.strictEqual(parsed.text, 'Updated'); + assert.strictEqual(parsed.number, 20); }); }); @@ -381,17 +379,17 @@ describe('Cell modification methods', () => { items: ['new1', 'new2'], }); - expect(test_cell.text).toBe('Updated via set_json'); - expect(test_cell.number).toBe(50); - expect(test_cell.items).toEqual(['new1', 'new2']); - expect(test_cell.id).not.toBe(TEST_ID); // id should be new + assert.strictEqual(test_cell.text, 'Updated via set_json'); + assert.strictEqual(test_cell.number, 50); + assert.deepEqual(test_cell.items, ['new1', 'new2']); + assert.notStrictEqual(test_cell.id, TEST_ID); // id should be new }); test('set_json rejects invalid data', () => { const test_cell = new BasicTestCell({app}); // Should reject invalid data with a schema error - expect(() => test_cell.set_json({number: 'not a number' as any})).toThrow(); + assert.throws(() => test_cell.set_json({number: 'not a number' as any})); }); test('set_json_partial updates only specified properties', () => { @@ -413,13 +411,13 @@ describe('Cell modification methods', () => { }); // Verify updated properties - expect(test_cell.text).toBe('Updated text'); - expect(test_cell.number).toBe(20); + assert.strictEqual(test_cell.text, 'Updated text'); + assert.strictEqual(test_cell.number, 20); // Verify untouched properties - expect(test_cell.items).toEqual(['item1', 'item2']); - expect(test_cell.flag).toBe(true); - expect(test_cell.id).toBe(TEST_ID); + assert.deepEqual(test_cell.items, ['item1', 'item2']); + assert.ok(test_cell.flag); + assert.strictEqual(test_cell.id, TEST_ID); }); test('set_json_partial handles null or undefined input', () => { @@ -432,12 +430,12 @@ describe('Cell modification methods', () => { }); // These should not throw errors - expect(() => test_cell.set_json_partial(null!)).not.toThrow(); - expect(() => test_cell.set_json_partial(undefined!)).not.toThrow(); + assert.doesNotThrow(() => test_cell.set_json_partial(null!)); + assert.doesNotThrow(() => test_cell.set_json_partial(undefined!)); // Properties should remain unchanged - expect(test_cell.id).toBe(TEST_ID); - expect(test_cell.text).toBe('Initial'); + assert.strictEqual(test_cell.id, TEST_ID); + assert.strictEqual(test_cell.text, 'Initial'); }); test('set_json_partial validates merged data against schema', () => { @@ -450,10 +448,10 @@ describe('Cell modification methods', () => { }); // Should reject invalid data with a schema error - expect(() => test_cell.set_json_partial({number: 'not a number' as any})).toThrow(); + assert.throws(() => test_cell.set_json_partial({number: 'not a number' as any})); // Original values should remain unchanged after failed update - expect(test_cell.text).toBe('Initial'); + assert.strictEqual(test_cell.text, 'Initial'); }); }); @@ -473,17 +471,17 @@ describe('Cell date formatting', () => { }); // Verify date objects - expect(test_cell.created_date).toBeInstanceOf(Date); - expect(test_cell.updated_date).toBeInstanceOf(Date); + assert.instanceOf(test_cell.created_date, Date); + assert.instanceOf(test_cell.updated_date, Date); // Verify formatted strings exist - expect(test_cell.created_formatted_short_date).not.toBeNull(); - expect(test_cell.created_formatted_datetime).not.toBeNull(); - expect(test_cell.created_formatted_time).not.toBeNull(); + assert.ok(test_cell.created_formatted_short_date); + assert.ok(test_cell.created_formatted_datetime); + assert.ok(test_cell.created_formatted_time); - expect(test_cell.updated_formatted_short_date).not.toBeNull(); - expect(test_cell.updated_formatted_datetime).not.toBeNull(); - expect(test_cell.updated_formatted_time).not.toBeNull(); + assert.ok(test_cell.updated_formatted_short_date); + assert.ok(test_cell.updated_formatted_datetime); + assert.ok(test_cell.updated_formatted_time); }); test('handles null updated date', () => { @@ -496,10 +494,10 @@ describe('Cell date formatting', () => { }, }); - expect(test_cell.updated_date).not.toBeNull(); - expect(test_cell.updated_formatted_short_date).not.toBeNull(); - expect(test_cell.updated_formatted_datetime).not.toBeNull(); - expect(test_cell.updated_formatted_time).not.toBeNull(); + assert.ok(test_cell.updated_date); + assert.ok(test_cell.updated_formatted_short_date); + assert.ok(test_cell.updated_formatted_datetime); + assert.ok(test_cell.updated_formatted_time); }); }); @@ -518,22 +516,22 @@ describe('Cell cloning', () => { const clone = original.clone(); // Should have same values - expect(clone.text).toBe('Original'); - expect(clone.number).toBe(42); - expect(clone.items).toEqual(['value1']); + assert.strictEqual(clone.text, 'Original'); + assert.strictEqual(clone.number, 42); + assert.deepEqual(clone.items, ['value1']); // But be a different instance - expect(clone).not.toBe(original); - expect(clone.id).not.toBe(original.id); // Should have new id + assert.notStrictEqual(clone, original); + assert.notStrictEqual(clone.id, original.id); // Should have new id // Changes to one shouldn't affect the other clone.text = 'Changed'; clone.number = 100; clone.items.push('value2'); - expect(original.text).toBe('Original'); - expect(original.number).toBe(42); - expect(original.items).toEqual(['value1']); + assert.strictEqual(original.text, 'Original'); + assert.strictEqual(original.number, 42); + assert.deepEqual(original.items, ['value1']); }); test('clone registers new instance in registry', () => { @@ -547,9 +545,9 @@ describe('Cell cloning', () => { const clone = original.clone(); // Both instances should be registered - expect(app.cell_registry.all.has(original.id)).toBe(true); - expect(app.cell_registry.all.has(clone.id)).toBe(true); - expect(app.cell_registry.all.get(clone.id)).toBe(clone); + assert.ok(app.cell_registry.all.has(original.id)); + assert.ok(app.cell_registry.all.has(clone.id)); + assert.ok(app.cell_registry.all.get(clone.id) === (clone as any)); }); }); @@ -564,10 +562,10 @@ describe('Schema validation', () => { }); // Initial state should be valid - expect(test_cell.json_parsed.success).toBe(true); + assert.ok(test_cell.json_parsed.success); // Invalid initialization should throw - expect( + assert.throws( () => new BasicTestCell({ app, @@ -576,6 +574,6 @@ describe('Schema validation', () => { text: 123 as any, }, }), - ).toThrow(); + ); }); }); diff --git a/src/test/cell.svelte.decoders.test.ts b/src/test/cell.svelte.decoders.test.ts index c5b91addf..16b07138f 100644 --- a/src/test/cell.svelte.decoders.test.ts +++ b/src/test/cell.svelte.decoders.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, vi, beforeEach} from 'vitest'; +import {test, vi, beforeEach, assert} from 'vitest'; import {z} from 'zod'; import {Cell, type CellOptions} from '$lib/cell.svelte.js'; @@ -60,7 +58,7 @@ test('Cell allows schema keys with no properties if a decoder is provided', () = }, }); - expect(cell.captured_value).toBe(99); + assert.strictEqual(cell.captured_value, 99); }); test('Cell supports virtual properties with custom handling', () => { @@ -104,9 +102,9 @@ test('Cell supports virtual properties with custom handling', () => { }, }); - expect(cell.visible_prop).toBe('visible'); - expect('hidden_prop' in cell).toBe(false); - expect(cell.processed_value).toBe(84); // 42 * 2 + assert.strictEqual(cell.visible_prop, 'visible'); + assert.ok(!('hidden_prop' in cell)); + assert.strictEqual(cell.processed_value, 84); // 42 * 2 }); test('Cell handles sentinel values with proper precedence', () => { @@ -159,13 +157,13 @@ test('Cell handles sentinel values with proper precedence', () => { }, }); - expect(cell.decoder_calls).toContain('handled_field_called'); - expect(cell.decoder_calls).toContain('default_field_called'); - expect(cell.decoder_calls).toContain('normal_field_called'); + assert.include(cell.decoder_calls, 'handled_field_called'); + assert.include(cell.decoder_calls, 'default_field_called'); + assert.include(cell.decoder_calls, 'normal_field_called'); - expect(cell.handled_field).toBe('initial_value'); - expect(cell.default_field).toBe(42); - expect(cell.normal_field).toBe(true); + assert.strictEqual(cell.handled_field, 'initial_value'); + assert.strictEqual(cell.default_field, 42); + assert.ok(cell.normal_field); }); test('Cell parser defaults take precedence over schema defaults', () => { @@ -201,6 +199,6 @@ test('Cell parser defaults take precedence over schema defaults', () => { json: {}, }); - expect(cell.id).toBe('parser_default_id'); - expect(cell.text).toBe('schema_default_text'); + assert.strictEqual(cell.id, 'parser_default_id'); + assert.strictEqual(cell.text, 'schema_default_text'); }); diff --git a/src/test/cell.svelte.inheritance.test.ts b/src/test/cell.svelte.inheritance.test.ts index 4230520f4..4bcc50685 100644 --- a/src/test/cell.svelte.inheritance.test.ts +++ b/src/test/cell.svelte.inheritance.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, vi, beforeEach} from 'vitest'; +import {test, vi, beforeEach, assert} from 'vitest'; import {z} from 'zod'; import {Cell, type CellOptions} from '$lib/cell.svelte.js'; @@ -75,10 +73,10 @@ test('Cell supports overriding assign_property', () => { }, }); - expect(cell.text).toBe('modified_original'); - expect(cell.assignment_log).toContain(`Assigned id: ${TEST_ID}`); - expect(cell.assignment_log).toContain('Assigned text: original'); - expect(cell.assignment_log).toContain('Assigned list: item'); + assert.strictEqual(cell.text, 'modified_original'); + assert.include(cell.assignment_log, `Assigned id: ${TEST_ID}`); + assert.include(cell.assignment_log, 'Assigned text: original'); + assert.include(cell.assignment_log, 'Assigned list: item'); }); test('Cell assign_property returns after handling property correctly', () => { @@ -122,15 +120,15 @@ test('Cell assign_property returns after handling property correctly', () => { }, }); - expect(cell.text).toBe('sample'); - expect(cell.number).toBe(42); + assert.strictEqual(cell.text, 'sample'); + assert.strictEqual(cell.number, 42); - expect(cell.execution_path).toContain('begin-text'); - expect(cell.execution_path).toContain('complete-text'); - expect(cell.execution_path).not.toContain('continue-text'); + assert.include(cell.execution_path, 'begin-text'); + assert.include(cell.execution_path, 'complete-text'); + assert.notInclude(cell.execution_path, 'continue-text'); - expect(cell.execution_path).toContain('begin-number'); - expect(cell.execution_path).toContain('continue-number'); + assert.include(cell.execution_path, 'begin-number'); + assert.include(cell.execution_path, 'continue-number'); }); test('Cell handles inherited properties correctly', () => { @@ -178,19 +176,19 @@ test('Cell handles inherited properties correctly', () => { }, }); - expect(cell.id).toBe(TEST_ID); - expect(cell.text).toBe('base_property'); - expect(cell.number).toBe(30); + assert.strictEqual(cell.id, TEST_ID); + assert.strictEqual(cell.text, 'base_property'); + assert.strictEqual(cell.number, 30); - expect(cell.derived_method()).toBe('derived_result'); - expect(cell.base_method()).toBe('overridden_result'); + assert.strictEqual(cell.derived_method(), 'derived_result'); + assert.strictEqual(cell.base_method(), 'overridden_result'); - expect('id' in cell).toBe(true); + assert.ok('id' in cell); const json = cell.json; - expect(json.id).toBe(TEST_ID); - expect(json.text).toBe('base_property'); - expect(json.number).toBe(30); + assert.strictEqual(json.id, TEST_ID); + assert.strictEqual(json.text, 'base_property'); + assert.strictEqual(json.number, 30); }); test('Cell properly handles collections with HANDLED sentinel', () => { @@ -242,8 +240,8 @@ test('Cell properly handles collections with HANDLED sentinel', () => { }, }); - expect(cell.stored_items).toEqual(['ONE', 'TWO', 'THREE']); - expect(cell.json.collection).toEqual(['ONE', 'TWO', 'THREE']); + assert.deepEqual(cell.stored_items, ['ONE', 'TWO', 'THREE']); + assert.deepEqual(cell.json.collection, ['ONE', 'TWO', 'THREE']); }); test('Cell registration and unregistration works correctly', () => { @@ -270,13 +268,13 @@ test('Cell registration and unregistration works correctly', () => { }); // Cell should be automatically registered - expect(app.cell_registry.all.has(cell_id)).toBe(true); - expect(app.cell_registry.all.get(cell_id)).toBe(cell); + assert.ok(app.cell_registry.all.has(cell_id)); + assert.ok(app.cell_registry.all.get(cell_id) === (cell as any)); // Test disposal cell.dispose(); - expect(app.cell_registry.all.has(cell_id)).toBe(false); + assert.ok(!app.cell_registry.all.has(cell_id)); // Test that disposing again is safe - expect(() => cell.dispose()).not.toThrow(); + assert.doesNotThrow(() => cell.dispose()); }); diff --git a/src/test/cell.svelte.special_types.test.ts b/src/test/cell.svelte.special_types.test.ts index ce1e831f5..d37d72efe 100644 --- a/src/test/cell.svelte.special_types.test.ts +++ b/src/test/cell.svelte.special_types.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, vi, beforeEach} from 'vitest'; +import {test, vi, beforeEach, assert} from 'vitest'; import {z} from 'zod'; import {Cell, type CellOptions} from '$lib/cell.svelte.js'; @@ -64,8 +62,9 @@ test('Cell uses registry for instantiating class relationships', () => { const test_object = {key: 'value'}; const result = cell.test_instantiate(test_object, 'TestType'); - expect(mock_instantiate).toHaveBeenCalledWith('TestType', test_object); - expect(result).toEqual({type: 'TestType', key: 'value'}); + assert.ok(mock_instantiate.mock.calls.length > 0); + assert.deepEqual(mock_instantiate.mock.calls[0], ['TestType', test_object] as any); + assert.deepEqual(result, {type: 'TestType', key: 'value'}); // Clean up mock_instantiate.mockRestore(); @@ -100,13 +99,13 @@ test('Cell.encode_property uses $state.snapshot for values', () => { // Test with Date object const test_date = new Date(`${TEST_YEAR}-01-15`); const encoded_date = cell.test_encode(test_date, 'date_field'); - expect(encoded_date instanceof Date).toBe(true); - expect((encoded_date as Date).getFullYear()).toBe(TEST_YEAR); + assert.ok(encoded_date instanceof Date); + assert.strictEqual(encoded_date.getFullYear(), TEST_YEAR); // Test with nested object const test_object = {outer: {inner: 42}}; const encoded_object = cell.test_encode(test_object, 'object_field'); - expect(encoded_object).toEqual(test_object); + assert.deepEqual(encoded_object, test_object); }); test('Cell handles special types like Map and Set', () => { @@ -160,25 +159,25 @@ test('Cell handles special types like Map and Set', () => { }); // Verify Map handling - expect(cell.map_field).toBeInstanceOf(Map); - expect(cell.map_field.get('key1')).toBe(1); - expect(cell.map_field.get('key2')).toBe(2); + assert.instanceOf(cell.map_field, Map); + assert.strictEqual(cell.map_field.get('key1'), 1); + assert.strictEqual(cell.map_field.get('key2'), 2); // Verify Set handling - expect(cell.set_field).toBeInstanceOf(Set); - expect(cell.set_field.has('item1')).toBe(true); - expect(cell.set_field.has('item2')).toBe(true); - expect(cell.set_field.has('item3')).toBe(true); + assert.instanceOf(cell.set_field, Set); + assert.ok(cell.set_field.has('item1')); + assert.ok(cell.set_field.has('item2')); + assert.ok(cell.set_field.has('item3')); // Test manual decoding const map_result = cell.test_decode([['key3', 3]], 'map_field'); - expect(map_result).toBeInstanceOf(Map); - expect(map_result.get('key3')).toBe(3); + assert.instanceOf(map_result, Map); + assert.strictEqual(map_result.get('key3'), 3); const set_result = cell.test_decode(['item4', 'item5'], 'set_field'); - expect(set_result).toBeInstanceOf(Set); - expect(set_result.has('item4')).toBe(true); - expect(set_result.has('item5')).toBe(true); + assert.instanceOf(set_result, Set); + assert.ok(set_result.has('item4')); + assert.ok(set_result.has('item5')); }); test('Cell - JSON serialization excludes undefined values correctly', () => { @@ -233,32 +232,32 @@ test('Cell - JSON serialization excludes undefined values correctly', () => { // Test minimal cell serialization const minimal_json = minimal_cell.to_json(); - expect(minimal_json.type).toBe('type1'); - expect(minimal_json.name).toBeUndefined(); - expect(minimal_json.data).toBeUndefined(); - expect(minimal_json.items).toBeUndefined(); - expect(minimal_json.state).toBeUndefined(); + assert.strictEqual(minimal_json.type, 'type1'); + assert.ok(minimal_json.name === undefined); + assert.ok(minimal_json.data === undefined); + assert.ok(minimal_json.items === undefined); + assert.ok(minimal_json.state === undefined); // Test complete cell serialization const complete_json = complete_cell.to_json(); - expect(complete_json.type).toBe('type2'); - expect(complete_json.name).toBe('test_name'); - expect(complete_json.data).toEqual({code: 'test_code'}); - expect(complete_json.data?.value).toBeUndefined(); - expect(complete_json.items).toEqual(['item1', 'item2']); - expect(complete_json.state).toBeUndefined(); + assert.strictEqual(complete_json.type, 'type2'); + assert.strictEqual(complete_json.name, 'test_name'); + assert.deepEqual(complete_json.data, {code: 'test_code'}); + assert.ok(complete_json.data?.value === undefined); + assert.deepEqual(complete_json.items, ['item1', 'item2']); + assert.ok(complete_json.state === undefined); // Test JSON stringification const minimal_string = JSON.stringify(minimal_cell); const parsed_minimal = JSON.parse(minimal_string); - expect(parsed_minimal.name).toBeUndefined(); - expect(parsed_minimal.data).toBeUndefined(); - expect(parsed_minimal.items).toBeUndefined(); - expect(parsed_minimal.state).toBeUndefined(); + assert.ok(parsed_minimal.name === undefined); + assert.ok(parsed_minimal.data === undefined); + assert.ok(parsed_minimal.items === undefined); + assert.ok(parsed_minimal.state === undefined); // Test nested property handling const complete_string = JSON.stringify(complete_cell); const parsed_complete = JSON.parse(complete_string); - expect(parsed_complete.data.code).toBe('test_code'); - expect('value' in parsed_complete.data).toBe(false); + assert.strictEqual(parsed_complete.data.code, 'test_code'); + assert.ok(!('value' in parsed_complete.data)); }); diff --git a/src/test/cell_helpers.test.ts b/src/test/cell_helpers.test.ts index 64290dc2d..a95cc201d 100644 --- a/src/test/cell_helpers.test.ts +++ b/src/test/cell_helpers.test.ts @@ -1,14 +1,12 @@ -// @slop Claude Sonnet 3.7 - -import {describe, test, expect} from 'vitest'; +import {describe, test, assert} from 'vitest'; import {z} from 'zod'; import {get_schema_class_info} from '$lib/cell_helpers.js'; describe('get_schema_class_info', () => { test('handles null or undefined schemas', () => { - expect(get_schema_class_info(null)).toBeNull(); - expect(get_schema_class_info(undefined)).toBeNull(); + assert.isNull(get_schema_class_info(null)); + assert.isNull(get_schema_class_info(undefined)); }); test('identifies basic schema types correctly', () => { @@ -20,14 +18,14 @@ describe('get_schema_class_info', () => { const number_info = get_schema_class_info(number_schema); const boolean_info = get_schema_class_info(boolean_schema); - expect(string_info?.type).toBe('ZodString'); - expect(string_info?.is_array).toBe(false); + assert.strictEqual(string_info?.type, 'ZodString'); + assert.ok(!string_info?.is_array); - expect(number_info?.type).toBe('ZodNumber'); - expect(number_info?.is_array).toBe(false); + assert.strictEqual(number_info?.type, 'ZodNumber'); + assert.ok(!number_info?.is_array); - expect(boolean_info?.type).toBe('ZodBoolean'); - expect(boolean_info?.is_array).toBe(false); + assert.strictEqual(boolean_info?.type, 'ZodBoolean'); + assert.ok(!boolean_info?.is_array); }); test('identifies array schemas correctly', () => { @@ -40,14 +38,14 @@ describe('get_schema_class_info', () => { const object_array_info = get_schema_class_info(object_array); // Test array identification - expect(string_array_info?.type).toBe('ZodArray'); - expect(string_array_info?.is_array).toBe(true); + assert.strictEqual(string_array_info?.type, 'ZodArray'); + assert.ok(string_array_info?.is_array); - expect(number_array_info?.type).toBe('ZodArray'); - expect(number_array_info?.is_array).toBe(true); + assert.strictEqual(number_array_info?.type, 'ZodArray'); + assert.ok(number_array_info?.is_array); - expect(object_array_info?.type).toBe('ZodArray'); - expect(object_array_info?.is_array).toBe(true); + assert.strictEqual(object_array_info?.type, 'ZodArray'); + assert.ok(object_array_info?.is_array); }); test('handles default wrapped schemas', () => { @@ -58,12 +56,12 @@ describe('get_schema_class_info', () => { const array_default_info = get_schema_class_info(array_with_default); // Default shouldn't change the core type - expect(string_default_info?.type).toBe('ZodString'); - expect(string_default_info?.is_array).toBe(false); + assert.strictEqual(string_default_info?.type, 'ZodString'); + assert.ok(!string_default_info?.is_array); // This is what's failing in the test - default-wrapped arrays should still be identified as arrays - expect(array_default_info?.type).toBe('ZodArray'); - expect(array_default_info?.is_array).toBe(true); + assert.strictEqual(array_default_info?.type, 'ZodArray'); + assert.ok(array_default_info?.is_array); }); test('handles object schemas', () => { @@ -73,8 +71,8 @@ describe('get_schema_class_info', () => { }); const object_info = get_schema_class_info(object_schema); - expect(object_info?.type).toBe('ZodObject'); - expect(object_info?.is_array).toBe(false); + assert.strictEqual(object_info?.type, 'ZodObject'); + assert.ok(!object_info?.is_array); }); test('detects class names set with cell_class', () => { @@ -82,7 +80,7 @@ describe('get_schema_class_info', () => { const schema_with_class = schema.meta({cell_class_name: 'TestClass'}); const info = get_schema_class_info(schema_with_class); - expect(info?.class_name).toBe('TestClass'); + assert.strictEqual(info?.class_name, 'TestClass'); }); test('detects element classes from element metadata', () => { @@ -90,8 +88,8 @@ describe('get_schema_class_info', () => { const array_schema = z.array(element_schema); const info = get_schema_class_info(array_schema); - expect(info?.is_array).toBe(true); - expect(info?.element_class).toBe('ElementClass'); + assert.ok(info?.is_array); + assert.strictEqual(info.element_class, 'ElementClass'); }); test('handles default-wrapped array with element metadata', () => { @@ -99,8 +97,8 @@ describe('get_schema_class_info', () => { const array_schema = z.array(element_schema).default([]); const info = get_schema_class_info(array_schema); - expect(info?.is_array).toBe(true); - expect(info?.element_class).toBe('ElementClass'); + assert.ok(info?.is_array); + assert.strictEqual(info.element_class, 'ElementClass'); }); test('reads element class from nested element schema', () => { @@ -112,8 +110,8 @@ describe('get_schema_class_info', () => { // Verify that get_schema_class_info can read element metadata const info = get_schema_class_info(array_schema); - expect(info?.is_array).toBe(true); - expect(info?.element_class).toBe('DirectElementClass'); + assert.ok(info?.is_array); + assert.strictEqual(info.element_class, 'DirectElementClass'); }); test('handles ZodDefault containing a ZodArray', () => { @@ -122,17 +120,17 @@ describe('get_schema_class_info', () => { const array_schema_default = array_schema.default([]); // We can see what the internal structure of ZodDefault looks like - expect(array_schema_default._zod.def).toBeDefined(); - expect(array_schema_default._zod.def.type).toBe('default'); - expect(array_schema_default._zod.def.innerType).toBeDefined(); - expect(array_schema_default._zod.def.innerType.def.type).toBe('array'); + assert.isDefined(array_schema_default._zod.def); + assert.strictEqual(array_schema_default._zod.def.type, 'default'); + assert.isDefined(array_schema_default._zod.def.innerType); + assert.strictEqual(array_schema_default._zod.def.innerType.def.type, 'array'); // Now test the function with our default-wrapped array const info = get_schema_class_info(array_schema_default); // The function should see through the ZodDefault to the ZodArray inside - expect(info?.type).toBe('ZodArray'); - expect(info?.is_array).toBe(true); + assert.strictEqual(info?.type, 'ZodArray'); + assert.ok(info?.is_array); }); test('handles complex nested schema wrapping', () => { @@ -140,15 +138,15 @@ describe('get_schema_class_info', () => { const nested_array_schema = z.array(z.string()).optional().default([]); const nested_info = get_schema_class_info(nested_array_schema); - expect(nested_info?.type).toBe('ZodArray'); - expect(nested_info?.is_array).toBe(true); + assert.strictEqual(nested_info?.type, 'ZodArray'); + assert.ok(nested_info?.is_array); // More extreme nesting: ZodDefault -> ZodOptional -> ZodDefault -> ZodArray const extreme_nesting = z.array(z.number()).default([]).optional().default([]); const extreme_info = get_schema_class_info(extreme_nesting); - expect(extreme_info?.type).toBe('ZodArray'); - expect(extreme_info?.is_array).toBe(true); + assert.strictEqual(extreme_info?.type, 'ZodArray'); + assert.ok(extreme_info?.is_array); }); test('handles ZodEffects wrapping arrays', () => { @@ -158,8 +156,8 @@ describe('get_schema_class_info', () => { .refine((arr) => arr.length > 0, {message: 'Array must not be empty'}); const refined_info = get_schema_class_info(refined_array); - expect(refined_info?.type).toBe('ZodArray'); - expect(refined_info?.is_array).toBe(true); + assert.strictEqual(refined_info?.type, 'ZodArray'); + assert.ok(refined_info?.is_array); // ZodEffects (transform) wrapping an array with default const transformed_array = z @@ -168,8 +166,8 @@ describe('get_schema_class_info', () => { .transform((arr) => arr.map((n) => n * 2)); const transformed_info = get_schema_class_info(transformed_array); - expect(transformed_info?.type).toBe('ZodArray'); - expect(transformed_info?.is_array).toBe(true); + assert.strictEqual(transformed_info?.type, 'ZodArray'); + assert.ok(transformed_info?.is_array); }); test('handles combinations of optional, default, and refinement', () => { @@ -182,8 +180,8 @@ describe('get_schema_class_info', () => { .optional(); const chain_info = get_schema_class_info(complex_chain); - expect(chain_info?.type).toBe('ZodArray'); - expect(chain_info?.is_array).toBe(true); + assert.strictEqual(chain_info?.type, 'ZodArray'); + assert.ok(chain_info?.is_array); }); test('recursive unwrapping preserves metadata through wrappers', () => { @@ -196,8 +194,8 @@ describe('get_schema_class_info', () => { // Check that metadata is preserved const info = get_schema_class_info(wrapped_array); - expect(info?.element_class).toBe('TestElement'); - expect(info?.is_array).toBe(true); + assert.strictEqual(info?.element_class, 'TestElement'); + assert.ok(info?.is_array); }); test('handles deeply nested schemas with element metadata', () => { @@ -207,8 +205,8 @@ describe('get_schema_class_info', () => { // Verify metadata is found correctly through the wrappers const info = get_schema_class_info(nested_schema); - expect(info?.is_array).toBe(true); - expect(info?.element_class).toBe('NestedElement'); + assert.ok(info?.is_array); + assert.strictEqual(info.element_class, 'NestedElement'); }); }); @@ -218,13 +216,13 @@ describe('cell_class', () => { const result = schema.meta({cell_class_name: 'TestCellClass'}); // Should add the metadata via .meta() - expect(result.meta()?.cell_class_name).toBe('TestCellClass'); + assert.strictEqual(result.meta()?.cell_class_name, 'TestCellClass'); // Should return a new schema instance (due to .meta() creating a new instance) - expect(result).not.toBe(schema); + assert.notStrictEqual(result, schema); // Get schema info should report it correctly const info = get_schema_class_info(result); - expect(info?.class_name).toBe('TestCellClass'); + assert.strictEqual(info?.class_name, 'TestCellClass'); }); }); diff --git a/src/test/codegen.test.ts b/src/test/codegen.test.ts index 80acb42b5..aa978c256 100644 --- a/src/test/codegen.test.ts +++ b/src/test/codegen.test.ts @@ -1,15 +1,13 @@ -// @slop Claude Opus 4 - // @vitest-environment jsdom -import {test, expect, describe} from 'vitest'; - +import {test, describe, assert} from 'vitest'; import { ImportBuilder, get_executor_phases, get_handler_return_type, generate_phase_handlers, -} from '$lib/codegen.js'; +} from '@fuzdev/fuz_app/actions/action_codegen.js'; + import { ping_action_spec, session_load_action_spec, @@ -26,7 +24,7 @@ describe('ImportBuilder', () => { imports.add_type('$lib/types.js', 'Foo'); imports.add_type('$lib/types.js', 'Bar'); - expect(imports.build()).toBe(`import type {Bar, Foo} from '$lib/types.js';`); + assert.strictEqual(imports.build(), `import type {Bar, Foo} from '$lib/types.js';`); }); test('add_types helper adds multiple types at once', () => { @@ -34,15 +32,18 @@ describe('ImportBuilder', () => { imports.add_types('$lib/types.js', 'TypeA', 'TypeB', 'TypeC'); - expect(imports.build()).toBe(`import type {TypeA, TypeB, TypeC} from '$lib/types.js';`); + assert.strictEqual( + imports.build(), + `import type {TypeA, TypeB, TypeC} from '$lib/types.js';`, + ); }); test('empty imports returns empty string', () => { const imports = new ImportBuilder(); - expect(imports.build()).toBe(''); - expect(imports.has_imports()).toBe(false); - expect(imports.import_count).toBe(0); + assert.strictEqual(imports.build(), ''); + assert.ok(!imports.has_imports()); + assert.strictEqual(imports.import_count, 0); }); }); @@ -54,7 +55,8 @@ describe('ImportBuilder', () => { imports.add_type('$lib/utils.js', 'HelperType'); imports.add('$lib/utils.js', 'another_helper'); - expect(imports.build()).toBe( + assert.strictEqual( + imports.build(), `import {another_helper, helper, type HelperType} from '$lib/utils.js';`, ); }); @@ -67,7 +69,8 @@ describe('ImportBuilder', () => { imports.add('$lib/mixed.js', 'value'); // This makes it mixed imports.add_type('$lib/mixed.js', 'TypeC'); - expect(imports.build()).toBe( + assert.strictEqual( + imports.build(), `import {value, type TypeA, type TypeB, type TypeC} from '$lib/mixed.js';`, ); }); @@ -84,7 +87,8 @@ describe('ImportBuilder', () => { imports.add('$lib/mixed.js', 'm_value'); // Should sort values first (alphabetically), then types (alphabetically) - expect(imports.build()).toBe( + assert.strictEqual( + imports.build(), `import {a_value, m_value, z_value, type AType, type MType, type ZType} from '$lib/mixed.js';`, ); }); @@ -96,7 +100,7 @@ describe('ImportBuilder', () => { imports.add('$lib/action_specs.js', '* as specs'); - expect(imports.build()).toBe(`import * as specs from '$lib/action_specs.js';`); + assert.strictEqual(imports.build(), `import * as specs from '$lib/action_specs.js';`); }); test('namespace import with other imports from same module', () => { @@ -108,9 +112,9 @@ describe('ImportBuilder', () => { const result = imports.build(); const lines = result.split('\n'); - expect(lines).toHaveLength(2); - expect(lines).toContain(`import * as utils from '$lib/utils.js';`); - expect(lines).toContain(`import {something} from '$lib/other.js';`); + assert.strictEqual(lines.length, 2); + assert.include(lines, `import * as utils from '$lib/utils.js';`); + assert.include(lines, `import {something} from '$lib/other.js';`); }); test('add_many with namespace import', () => { @@ -118,7 +122,7 @@ describe('ImportBuilder', () => { imports.add_many('$lib/helpers.js', '* as helpers'); - expect(imports.build()).toBe(`import * as helpers from '$lib/helpers.js';`); + assert.strictEqual(imports.build(), `import * as helpers from '$lib/helpers.js';`); }); test('namespace imports are not mixed with regular imports', () => { @@ -129,7 +133,7 @@ describe('ImportBuilder', () => { imports.add('$lib/module.js', 'specific'); // Namespace imports should be on their own line - expect(imports.build()).toBe(`import * as mod from '$lib/module.js';`); + assert.strictEqual(imports.build(), `import * as mod from '$lib/module.js';`); }); }); @@ -140,7 +144,7 @@ describe('ImportBuilder', () => { imports.add_type('$lib/utils.js', 'Item'); imports.add('$lib/utils.js', 'Item'); // Upgrades to value - expect(imports.build()).toBe(`import {Item} from '$lib/utils.js';`); + assert.strictEqual(imports.build(), `import {Item} from '$lib/utils.js';`); }); test('type import does not downgrade existing value import', () => { @@ -149,7 +153,7 @@ describe('ImportBuilder', () => { imports.add('$lib/utils.js', 'Item'); imports.add_type('$lib/utils.js', 'Item'); // Should not downgrade - expect(imports.build()).toBe(`import {Item} from '$lib/utils.js';`); + assert.strictEqual(imports.build(), `import {Item} from '$lib/utils.js';`); }); test('duplicate imports are deduplicated', () => { @@ -159,7 +163,7 @@ describe('ImportBuilder', () => { imports.add_type('$lib/types.js', 'Foo'); imports.add_type('$lib/types.js', 'Foo'); - expect(imports.build()).toBe(`import type {Foo} from '$lib/types.js';`); + assert.strictEqual(imports.build(), `import type {Foo} from '$lib/types.js';`); }); test('namespace imports override previous imports', () => { @@ -168,7 +172,7 @@ describe('ImportBuilder', () => { imports.add('$lib/module.js', 'foo'); imports.add('$lib/module.js', '* as module'); // Should override - expect(imports.build()).toBe(`import * as module from '$lib/module.js';`); + assert.strictEqual(imports.build(), `import * as module from '$lib/module.js';`); }); }); @@ -183,10 +187,10 @@ describe('ImportBuilder', () => { const result = imports.build(); const lines = result.split('\n'); - expect(lines).toHaveLength(3); - expect(lines).toContain(`import type {TypeA, TypeB} from '$lib/types.js';`); - expect(lines).toContain(`import {util} from '$lib/utils.js';`); - expect(lines).toContain(`import type {SchemaA, SchemaB} from '$lib/schemas.js';`); + assert.strictEqual(lines.length, 3); + assert.include(lines, `import type {TypeA, TypeB} from '$lib/types.js';`); + assert.include(lines, `import {util} from '$lib/utils.js';`); + assert.include(lines, `import type {SchemaA, SchemaB} from '$lib/schemas.js';`); }); test('imports are sorted alphabetically within modules', () => { @@ -196,7 +200,10 @@ describe('ImportBuilder', () => { imports.add_type('$lib/types.js', 'Apple'); imports.add_type('$lib/types.js', 'Middle'); - expect(imports.build()).toBe(`import type {Apple, Middle, Zebra} from '$lib/types.js';`); + assert.strictEqual( + imports.build(), + `import type {Apple, Middle, Zebra} from '$lib/types.js';`, + ); }); test('handles imports with underscores and numbers correctly', () => { @@ -208,7 +215,8 @@ describe('ImportBuilder', () => { imports.add_type('$lib/types.js', 'PUBLIC_TYPE'); // Underscores sort before letters in most locales - expect(imports.build()).toBe( + assert.strictEqual( + imports.build(), `import type {PUBLIC_TYPE, Type_1, Type_2, _Private_Type} from '$lib/types.js';`, ); }); @@ -228,13 +236,13 @@ describe('ImportBuilder', () => { const lines = imports.preview(); // Module order should be based on insertion order - expect(lines[0]).toContain('$lib/third.js'); - expect(lines[1]).toContain('$lib/first.js'); - expect(lines[2]).toContain('$lib/second.js'); + assert.include(lines[0], '$lib/third.js'); + assert.include(lines[1], '$lib/first.js'); + assert.include(lines[2], '$lib/second.js'); // But items within modules are sorted - expect(lines[0]).toBe(`import type {Type3, Type3b} from '$lib/third.js';`); - expect(lines[1]).toBe(`import type {Type1, Type1b} from '$lib/first.js';`); + assert.strictEqual(lines[0], `import type {Type3, Type3b} from '$lib/third.js';`); + assert.strictEqual(lines[1], `import type {Type1, Type1b} from '$lib/first.js';`); }); test('handles mixed namespace and regular imports across modules', () => { @@ -247,11 +255,11 @@ describe('ImportBuilder', () => { const lines = imports.preview(); - expect(lines).toHaveLength(4); - expect(lines).toContain(`import * as specs from '$lib/specs.js';`); - expect(lines).toContain(`import type {TypeA} from '$lib/types.js';`); - expect(lines).toContain(`import {helper} from '$lib/utils.js';`); - expect(lines).toContain(`import * as schemas from '$lib/schemas.js';`); + assert.strictEqual(lines.length, 4); + assert.include(lines, `import * as specs from '$lib/specs.js';`); + assert.include(lines, `import type {TypeA} from '$lib/types.js';`); + assert.include(lines, `import {helper} from '$lib/utils.js';`); + assert.include(lines, `import * as schemas from '$lib/schemas.js';`); }); }); @@ -259,27 +267,27 @@ describe('ImportBuilder', () => { test('has_imports returns correct state', () => { const imports = new ImportBuilder(); - expect(imports.has_imports()).toBe(false); + assert.ok(!imports.has_imports()); imports.add_type('$lib/types.js', 'Foo'); - expect(imports.has_imports()).toBe(true); + assert.ok(imports.has_imports()); }); test('import_count returns correct count', () => { const imports = new ImportBuilder(); - expect(imports.import_count).toBe(0); + assert.strictEqual(imports.import_count, 0); imports.add_type('$lib/types.js', 'Foo'); - expect(imports.import_count).toBe(1); + assert.strictEqual(imports.import_count, 1); imports.add('$lib/utils.js', 'bar'); - expect(imports.import_count).toBe(2); + assert.strictEqual(imports.import_count, 2); // Adding to existing module doesn't increase count imports.add_type('$lib/types.js', 'Bar'); - expect(imports.import_count).toBe(2); + assert.strictEqual(imports.import_count, 2); }); test('preview returns array of import statements', () => { @@ -290,9 +298,9 @@ describe('ImportBuilder', () => { const preview = imports.preview(); - expect(preview).toHaveLength(2); - expect(preview[0]).toBe(`import type {Bar, Foo} from '$lib/types.js';`); - expect(preview[1]).toBe(`import {helper} from '$lib/utils.js';`); + assert.strictEqual(preview.length, 2); + assert.strictEqual(preview[0], `import type {Bar, Foo} from '$lib/types.js';`); + assert.strictEqual(preview[1], `import {helper} from '$lib/utils.js';`); }); test('clear removes all imports', () => { @@ -301,12 +309,12 @@ describe('ImportBuilder', () => { imports.add_types('$lib/types.js', 'Foo', 'Bar'); imports.add('$lib/utils.js', 'helper'); - expect(imports.import_count).toBe(2); + assert.strictEqual(imports.import_count, 2); imports.clear(); - expect(imports.import_count).toBe(0); - expect(imports.build()).toBe(''); + assert.strictEqual(imports.import_count, 0); + assert.strictEqual(imports.build(), ''); }); test('chaining works correctly', () => { @@ -319,8 +327,8 @@ describe('ImportBuilder', () => { .clear() .add_type('$lib/final.js', 'Final'); - expect(result).toBe(imports); // Chainable - expect(imports.build()).toBe(`import type {Final} from '$lib/final.js';`); + assert.strictEqual(result, imports); // Chainable + assert.strictEqual(imports.build(), `import type {Final} from '$lib/final.js';`); }); }); @@ -330,7 +338,7 @@ describe('ImportBuilder', () => { imports.add_many('$lib/utils.js', 'util_a', 'util_b', 'util_c'); - expect(imports.build()).toBe(`import {util_a, util_b, util_c} from '$lib/utils.js';`); + assert.strictEqual(imports.build(), `import {util_a, util_b, util_c} from '$lib/utils.js';`); }); test('add_many can handle namespace imports', () => { @@ -339,7 +347,7 @@ describe('ImportBuilder', () => { imports.add_many('$lib/all.js', '* as all', 'specific'); // Only the namespace import should be used - expect(imports.build()).toBe(`import * as all from '$lib/all.js';`); + assert.strictEqual(imports.build(), `import * as all from '$lib/all.js';`); }); }); @@ -350,8 +358,8 @@ describe('ImportBuilder', () => { imports.add('$lib/module.js', ''); // Empty imports should be ignored - expect(imports.build()).toBe(''); - expect(imports.has_imports()).toBe(false); + assert.strictEqual(imports.build(), ''); + assert.ok(!imports.has_imports()); }); test('handles special characters in import names', () => { @@ -360,7 +368,7 @@ describe('ImportBuilder', () => { imports.add('$lib/module.js', '$special'); imports.add('$lib/module.js', '_underscore'); - expect(imports.build()).toBe(`import {$special, _underscore} from '$lib/module.js';`); + assert.strictEqual(imports.build(), `import {$special, _underscore} from '$lib/module.js';`); }); }); }); @@ -369,7 +377,7 @@ describe('get_executor_phases', () => { describe('request_response actions', () => { test('frontend initiator - ping spec', () => { // ping has initiator: 'both' - expect(get_executor_phases(ping_action_spec, 'frontend')).toEqual([ + assert.deepEqual(get_executor_phases(ping_action_spec, 'frontend'), [ 'send_request', 'receive_response', 'send_error', @@ -377,7 +385,7 @@ describe('get_executor_phases', () => { 'receive_request', 'send_response', ]); - expect(get_executor_phases(ping_action_spec, 'backend')).toEqual([ + assert.deepEqual(get_executor_phases(ping_action_spec, 'backend'), [ 'send_request', 'receive_response', 'send_error', @@ -389,13 +397,13 @@ describe('get_executor_phases', () => { test('frontend initiator - session_load spec', () => { // load_session has initiator: 'frontend' - expect(get_executor_phases(session_load_action_spec, 'frontend')).toEqual([ + assert.deepEqual(get_executor_phases(session_load_action_spec, 'frontend'), [ 'send_request', 'receive_response', 'send_error', 'receive_error', ]); - expect(get_executor_phases(session_load_action_spec, 'backend')).toEqual([ + assert.deepEqual(get_executor_phases(session_load_action_spec, 'backend'), [ 'receive_request', 'send_response', 'send_error', @@ -404,13 +412,13 @@ describe('get_executor_phases', () => { test('frontend initiator - completion_create spec', () => { // create_completion has initiator: 'frontend' - expect(get_executor_phases(completion_create_action_spec, 'frontend')).toEqual([ + assert.deepEqual(get_executor_phases(completion_create_action_spec, 'frontend'), [ 'send_request', 'receive_response', 'send_error', 'receive_error', ]); - expect(get_executor_phases(completion_create_action_spec, 'backend')).toEqual([ + assert.deepEqual(get_executor_phases(completion_create_action_spec, 'backend'), [ 'receive_request', 'send_response', 'send_error', @@ -421,16 +429,16 @@ describe('get_executor_phases', () => { describe('remote_notification actions', () => { test('backend initiator - filer_change spec', () => { // filer_change has initiator: 'backend' - expect(get_executor_phases(filer_change_action_spec, 'frontend')).toEqual(['receive']); - expect(get_executor_phases(filer_change_action_spec, 'backend')).toEqual(['send']); + assert.deepEqual(get_executor_phases(filer_change_action_spec, 'frontend'), ['receive']); + assert.deepEqual(get_executor_phases(filer_change_action_spec, 'backend'), ['send']); }); }); describe('local_call actions', () => { test('frontend initiator - toggle_main_menu spec', () => { // toggle_main_menu has initiator: 'frontend' - expect(get_executor_phases(toggle_main_menu_action_spec, 'frontend')).toEqual(['execute']); - expect(get_executor_phases(toggle_main_menu_action_spec, 'backend')).toEqual([]); + assert.deepEqual(get_executor_phases(toggle_main_menu_action_spec, 'frontend'), ['execute']); + assert.deepEqual(get_executor_phases(toggle_main_menu_action_spec, 'backend'), []); }); }); @@ -438,8 +446,8 @@ describe('get_executor_phases', () => { test('phases are returned in correct order', () => { const frontend_phases = get_executor_phases(ping_action_spec, 'frontend'); // Send phases should come before receive phases - expect(frontend_phases.indexOf('send_request')).toBeLessThan( - frontend_phases.indexOf('receive_request'), + assert.ok( + frontend_phases.indexOf('send_request') < frontend_phases.indexOf('receive_request'), ); }); @@ -448,7 +456,7 @@ describe('get_executor_phases', () => { ...toggle_main_menu_action_spec, initiator: 'backend' as const, }; - expect(get_executor_phases(spec_with_backend_only, 'frontend')).toEqual([]); + assert.deepEqual(get_executor_phases(spec_with_backend_only, 'frontend'), []); }); }); }); @@ -460,29 +468,32 @@ describe('get_handler_return_type', () => { // ping_action_spec is a request/response action const result = get_handler_return_type(ping_action_spec, 'receive_request', imports, './'); - expect(result).toBe(`ActionOutputs['ping'] | Promise`); + assert.strictEqual(result, `ActionOutputs['ping'] | Promise`); // Check that ActionOutputs was added to imports const built = imports.build(); - expect(built).toContain('ActionOutputs'); - expect(built).toContain('./action_collections.js'); + assert.include(built, 'ActionOutputs'); + assert.include(built, './action_collections.js'); }); test('other phases return void and do not add imports', () => { const imports = new ImportBuilder(); - expect(get_handler_return_type(session_load_action_spec, 'send_request', imports, './')).toBe( + assert.strictEqual( + get_handler_return_type(session_load_action_spec, 'send_request', imports, './'), 'void | Promise', ); - expect( + assert.strictEqual( get_handler_return_type(session_load_action_spec, 'send_response', imports, './'), - ).toBe('void | Promise'); - expect( + 'void | Promise', + ); + assert.strictEqual( get_handler_return_type(session_load_action_spec, 'receive_response', imports, './'), - ).toBe('void | Promise'); + 'void | Promise', + ); // Should not add ActionOutputs for void returns - expect(imports.build()).toBe(''); + assert.strictEqual(imports.build(), ''); }); }); @@ -497,10 +508,10 @@ describe('get_handler_return_type', () => { imports, './', ); - expect(result).toBe(`ActionOutputs['toggle_main_menu']`); + assert.strictEqual(result, `ActionOutputs['toggle_main_menu']`); // Should add ActionOutputs import - expect(imports.build()).toContain('ActionOutputs'); + assert.include(imports.build(), 'ActionOutputs'); }); test('execute phase returns Promise for async local_call', () => { @@ -513,7 +524,8 @@ describe('get_handler_return_type', () => { }; const result = get_handler_return_type(async_local_spec, 'execute', imports, './'); - expect(result).toBe( + assert.strictEqual( + result, `ActionOutputs['toggle_main_menu'] | Promise`, ); }); @@ -523,15 +535,17 @@ describe('get_handler_return_type', () => { test('all phases return void', () => { const imports = new ImportBuilder(); - expect(get_handler_return_type(filer_change_action_spec, 'send', imports, './')).toBe( + assert.strictEqual( + get_handler_return_type(filer_change_action_spec, 'send', imports, './'), 'void | Promise', ); - expect(get_handler_return_type(filer_change_action_spec, 'receive', imports, './')).toBe( + assert.strictEqual( + get_handler_return_type(filer_change_action_spec, 'receive', imports, './'), 'void | Promise', ); // Should not add imports for void returns - expect(imports.build()).toBe(''); + assert.strictEqual(imports.build(), ''); }); }); @@ -541,15 +555,15 @@ describe('get_handler_return_type', () => { // First call adds import get_handler_return_type(ping_action_spec, 'receive_request', imports, './'); - expect(imports.import_count).toBe(1); + assert.strictEqual(imports.import_count, 1); // Second call doesn't add duplicate get_handler_return_type(session_load_action_spec, 'receive_request', imports, './'); - expect(imports.import_count).toBe(1); + assert.strictEqual(imports.import_count, 1); // Void return doesn't add import get_handler_return_type(ping_action_spec, 'send_request', imports, './'); - expect(imports.import_count).toBe(1); + assert.strictEqual(imports.import_count, 1); }); }); }); @@ -560,52 +574,52 @@ describe('generate_phase_handlers', () => { const imports = new ImportBuilder(); const result = generate_phase_handlers(toggle_main_menu_action_spec, 'backend', imports); - expect(result).toBe('toggle_main_menu?: never'); - expect(imports.has_imports()).toBe(false); + assert.strictEqual(result, 'toggle_main_menu?: never'); + assert.ok(!imports.has_imports()); }); test('generates handlers for request_response action', () => { const imports = new ImportBuilder(); const result = generate_phase_handlers(session_load_action_spec, 'frontend', imports); - expect(result).toContain('session_load?: {'); - expect(result).toContain('send_request?:'); - expect(result).toContain('receive_response?:'); - expect(result).not.toContain('receive_request'); + assert.include(result, 'session_load?: {'); + assert.include(result, 'send_request?:'); + assert.include(result, 'receive_response?:'); + assert.notInclude(result, 'receive_request'); // Check imports were added - expect(imports.has_imports()).toBe(true); + assert.ok(imports.has_imports()); const import_str = imports.build(); - expect(import_str).toContain('ActionEvent'); - expect(import_str).toContain('Frontend'); + assert.include(import_str, 'ActionEvent'); + assert.include(import_str, 'Frontend'); }); test('generates handlers for notification action', () => { const imports = new ImportBuilder(); const result = generate_phase_handlers(filer_change_action_spec, 'backend', imports); - expect(result).toContain('filer_change?: {'); - expect(result).toContain('send?:'); - expect(result).not.toContain('receive?:'); + assert.include(result, 'filer_change?: {'); + assert.include(result, 'send?:'); + assert.notInclude(result, 'receive?:'); const import_str = imports.build(); - expect(import_str).toContain('ActionEvent'); - expect(import_str).toContain('Backend'); + assert.include(import_str, 'ActionEvent'); + assert.include(import_str, 'Backend'); }); test('generates handlers for local_call action', () => { const imports = new ImportBuilder(); const result = generate_phase_handlers(toggle_main_menu_action_spec, 'frontend', imports); - expect(result).toContain('toggle_main_menu?: {'); - expect(result).toContain('execute?:'); - expect(result).toContain(`ActionOutputs['toggle_main_menu']`); - expect(result).not.toContain('Promise'); // It's a sync action + assert.include(result, 'toggle_main_menu?: {'); + assert.include(result, 'execute?:'); + assert.include(result, `ActionOutputs['toggle_main_menu']`); + assert.notInclude(result, 'Promise'); // It's a sync action const import_str = imports.build(); - expect(import_str).toContain('ActionEvent'); - expect(import_str).toContain('ActionOutputs'); // Added by get_handler_return_type - expect(import_str).toContain('Frontend'); + assert.include(import_str, 'ActionEvent'); + assert.include(import_str, 'ActionOutputs'); // Added by get_handler_return_type + assert.include(import_str, 'Frontend'); }); test('uses type-only imports when appropriate', () => { @@ -617,7 +631,7 @@ describe('generate_phase_handlers', () => { const lines = import_str.split('\n'); lines.forEach((line) => { if (line.trim()) { - expect(line).toMatch(/^import type/); + assert.match(line, /^import type/); } }); }); @@ -626,10 +640,10 @@ describe('generate_phase_handlers', () => { const imports = new ImportBuilder(); const result = generate_phase_handlers(ping_action_spec, 'frontend', imports); - expect(result).toContain('send_request?:'); - expect(result).toContain('receive_response?:'); - expect(result).toContain('receive_request?:'); - expect(result).toContain('send_response?:'); + assert.include(result, 'send_request?:'); + assert.include(result, 'receive_response?:'); + assert.include(result, 'receive_request?:'); + assert.include(result, 'send_response?:'); }); test('uses phase and step type parameters in handler signature', () => { @@ -637,16 +651,20 @@ describe('generate_phase_handlers', () => { const result = generate_phase_handlers(ping_action_spec, 'frontend', imports); // Should use the new type parameter syntax instead of data override - expect(result).toContain( + assert.include( + result, `action_event: ActionEvent<'ping', Frontend, 'send_request', 'handling'>`, ); - expect(result).toContain( + assert.include( + result, `action_event: ActionEvent<'ping', Frontend, 'receive_response', 'handling'>`, ); - expect(result).toContain( + assert.include( + result, `action_event: ActionEvent<'ping', Frontend, 'receive_request', 'handling'>`, ); - expect(result).toContain( + assert.include( + result, `action_event: ActionEvent<'ping', Frontend, 'send_response', 'handling'>`, ); }); @@ -656,12 +674,12 @@ describe('generate_phase_handlers', () => { // ping has receive_request handler on backend which returns output const result = generate_phase_handlers(ping_action_spec, 'backend', imports); - expect(result).toContain('receive_request?:'); - expect(result).toContain(`ActionOutputs['ping'] | Promise`); + assert.include(result, 'receive_request?:'); + assert.include(result, `ActionOutputs['ping'] | Promise`); // Check that ActionOutputs was imported const import_str = imports.build(); - expect(import_str).toContain('ActionOutputs'); + assert.include(import_str, 'ActionOutputs'); }); test('handler formatting is consistent', () => { @@ -670,9 +688,9 @@ describe('generate_phase_handlers', () => { // Check indentation and formatting const lines = result.split('\n'); - expect(lines[0]).toMatch(/^ping\?: \{$/); - expect(lines[1]).toMatch(/^\t\t/); // Two tabs for handler definitions - expect(lines[lines.length - 1]).toMatch(/^\t\}$/); // One tab for closing brace + assert.match(lines[0]!, /^ping\?: \{$/); + assert.match(lines[1]!, /^\t\t/); // Two tabs for handler definitions + assert.match(lines[lines.length - 1]!, /^\t\}$/); // One tab for closing brace }); test('imports are deduplicated across multiple specs', () => { @@ -686,9 +704,9 @@ describe('generate_phase_handlers', () => { const import_str = imports.build(); // Should have exactly one import of each type - expect(import_str.match(/ActionEvent/g)?.length).toBe(1); - expect(import_str.match(/Frontend/g)?.length).toBe(1); - expect(import_str.match(/ActionOutputs/g)?.length).toBe(1); + assert.strictEqual(import_str.match(/ActionEvent/g)?.length, 1); + assert.strictEqual(import_str.match(/Frontend/g)?.length, 1); + assert.strictEqual(import_str.match(/ActionOutputs/g)?.length, 1); }); test('frontend generates correct relative import paths', () => { @@ -696,9 +714,9 @@ describe('generate_phase_handlers', () => { generate_phase_handlers(ping_action_spec, 'frontend', imports); const import_str = imports.build(); - expect(import_str).toContain("from './action_event.js'"); - expect(import_str).toContain("from './frontend.svelte.js'"); - expect(import_str).toContain("from './action_collections.js'"); + assert.include(import_str, "from './action_event.js'"); + assert.include(import_str, "from './frontend.svelte.js'"); + assert.include(import_str, "from './action_collections.js'"); }); test('backend generates correct relative import paths', () => { @@ -706,8 +724,8 @@ describe('generate_phase_handlers', () => { generate_phase_handlers(ping_action_spec, 'backend', imports); const import_str = imports.build(); - expect(import_str).toContain("from '../action_event.js'"); - expect(import_str).toContain("from './backend.js'"); - expect(import_str).toContain("from '../action_collections.js'"); + assert.include(import_str, "from '../action_event.js'"); + assert.include(import_str, "from './backend.js'"); + assert.include(import_str, "from '../action_collections.js'"); }); }); diff --git a/src/test/config_defaults.test.ts b/src/test/config_defaults.test.ts index 1b5c34fc9..10f9a48e2 100644 --- a/src/test/config_defaults.test.ts +++ b/src/test/config_defaults.test.ts @@ -1,4 +1,4 @@ -import {test, expect} from 'vitest'; +import {test, assert} from 'vitest'; import {providers_default, models_default, chat_template_defaults} from '$lib/config_defaults.js'; @@ -11,10 +11,10 @@ test('all model provider_names exist in providers_default', () => { // Check that each model's provider exists for (const provider_name of model_provider_names) { - expect( + assert.ok( provider_names.has(provider_name), `Provider "${provider_name}" used in models_default does not exist in providers_default`, - ).toBe(true); + ); } }); @@ -35,8 +35,9 @@ test('all chat template model_names exist in models_default', () => { } } - expect( + assert.deepEqual( missing_models, + [], `The following models in chat_template_defaults do not exist in models_default: ${missing_models.join(', ')}`, - ).toEqual([]); + ); }); diff --git a/src/test/diskfile_editor_state.svelte.base.test.ts b/src/test/diskfile_editor_state.svelte.base.test.ts index ea7942c8d..197c45fe9 100644 --- a/src/test/diskfile_editor_state.svelte.base.test.ts +++ b/src/test/diskfile_editor_state.svelte.base.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, beforeEach, describe} from 'vitest'; +import {test, beforeEach, describe, assert} from 'vitest'; import {DiskfileEditorState} from '$lib/diskfile_editor_state.svelte.js'; import {DiskfilePath, SerializableDisknode} from '$lib/diskfile_types.js'; @@ -40,31 +38,31 @@ beforeEach(() => { describe('initialization', () => { test('editor_state initializes with correct values', () => { - expect(editor_state.original_content).toBe(TEST_CONTENT); - expect(editor_state.current_content).toBe(TEST_CONTENT); - expect(editor_state.has_changes).toBe(false); - expect(editor_state.content_was_modified_by_user).toBe(false); - expect(editor_state.unsaved_edit_entry_id).toBeNull(); - expect(editor_state.last_seen_disk_content).toBe(TEST_CONTENT); + assert.strictEqual(editor_state.original_content, TEST_CONTENT); + assert.strictEqual(editor_state.current_content, TEST_CONTENT); + assert.ok(!editor_state.has_changes); + assert.ok(!editor_state.content_was_modified_by_user); + assert.isNull(editor_state.unsaved_edit_entry_id); + assert.strictEqual(editor_state.last_seen_disk_content, TEST_CONTENT); // Selected history entry should be initialized to the current entry const history = app.get_diskfile_history(TEST_PATH); - expect(history).toBeDefined(); - expect(history!.entries.length).toBe(1); - expect(editor_state.selected_history_entry_id).toBe(history!.entries[0]!.id); - expect(history!.entries[0]!.content).toBe(TEST_CONTENT); + assert.isDefined(history); + assert.strictEqual(history.entries.length, 1); + assert.strictEqual(editor_state.selected_history_entry_id, history.entries[0]!.id); + assert.strictEqual(history.entries[0]!.content, TEST_CONTENT); }); test('editor_state initializes with correct history entry', () => { const history = app.get_diskfile_history(TEST_PATH); - expect(history).toBeDefined(); - expect(history!.entries.length).toBe(1); + assert.isDefined(history); + assert.strictEqual(history.entries.length, 1); // The initial entry should contain the original content - expect(history!.entries[0]!.content).toBe(TEST_CONTENT); - expect(history!.entries[0]!.is_unsaved_edit).toBe(false); - expect(history!.entries[0]!.is_disk_change).toBe(false); - expect(history!.entries[0]!.is_original_state).toBe(true); + assert.strictEqual(history.entries[0]!.content, TEST_CONTENT); + assert.ok(!history.entries[0]!.is_unsaved_edit); + assert.ok(!history.entries[0]!.is_disk_change); + assert.ok(history.entries[0]!.is_original_state); }); test('editor_state handles initialization with null content', () => { @@ -82,15 +80,15 @@ describe('initialization', () => { }); // Check state properties - expect(null_editor_state.original_content).toBeNull(); - expect(null_editor_state.current_content).toBe(''); - expect(null_editor_state.has_changes).toBe(false); - expect(null_editor_state.last_seen_disk_content).toBeNull(); + assert.isNull(null_editor_state.original_content); + assert.strictEqual(null_editor_state.current_content, ''); + assert.ok(!null_editor_state.has_changes); + assert.isNull(null_editor_state.last_seen_disk_content); // History should still be created const history = app.get_diskfile_history(null_diskfile.path); - expect(history).toBeDefined(); - expect(history!.entries.length).toBe(0); // No entries for null content + assert.isDefined(history); + assert.strictEqual(history.entries.length, 0); // No entries for null content }); }); @@ -99,35 +97,35 @@ describe('content editing', () => { const new_content = 'Modified content'; editor_state.current_content = new_content; - expect(editor_state.current_content).toBe(new_content); - expect(editor_state.has_changes).toBe(true); - expect(editor_state.content_was_modified_by_user).toBe(true); + assert.strictEqual(editor_state.current_content, new_content); + assert.ok(editor_state.has_changes); + assert.ok(editor_state.content_was_modified_by_user); }); test('content modifications track user edits flag', () => { // Initial state - no user edits - expect(editor_state.content_was_modified_by_user).toBe(false); + assert.ok(!editor_state.content_was_modified_by_user); // Change content - should mark as user-edited editor_state.current_content = 'User edit'; - expect(editor_state.content_was_modified_by_user).toBe(true); + assert.ok(editor_state.content_was_modified_by_user); // Change back to original - should clear user-edited flag editor_state.current_content = TEST_CONTENT; - expect(editor_state.content_was_modified_by_user).toBe(false); + assert.ok(!editor_state.content_was_modified_by_user); }); test('has_changes tracks difference between current and original content', () => { // Initial state - no changes - expect(editor_state.has_changes).toBe(false); + assert.ok(!editor_state.has_changes); // Make a change editor_state.current_content = 'Changed content'; - expect(editor_state.has_changes).toBe(true); + assert.ok(editor_state.has_changes); // Change back to original editor_state.current_content = TEST_CONTENT; - expect(editor_state.has_changes).toBe(false); + assert.ok(!editor_state.has_changes); }); test('editing content preserves selection state', () => { @@ -137,56 +135,56 @@ describe('content editing', () => { // Get the selected entry id const selected_id = editor_state.selected_history_entry_id; - expect(selected_id).not.toBeNull(); + assert.ok(selected_id !== null); // Make another edit editor_state.current_content = 'Second edit'; // Selection should still be active - expect(editor_state.selected_history_entry_id).not.toBeNull(); + assert.ok(editor_state.selected_history_entry_id !== null); // Content should be updated in the selected entry - const updated_entry = history.find_entry_by_id(editor_state.selected_history_entry_id!); - expect(updated_entry).toBeDefined(); - expect(updated_entry!.content).toBe('Second edit'); + const updated_entry = history.find_entry_by_id(editor_state.selected_history_entry_id); + assert.isDefined(updated_entry); + assert.strictEqual(updated_entry.content, 'Second edit'); }); test('editing to match original content clears user modified flag', () => { // Make an edit editor_state.current_content = 'User edit'; - expect(editor_state.content_was_modified_by_user).toBe(true); - expect(editor_state.has_changes).toBe(true); + assert.ok(editor_state.content_was_modified_by_user); + assert.ok(editor_state.has_changes); // Edit back to match original editor_state.current_content = TEST_CONTENT; // Flags should be cleared - expect(editor_state.content_was_modified_by_user).toBe(false); - expect(editor_state.has_changes).toBe(false); + assert.ok(!editor_state.content_was_modified_by_user); + assert.ok(!editor_state.has_changes); }); }); describe('content metrics', () => { test('editor provides accurate content length metrics', () => { // Initial length - expect(editor_state.original_length).toBe(TEST_CONTENT.length); - expect(editor_state.current_length).toBe(TEST_CONTENT.length); - expect(editor_state.length_diff).toBe(0); - expect(editor_state.length_diff_percent).toBe(0); + assert.strictEqual(editor_state.original_length, TEST_CONTENT.length); + assert.strictEqual(editor_state.current_length, TEST_CONTENT.length); + assert.strictEqual(editor_state.length_diff, 0); + assert.strictEqual(editor_state.length_diff_percent, 0); // Update content const new_content = 'Shorter'; editor_state.current_content = new_content; // Check metrics - expect(editor_state.current_length).toBe(new_content.length); - expect(editor_state.length_diff).toBe(new_content.length - TEST_CONTENT.length); + assert.strictEqual(editor_state.current_length, new_content.length); + assert.strictEqual(editor_state.length_diff, new_content.length - TEST_CONTENT.length); // Percent change should be negative const expected_percent = Math.round( ((new_content.length - TEST_CONTENT.length) / TEST_CONTENT.length) * 100, ); - expect(editor_state.length_diff_percent).toBe(expected_percent); + assert.strictEqual(editor_state.length_diff_percent, expected_percent); }); test('editor provides accurate token metrics', () => { @@ -195,9 +193,10 @@ describe('content metrics', () => { editor_state.current_content = token_test_content; // Verify token calculations - expect(editor_state.current_token_count).toBeGreaterThan(0); - expect(editor_state.current_token_count).toBe(editor_state.current_token_count); - expect(editor_state.token_diff).toBe( + assert.ok(editor_state.current_token_count > 0); + assert.strictEqual(editor_state.current_token_count, editor_state.current_token_count); + assert.strictEqual( + editor_state.token_diff, editor_state.current_token_count - editor_state.original_token_count, ); @@ -207,7 +206,7 @@ describe('content metrics', () => { editor_state.original_token_count) * 100, ); - expect(editor_state.token_diff_percent).toBe(expected_token_percent); + assert.strictEqual(editor_state.token_diff_percent, expected_token_percent); }); test('editor handles metrics for empty content', () => { @@ -215,15 +214,15 @@ describe('content metrics', () => { editor_state.current_content = ''; // Check length metrics - expect(editor_state.current_length).toBe(0); - expect(editor_state.length_diff).toBe(-TEST_CONTENT.length); - expect(editor_state.length_diff_percent).toBe(-100); + assert.strictEqual(editor_state.current_length, 0); + assert.strictEqual(editor_state.length_diff, -TEST_CONTENT.length); + assert.strictEqual(editor_state.length_diff_percent, -100); // Check token metrics - expect(editor_state.current_token_count).toBe(0); - expect(editor_state.current_token_count).toBe(0); - expect(editor_state.token_diff).toBe(-editor_state.original_token_count); - expect(editor_state.token_diff_percent).toBe(-100); + assert.strictEqual(editor_state.current_token_count, 0); + assert.strictEqual(editor_state.current_token_count, 0); + assert.strictEqual(editor_state.token_diff, -editor_state.original_token_count); + assert.strictEqual(editor_state.token_diff_percent, -100); }); test('length_diff_percent handles zero original length correctly', () => { @@ -244,12 +243,12 @@ describe('content metrics', () => { empty_editor_state.current_content = 'New content'; // Since original length was 0, percentage should be 100% - expect(empty_editor_state.original_length).toBe(0); - expect(empty_editor_state.length_diff_percent).toBe(100); + assert.strictEqual(empty_editor_state.original_length, 0); + assert.strictEqual(empty_editor_state.length_diff_percent, 100); // Same for tokens - expect(empty_editor_state.original_token_count).toBe(0); - expect(empty_editor_state.token_diff_percent).toBe(100); + assert.strictEqual(empty_editor_state.original_token_count, 0); + assert.strictEqual(empty_editor_state.token_diff_percent, 100); }); }); @@ -271,17 +270,17 @@ describe('file management', () => { editor_state.update_diskfile(another_diskfile); // Verify state was properly updated - expect(editor_state.diskfile).toBe(another_diskfile); - expect(editor_state.original_content).toBe(another_content); - expect(editor_state.current_content).toBe(another_content); - expect(editor_state.has_changes).toBe(false); - expect(editor_state.content_was_modified_by_user).toBe(false); + assert.strictEqual(editor_state.diskfile, another_diskfile); + assert.strictEqual(editor_state.original_content, another_content); + assert.strictEqual(editor_state.current_content, another_content); + assert.ok(!editor_state.has_changes); + assert.ok(!editor_state.content_was_modified_by_user); // History should be initialized for the new file const new_history = app.get_diskfile_history(another_path); - expect(new_history).toBeDefined(); - expect(new_history!.entries.length).toBe(1); - expect(new_history!.entries[0]!.content).toBe(another_content); + assert.isDefined(new_history); + assert.strictEqual(new_history.entries.length, 1); + assert.strictEqual(new_history.entries[0]!.content, another_content); }); test('update_diskfile does nothing when same diskfile is provided', () => { @@ -296,8 +295,8 @@ describe('file management', () => { editor_state.update_diskfile(test_diskfile); // State should remain unchanged - expect(editor_state.current_content).toBe(current_content); - expect(editor_state.content_was_modified_by_user).toBe(current_modified); + assert.strictEqual(editor_state.current_content, current_content); + assert.strictEqual(editor_state.content_was_modified_by_user, current_modified); }); test('reset clears editor state and reverts to original content', () => { @@ -313,47 +312,47 @@ describe('file management', () => { editor_state.reset(); // Verify state is reset - expect(editor_state.current_content).toBe(TEST_CONTENT); - expect(editor_state.has_changes).toBe(false); - expect(editor_state.content_was_modified_by_user).toBe(false); - expect(editor_state.unsaved_edit_entry_id).toBeNull(); - expect(editor_state.selected_history_entry_id).toBeNull(); + assert.strictEqual(editor_state.current_content, TEST_CONTENT); + assert.ok(!editor_state.has_changes); + assert.ok(!editor_state.content_was_modified_by_user); + assert.isNull(editor_state.unsaved_edit_entry_id); + assert.isNull(editor_state.selected_history_entry_id); }); }); describe('derived state', () => { test('derived property has_history is accurate', () => { // Initial state - only one entry, should not have history - expect(editor_state.has_history).toBe(false); + assert.ok(!editor_state.has_history); // Add an entry editor_state.current_content = 'New content'; // Now we should have history - expect(editor_state.has_history).toBe(true); + assert.ok(editor_state.has_history); }); test('derived property has_unsaved_edits is accurate', async () => { // Initial state - no unsaved edits - expect(editor_state.has_unsaved_edits).toBe(false); + assert.ok(!editor_state.has_unsaved_edits); // Make an edit editor_state.current_content = 'Unsaved edit'; // Now we should have unsaved edits - expect(editor_state.has_unsaved_edits).toBe(true); + assert.ok(editor_state.has_unsaved_edits); // Save the changes await editor_state.save_changes(); // No more unsaved edits - expect(editor_state.has_unsaved_edits).toBe(false); + assert.ok(!editor_state.has_unsaved_edits); }); test('derived properties for UI state management', () => { // Initial state - expect(editor_state.can_clear_history).toBe(false); - expect(editor_state.can_clear_unsaved_edits).toBe(false); + assert.ok(!editor_state.can_clear_history); + assert.ok(!editor_state.can_clear_unsaved_edits); // Add a saved entry const history = app.get_diskfile_history(TEST_PATH)!; @@ -361,13 +360,13 @@ describe('derived state', () => { history.add_entry('Saved entry 2'); // Now we can clear history - expect(editor_state.can_clear_history).toBe(true); + assert.ok(editor_state.can_clear_history); // Add an unsaved entry editor_state.current_content = 'Unsaved edit'; // Now we can clear unsaved edits as well - expect(editor_state.can_clear_unsaved_edits).toBe(true); + assert.ok(editor_state.can_clear_unsaved_edits); }); test('content_matching_entry_ids tracks entries with matching content', () => { @@ -378,17 +377,17 @@ describe('derived state', () => { const entry3 = history.add_entry('Duplicate content'); // Initial check - current content doesn't match any entry - expect(editor_state.content_matching_entry_ids).not.toContain(entry1.id); - expect(editor_state.content_matching_entry_ids).not.toContain(entry2.id); - expect(editor_state.content_matching_entry_ids).not.toContain(entry3.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry1.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry2.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry3.id); // Set content to match duplicates editor_state.current_content = 'Duplicate content'; // Verify matching entries are tracked - expect(editor_state.content_matching_entry_ids).toContain(entry2.id); - expect(editor_state.content_matching_entry_ids).toContain(entry3.id); - expect(editor_state.content_matching_entry_ids).not.toContain(entry1.id); + assert.include(editor_state.content_matching_entry_ids, entry2.id); + assert.include(editor_state.content_matching_entry_ids, entry3.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry1.id); }); }); @@ -401,21 +400,21 @@ describe('saving changes', () => { const result = await editor_state.save_changes(); // Verify result and diskfile update - expect(result).toBe(true); - expect(test_diskfile.content).toBe('Content to save'); - expect(editor_state.last_seen_disk_content).toBe('Content to save'); - expect(editor_state.content_was_modified_by_user).toBe(false); + assert.ok(result); + assert.strictEqual(test_diskfile.content, 'Content to save'); + assert.strictEqual(editor_state.last_seen_disk_content, 'Content to save'); + assert.ok(!editor_state.content_was_modified_by_user); }); test('save_changes with no changes returns false', async () => { // Don't make any changes - expect(editor_state.has_changes).toBe(false); + assert.ok(!editor_state.has_changes); // Try to save const result = await editor_state.save_changes(); // Verify nothing was saved - expect(result).toBe(false); + assert.ok(!result); }); test('save_changes creates history entry with correct properties', async () => { @@ -427,8 +426,8 @@ describe('saving changes', () => { // Check history entry const history = app.get_diskfile_history(TEST_PATH)!; - expect(history.entries[0]!.content).toBe('Content to be saved'); - expect(history.entries[0]!.is_unsaved_edit).toBe(false); - expect(history.entries[0]!.is_disk_change).toBe(false); + assert.strictEqual(history.entries[0]!.content, 'Content to be saved'); + assert.ok(!history.entries[0]!.is_unsaved_edit); + assert.ok(!history.entries[0]!.is_disk_change); }); }); diff --git a/src/test/diskfile_editor_state.svelte.disk_changes.test.ts b/src/test/diskfile_editor_state.svelte.disk_changes.test.ts index 614078fcc..1037b8e0f 100644 --- a/src/test/diskfile_editor_state.svelte.disk_changes.test.ts +++ b/src/test/diskfile_editor_state.svelte.disk_changes.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, beforeEach, describe} from 'vitest'; +import {test, beforeEach, describe, assert} from 'vitest'; import {DiskfileEditorState} from '$lib/diskfile_editor_state.svelte.js'; import {DiskfilePath, SerializableDisknode} from '$lib/diskfile_types.js'; @@ -41,7 +39,7 @@ beforeEach(() => { describe('disk change detection', () => { test('identifies when disk content changes', () => { // Initial state - no disk content tracking issues - expect(editor_state.last_seen_disk_content).toBe(TEST_CONTENT); + assert.strictEqual(editor_state.last_seen_disk_content, TEST_CONTENT); // Simulate a change on disk const new_disk_content = 'Content changed on disk'; @@ -51,8 +49,8 @@ describe('disk change detection', () => { editor_state.check_disk_changes(); // Since there are no user edits, content should be auto-updated - expect(editor_state.current_content).toBe(new_disk_content); - expect(editor_state.last_seen_disk_content).toBe(new_disk_content); + assert.strictEqual(editor_state.current_content, new_disk_content); + assert.strictEqual(editor_state.last_seen_disk_content, new_disk_content); }); test('with no user edits automatically updates content and selection', () => { @@ -64,8 +62,8 @@ describe('disk change detection', () => { editor_state.check_disk_changes(); // Content should be auto-updated - expect(editor_state.current_content).toBe(disk_content); - expect(editor_state.last_seen_disk_content).toBe(disk_content); + assert.strictEqual(editor_state.current_content, disk_content); + assert.strictEqual(editor_state.last_seen_disk_content, disk_content); // History should have a new entry with disk change flag const history = app.get_diskfile_history(TEST_PATH)!; @@ -73,13 +71,13 @@ describe('disk change detection', () => { (entry) => entry.is_disk_change && entry.content === disk_content, ); - expect(disk_entry).toMatchObject({ + assert.include(disk_entry, { content: disk_content, is_disk_change: true, }); // Selection should point to the new disk change entry - expect(editor_state.selected_history_entry_id).toBe(disk_entry!.id); + assert.strictEqual(editor_state.selected_history_entry_id, disk_entry!.id); }); test('ignores null content states', () => { @@ -91,7 +89,7 @@ describe('disk change detection', () => { editor_state.check_disk_changes(); // Nothing should happen, no errors - expect(editor_state.last_seen_disk_content).toBeNull(); + assert.isNull(editor_state.last_seen_disk_content); }); test('does nothing if disk content matches last seen', () => { @@ -103,7 +101,7 @@ describe('disk change detection', () => { editor_state.check_disk_changes(); // Last seen should remain unchanged - expect(editor_state.last_seen_disk_content).toBe('Last seen content'); + assert.strictEqual(editor_state.last_seen_disk_content, 'Last seen content'); }); test('handles first-time initialization correctly', () => { @@ -126,24 +124,24 @@ describe('disk change detection', () => { new_editor_state.check_disk_changes(); // last_seen_disk_content should be initialized - expect(new_editor_state.last_seen_disk_content).toBe('Initial content'); - expect(new_editor_state.current_content).toBe('Initial content'); + assert.strictEqual(new_editor_state.last_seen_disk_content, 'Initial content'); + assert.strictEqual(new_editor_state.current_content, 'Initial content'); }); test('with user edits adds disk change to history but preserves user content', () => { // First make a user edit editor_state.current_content = 'User edited content'; - expect(editor_state.content_was_modified_by_user).toBe(true); + assert.ok(editor_state.content_was_modified_by_user); // Simulate disk change test_diskfile.content = 'Changed on disk'; editor_state.check_disk_changes(); // User content should be preserved - expect(editor_state.current_content).toBe('User edited content'); + assert.strictEqual(editor_state.current_content, 'User edited content'); // Last seen content should be updated - expect(editor_state.last_seen_disk_content).toBe('Changed on disk'); + assert.strictEqual(editor_state.last_seen_disk_content, 'Changed on disk'); // Find the disk change entry const history = app.get_diskfile_history(TEST_PATH)!; @@ -151,13 +149,13 @@ describe('disk change detection', () => { (entry) => entry.is_disk_change && entry.content === 'Changed on disk', ); - expect(disk_entry).toMatchObject({ + assert.include(disk_entry, { content: 'Changed on disk', is_disk_change: true, }); // Selection should not automatically change to disk entry - expect(editor_state.selected_history_entry_id).not.toBe(disk_entry!.id); + assert.notStrictEqual(editor_state.selected_history_entry_id, disk_entry!.id); }); test('skips adding disk change if content matches existing entries', () => { @@ -172,7 +170,7 @@ describe('disk change detection', () => { editor_state.check_disk_changes(); // No new entry should be added - expect(history.entries.length).toBe(count_after_first); + assert.strictEqual(history.entries.length, count_after_first); }); test('marks existing entry as disk change when content matches', () => { @@ -184,19 +182,19 @@ describe('disk change detection', () => { }); // Verify the entry isn't marked as a disk change yet - expect(entry.is_disk_change).toBe(false); - expect(entry.is_unsaved_edit).toBe(true); + assert.ok(!entry.is_disk_change); + assert.ok(entry.is_unsaved_edit); // Make a disk change that matches the existing entry's content test_diskfile.content = 'New content on disk'; editor_state.check_disk_changes(); // The existing entry should now be marked as a disk change and not an unsaved edit - expect(history.entries[0]!.is_disk_change).toBe(true); - expect(history.entries[0]!.is_unsaved_edit).toBe(false); + assert.ok(history.entries[0]!.is_disk_change); + assert.ok(!history.entries[0]!.is_unsaved_edit); // No new entry should be created - expect(history.entries.length).toBe(2); // Original + our added entry + assert.strictEqual(history.entries.length, 2); // Original + our added entry }); }); @@ -221,12 +219,12 @@ describe('file history management', () => { (e) => e.content === 'Second disk change' && e.is_disk_change, ); - expect(firstEntry).toMatchObject({ + assert.include(firstEntry, { content: 'First disk change', is_disk_change: true, }); - expect(secondEntry).toMatchObject({ + assert.include(secondEntry, { content: 'Second disk change', is_disk_change: true, }); @@ -242,7 +240,7 @@ describe('file history management', () => { editor_state.check_disk_changes(); // Selection should remain on user's edit - expect(editor_state.selected_history_entry_id).toBe(selected_id); + assert.strictEqual(editor_state.selected_history_entry_id, selected_id); }); test('with user selection of older history maintains that selection during disk change', () => { @@ -252,14 +250,14 @@ describe('file history management', () => { // Select the older entry editor_state.set_content_from_history(older_entry.id); - expect(editor_state.selected_history_entry_id).toBe(older_entry.id); + assert.strictEqual(editor_state.selected_history_entry_id, older_entry.id); // Simulate disk change test_diskfile.content = 'New disk content'; editor_state.check_disk_changes(); // Selection should remain on the older entry - expect(editor_state.selected_history_entry_id).toBe(older_entry.id); + assert.strictEqual(editor_state.selected_history_entry_id, older_entry.id); }); }); @@ -272,13 +270,13 @@ describe('save changes behavior', () => { await editor_state.save_changes(); // Content should be saved to disk - expect(test_diskfile.content).toBe('User edit to save'); + assert.strictEqual(test_diskfile.content, 'User edit to save'); // Last seen disk content should be updated - expect(editor_state.last_seen_disk_content).toBe('User edit to save'); + assert.strictEqual(editor_state.last_seen_disk_content, 'User edit to save'); // User modified flag should be cleared - expect(editor_state.content_was_modified_by_user).toBe(false); + assert.ok(!editor_state.content_was_modified_by_user); }); test('saving during disk changes preserves selected content', async () => { @@ -293,10 +291,10 @@ describe('save changes behavior', () => { await editor_state.save_changes(); // Disk should have user content - expect(test_diskfile.content).toBe('User edit'); + assert.strictEqual(test_diskfile.content, 'User edit'); // Last seen content should be updated - expect(editor_state.last_seen_disk_content).toBe('User edit'); + assert.strictEqual(editor_state.last_seen_disk_content, 'User edit'); }); }); @@ -307,14 +305,14 @@ describe('edge cases', () => { editor_state.check_disk_changes(); // With no user edits, content should be updated to empty - expect(editor_state.current_content).toBe(''); - expect(editor_state.last_seen_disk_content).toBe(''); + assert.strictEqual(editor_state.current_content, ''); + assert.strictEqual(editor_state.last_seen_disk_content, ''); // History should include empty content entry const history = app.get_diskfile_history(TEST_PATH)!; const empty_entry = history.entries.find((e) => e.content === '' && e.is_disk_change); - expect(empty_entry).toMatchObject({ + assert.include(empty_entry, { content: '', is_disk_change: true, }); @@ -344,10 +342,10 @@ describe('edge cases', () => { empty_history_editor.check_disk_changes(); // Should handle gracefully without errors - expect(empty_history_editor.current_content).toBe('Disk changed'); + assert.strictEqual(empty_history_editor.current_content, 'Disk changed'); // The implementation should create an entry with the disk_change flag - expect(history.entries[0]!).toMatchObject({ + assert.include(history.entries[0]!, { content: 'Disk changed', is_disk_change: true, }); @@ -364,7 +362,7 @@ describe('edge cases', () => { editor_state.check_disk_changes(); // Should not crash and maintain same state - expect(editor_state.current_content).toBe('User edit'); + assert.strictEqual(editor_state.current_content, 'User edit'); }); test('editing to match disk content is handled properly', () => { @@ -376,6 +374,6 @@ describe('edge cases', () => { editor_state.current_content = 'Disk content'; // User modified state should be false since it matches disk content - expect(editor_state.content_was_modified_by_user).toBe(false); + assert.ok(!editor_state.content_was_modified_by_user); }); }); diff --git a/src/test/diskfile_editor_state.svelte.history.test.ts b/src/test/diskfile_editor_state.svelte.history.test.ts index 45a1a237a..dfac4f2d1 100644 --- a/src/test/diskfile_editor_state.svelte.history.test.ts +++ b/src/test/diskfile_editor_state.svelte.history.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, beforeEach, describe} from 'vitest'; +import {test, beforeEach, describe, assert} from 'vitest'; import {DiskfileEditorState} from '$lib/diskfile_editor_state.svelte.js'; import {DiskfilePath, SerializableDisknode} from '$lib/diskfile_types.js'; @@ -45,20 +43,20 @@ describe('unsaved edit creation', () => { editor_state.current_content = new_content; // Verify an unsaved entry was created - expect(editor_state.unsaved_edit_entry_id).not.toBeNull(); + assert.ok(editor_state.unsaved_edit_entry_id !== null); // Verify the new entry const history = app.get_diskfile_history(TEST_PATH)!; - const new_entry = history.find_entry_by_id(editor_state.unsaved_edit_entry_id!); + const new_entry = history.find_entry_by_id(editor_state.unsaved_edit_entry_id); - expect(new_entry).toMatchObject({ + assert.include(new_entry, { content: new_content, is_unsaved_edit: true, label: 'Unsaved edit', }); // Selection should match the unsaved entry - expect(editor_state.selected_history_entry_id).toBe(editor_state.unsaved_edit_entry_id); + assert.strictEqual(editor_state.selected_history_entry_id, editor_state.unsaved_edit_entry_id); }); test('multiple content updates modify the same unsaved entry', () => { @@ -67,20 +65,20 @@ describe('unsaved edit creation', () => { // Track the entry id const unsaved_id = editor_state.unsaved_edit_entry_id; - expect(unsaved_id).not.toBeNull(); + assert.ok(unsaved_id !== null); // Make additional edits editor_state.current_content = 'Second edit'; editor_state.current_content = 'Third edit'; // Verify the same entry was updated - expect(editor_state.unsaved_edit_entry_id).toBe(unsaved_id); + assert.strictEqual(editor_state.unsaved_edit_entry_id, unsaved_id); // Verify the entry content was updated const history = app.get_diskfile_history(TEST_PATH)!; - const updated_entry = history.find_entry_by_id(unsaved_id!); + const updated_entry = history.find_entry_by_id(unsaved_id); - expect(updated_entry).toMatchObject({ + assert.include(updated_entry, { content: 'Third edit', is_unsaved_edit: true, }); @@ -95,11 +93,11 @@ describe('unsaved edit creation', () => { editor_state.current_content = TEST_CONTENT; // Verify unsaved entry was removed - expect(editor_state.unsaved_edit_entry_id).toBeNull(); + assert.isNull(editor_state.unsaved_edit_entry_id); // Entry should no longer exist const history = app.get_diskfile_history(TEST_PATH)!; - expect(history.find_entry_by_id(unsaved_id!)).toBeUndefined(); + assert.ok(history.find_entry_by_id(unsaved_id!) === undefined); }); test('editing to match existing content selects that entry instead of creating new one', () => { @@ -111,8 +109,8 @@ describe('unsaved edit creation', () => { editor_state.current_content = 'Existing content'; // Existing entry should be selected - expect(editor_state.selected_history_entry_id).toBe(existing_entry.id); - expect(editor_state.unsaved_edit_entry_id).toBeNull(); + assert.strictEqual(editor_state.selected_history_entry_id, existing_entry.id); + assert.isNull(editor_state.unsaved_edit_entry_id); }); test('editing to match existing unsaved edit selects that entry', () => { @@ -128,8 +126,8 @@ describe('unsaved edit creation', () => { editor_state.current_content = 'Unsaved content'; // The existing unsaved entry should be selected - expect(editor_state.selected_history_entry_id).toBe(unsaved_entry.id); - expect(editor_state.unsaved_edit_entry_id).toBe(unsaved_entry.id); + assert.strictEqual(editor_state.selected_history_entry_id, unsaved_entry.id); + assert.strictEqual(editor_state.unsaved_edit_entry_id, unsaved_entry.id); }); }); @@ -144,15 +142,15 @@ describe('history navigation', () => { editor_state.set_content_from_history(entry1.id); // Verify selection and content - expect(editor_state.selected_history_entry_id).toBe(entry1.id); - expect(editor_state.current_content).toBe('Entry 1'); + assert.strictEqual(editor_state.selected_history_entry_id, entry1.id); + assert.strictEqual(editor_state.current_content, 'Entry 1'); // Select second entry editor_state.set_content_from_history(entry2.id); // Verify selection and content updated - expect(editor_state.selected_history_entry_id).toBe(entry2.id); - expect(editor_state.current_content).toBe('Entry 2'); + assert.strictEqual(editor_state.selected_history_entry_id, entry2.id); + assert.strictEqual(editor_state.current_content, 'Entry 2'); }); test('set_content_from_history with unsaved edit sets unsaved_edit_entry_id', () => { @@ -164,8 +162,8 @@ describe('history navigation', () => { editor_state.set_content_from_history(unsaved_entry.id); // Verify both ids are set correctly - expect(editor_state.selected_history_entry_id).toBe(unsaved_entry.id); - expect(editor_state.unsaved_edit_entry_id).toBe(unsaved_entry.id); + assert.strictEqual(editor_state.selected_history_entry_id, unsaved_entry.id); + assert.strictEqual(editor_state.unsaved_edit_entry_id, unsaved_entry.id); }); test('set_content_from_history with saved entry clears unsaved_edit_entry_id', () => { @@ -175,14 +173,14 @@ describe('history navigation', () => { // First select an unsaved entry editor_state.current_content = 'Unsaved content'; - expect(editor_state.unsaved_edit_entry_id).not.toBeNull(); + assert.ok(editor_state.unsaved_edit_entry_id !== null); // Now select the saved entry editor_state.set_content_from_history(saved_entry.id); // Verify unsaved edit id is cleared - expect(editor_state.selected_history_entry_id).toBe(saved_entry.id); - expect(editor_state.unsaved_edit_entry_id).toBeNull(); + assert.strictEqual(editor_state.selected_history_entry_id, saved_entry.id); + assert.isNull(editor_state.unsaved_edit_entry_id); }); test('content_matching_entry_ids tracks entries with matching content', () => { @@ -193,17 +191,17 @@ describe('history navigation', () => { const entry3 = history.add_entry('Duplicate content'); // Initial check - current content doesn't match any entry - expect(editor_state.content_matching_entry_ids).not.toContain(entry1.id); - expect(editor_state.content_matching_entry_ids).not.toContain(entry2.id); - expect(editor_state.content_matching_entry_ids).not.toContain(entry3.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry1.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry2.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry3.id); // Set content to match duplicates editor_state.current_content = 'Duplicate content'; // Verify matching entries are tracked - expect(editor_state.content_matching_entry_ids).toContain(entry2.id); - expect(editor_state.content_matching_entry_ids).toContain(entry3.id); - expect(editor_state.content_matching_entry_ids).not.toContain(entry1.id); + assert.include(editor_state.content_matching_entry_ids, entry2.id); + assert.include(editor_state.content_matching_entry_ids, entry3.id); + assert.notInclude(editor_state.content_matching_entry_ids, entry1.id); }); }); @@ -211,34 +209,34 @@ describe('saving history changes', () => { test('save_changes persists content and converts unsaved to saved', async () => { // Make an edit to create unsaved entry editor_state.current_content = 'Content to save'; - expect(editor_state.unsaved_edit_entry_id).not.toBeNull(); + assert.ok(editor_state.unsaved_edit_entry_id !== null); // Save changes await editor_state.save_changes(); // Verify the unsaved flag was cleared - expect(editor_state.unsaved_edit_entry_id).toBeNull(); + assert.isNull(editor_state.unsaved_edit_entry_id); // A new entry should be created with correct properties const history = app.get_diskfile_history(TEST_PATH)!; - expect(history.entries[0]!).toMatchObject({ + assert.include(history.entries[0]!, { content: 'Content to save', is_unsaved_edit: false, }); // Selection should point to the new entry - expect(editor_state.selected_history_entry_id).toBe(history.entries[0]!.id); + assert.strictEqual(editor_state.selected_history_entry_id, history.entries[0]!.id); }); test('save_changes with no changes returns false', async () => { // Don't make any changes - expect(editor_state.has_changes).toBe(false); + assert.ok(!editor_state.has_changes); // Try to save const result = await editor_state.save_changes(); // Verify nothing was saved - expect(result).toBe(false); + assert.ok(!result); }); test('save_changes updates the diskfile content', async () => { @@ -249,10 +247,10 @@ describe('saving history changes', () => { await editor_state.save_changes(); // Verify diskfile was updated - expect(test_diskfile.content).toBe('New saved content'); + assert.strictEqual(test_diskfile.content, 'New saved content'); // Verify last_seen_disk_content was updated - expect(editor_state.last_seen_disk_content).toBe('New saved content'); + assert.strictEqual(editor_state.last_seen_disk_content, 'New saved content'); }); }); @@ -274,20 +272,20 @@ describe('managing unsaved edits', () => { const unsaved2_id = editor_state.unsaved_edit_entry_id; // Verify both unsaved entries exist - expect(unsaved1_id).not.toBeNull(); - expect(unsaved2_id).not.toBeNull(); - expect(unsaved1_id).not.toBe(unsaved2_id); + assert.ok(unsaved1_id !== null); + assert.ok(unsaved2_id !== null); + assert.notStrictEqual(unsaved1_id, unsaved2_id); // Verify both entries in history - const unsaved1 = history.find_entry_by_id(unsaved1_id!); - const unsaved2 = history.find_entry_by_id(unsaved2_id!); + const unsaved1 = history.find_entry_by_id(unsaved1_id); + const unsaved2 = history.find_entry_by_id(unsaved2_id); - expect(unsaved1).toMatchObject({ + assert.include(unsaved1, { content: 'Modified 1', is_unsaved_edit: true, }); - expect(unsaved2).toMatchObject({ + assert.include(unsaved2, { content: 'Modified 2', is_unsaved_edit: true, }); @@ -308,10 +306,10 @@ describe('managing unsaved edits', () => { // Verify all unsaved entries are gone const unsaved_after = history.entries.filter((e) => e.is_unsaved_edit); - expect(unsaved_after.length).toBe(0); + assert.strictEqual(unsaved_after.length, 0); // Unsaved edit id should be cleared - expect(editor_state.unsaved_edit_entry_id).toBeNull(); + assert.isNull(editor_state.unsaved_edit_entry_id); }); test('clear_unsaved_edits updates selection when selected entry is removed', () => { @@ -320,13 +318,13 @@ describe('managing unsaved edits', () => { const unsaved_id = editor_state.unsaved_edit_entry_id; // Verify it's selected - expect(editor_state.selected_history_entry_id).toBe(unsaved_id); + assert.strictEqual(editor_state.selected_history_entry_id, unsaved_id); // Clear unsaved edits editor_state.clear_unsaved_edits(); // Selection should be updated to a valid entry or null - expect(editor_state.selected_history_entry_id).not.toBe(unsaved_id); + assert.notStrictEqual(editor_state.selected_history_entry_id, unsaved_id); }); }); @@ -342,16 +340,16 @@ describe('history clearing', () => { editor_state.clear_history(); // Only one entry should remain - expect(history.entries.length).toBe(1); - expect(history.entries[0]).toMatchObject({ + assert.strictEqual(history.entries.length, 1); + assert.include(history.entries[0], { id: newest.id, content: 'Newest entry', is_original_state: true, }); // Selection should be updated - expect(editor_state.selected_history_entry_id).toBe(newest.id); - expect(editor_state.unsaved_edit_entry_id).toBeNull(); + assert.strictEqual(editor_state.selected_history_entry_id, newest.id); + assert.isNull(editor_state.unsaved_edit_entry_id); }); test('clear_history preserves all unsaved edits', () => { @@ -376,13 +374,13 @@ describe('history clearing', () => { editor_state.clear_history(); // Verify the specific unsaved entries still exist - expect(history.find_entry_by_id(unsaved_entry1.id)).toMatchObject({ + assert.include(history.find_entry_by_id(unsaved_entry1.id), { content: 'Unsaved edit 1', is_unsaved_edit: true, label: 'Unsaved 1', }); - expect(history.find_entry_by_id(unsaved_entry2.id)).toMatchObject({ + assert.include(history.find_entry_by_id(unsaved_entry2.id), { content: 'Unsaved edit 2', is_unsaved_edit: true, label: 'Unsaved 2', @@ -390,13 +388,13 @@ describe('history clearing', () => { // Verify the newest non-unsaved entry was also preserved const newest_after_clear = history.entries.find((entry) => !entry.is_unsaved_edit); - expect(newest_after_clear).toMatchObject({ + assert.include(newest_after_clear, { content: 'Newest entry', is_original_state: true, }); // Verify the original entry was removed (since it's not the newest saved entry) const original_entry = history.entries.find((entry) => entry.content === TEST_CONTENT); - expect(original_entry).toBeUndefined(); + assert.ok(original_entry === undefined); }); }); diff --git a/src/test/diskfile_history.svelte.test.ts b/src/test/diskfile_history.svelte.test.ts index e5b0b7d5a..1f255d029 100644 --- a/src/test/diskfile_history.svelte.test.ts +++ b/src/test/diskfile_history.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, beforeEach, describe} from 'vitest'; +import {test, beforeEach, describe, assert} from 'vitest'; import {DiskfileHistory} from '$lib/diskfile_history.svelte.js'; import {DiskfilePath} from '$lib/diskfile_types.js'; @@ -35,10 +33,10 @@ describe('DiskfileHistory', () => { describe('initialization', () => { test('creates empty history state', () => { - expect(history.path).toBe(TEST_PATH); - expect(history.entries).toEqual([]); - expect(history.max_entries).toBe(100); - expect(history.current_entry).toBe(null); + assert.strictEqual(history.path, TEST_PATH); + assert.deepEqual(history.entries, []); + assert.strictEqual(history.max_entries, 100); + assert.isNull(history.current_entry); }); }); @@ -47,13 +45,13 @@ describe('DiskfileHistory', () => { const entry = history.add_entry(TEST_CONTENT); // Verify entry was created with proper structure - expect(history.entries.length).toBe(1); - expect(entry.content).toBe(TEST_CONTENT); - expect(entry.id).toBeDefined(); - expect(typeof entry.created).toBe('number'); - expect(entry.is_disk_change).toBe(false); - expect(entry.is_unsaved_edit).toBe(false); - expect(entry.is_original_state).toBe(false); + assert.strictEqual(history.entries.length, 1); + assert.strictEqual(entry.content, TEST_CONTENT); + assert.isDefined(entry.id); + assert.strictEqual(typeof entry.created, 'number'); + assert.ok(!entry.is_disk_change); + assert.ok(!entry.is_unsaved_edit); + assert.ok(!entry.is_original_state); }); test('add_entry with custom options sets all properties', () => { @@ -68,11 +66,11 @@ describe('DiskfileHistory', () => { }); // Verify all options were applied - expect(entry.created).toBe(custom_timestamp); - expect(entry.is_disk_change).toBe(true); - expect(entry.is_unsaved_edit).toBe(true); - expect(entry.is_original_state).toBe(true); - expect(entry.label).toBe('Custom Label'); + assert.strictEqual(entry.created, custom_timestamp); + assert.ok(entry.is_disk_change); + assert.ok(entry.is_unsaved_edit); + assert.ok(entry.is_original_state); + assert.strictEqual(entry.label, 'Custom Label'); }); test('add_entry skips duplicate content back-to-back', () => { @@ -83,9 +81,9 @@ describe('DiskfileHistory', () => { const duplicate = history.add_entry(TEST_CONTENT); // Verify no new entry was added and original was returned - expect(history.entries.length).toBe(1); - expect(duplicate).toEqual(first); - expect(duplicate.id).toBe(first.id); + assert.strictEqual(history.entries.length, 1); + assert.deepEqual(duplicate, first); + assert.strictEqual(duplicate.id, first.id); }); test('add_entry creates immutable entry array', () => { @@ -96,8 +94,8 @@ describe('DiskfileHistory', () => { history.add_entry(TEST_CONTENT); // Verify entries array was replaced, not mutated in place - expect(history.entries).not.toBe(initial_entries); - expect(history.entries.length).toBe(1); + assert.notStrictEqual(history.entries, initial_entries); + assert.strictEqual(history.entries.length, 1); }); }); @@ -113,10 +111,10 @@ describe('DiskfileHistory', () => { history.add_entry('content 1', {created: time1}); // Verify entries are sorted newest first - expect(history.entries.length).toBe(3); - expect(history.entries[0]!.content).toBe('content 3'); - expect(history.entries[1]!.content).toBe('content 2'); - expect(history.entries[2]!.content).toBe('content 1'); + assert.strictEqual(history.entries.length, 3); + assert.strictEqual(history.entries[0]!.content, 'content 3'); + assert.strictEqual(history.entries[1]!.content, 'content 2'); + assert.strictEqual(history.entries[2]!.content, 'content 1'); }); test('current_entry returns most recent entry', () => { @@ -125,12 +123,12 @@ describe('DiskfileHistory', () => { const latest = history.add_entry('latest entry'); // Verify current_entry points to most recent - expect(history.current_entry).toBe(history.entries[0]); - expect(history.current_entry).toEqual(latest); + assert.strictEqual(history.current_entry, history.entries[0]); + assert.deepEqual(history.current_entry, latest); }); test('current_entry is null when history is empty', () => { - expect(history.current_entry).toBe(null); + assert.isNull(history.current_entry); }); }); @@ -146,10 +144,10 @@ describe('DiskfileHistory', () => { history.add_entry('content 4', {created: Date.now()}); // Verify only the most recent entries were kept - expect(history.entries.length).toBe(3); - expect(history.entries[0]!.content).toBe('content 4'); - expect(history.entries[1]!.content).toBe('content 3'); - expect(history.entries[2]!.content).toBe('content 2'); + assert.strictEqual(history.entries.length, 3); + assert.strictEqual(history.entries[0]!.content, 'content 4'); + assert.strictEqual(history.entries[1]!.content, 'content 3'); + assert.strictEqual(history.entries[2]!.content, 'content 2'); }); test('add_entry correctly handles insertion with capacity limit', () => { @@ -169,9 +167,9 @@ describe('DiskfileHistory', () => { history.add_entry('oldest entry', {created: oldest_time}); // Verify correct entries were kept (newest two) - expect(history.entries.length).toBe(2); - expect(history.entries[0]!.content).toBe('newest entry'); - expect(history.entries[1]!.content).toBe('middle entry'); + assert.strictEqual(history.entries.length, 2); + assert.strictEqual(history.entries[0]!.content, 'newest entry'); + assert.strictEqual(history.entries[1]!.content, 'middle entry'); }); }); @@ -186,9 +184,9 @@ describe('DiskfileHistory', () => { const found = history.find_entry_by_id(entry2.id); // Verify the right entry was found - expect(found).toBeDefined(); - expect(found!.id).toBe(entry2.id); - expect(found!.content).toBe('content 2'); + assert.isDefined(found); + assert.strictEqual(found.id, entry2.id); + assert.strictEqual(found.content, 'content 2'); }); test('find_entry_by_id returns undefined for non-existent id', () => { @@ -201,7 +199,7 @@ describe('DiskfileHistory', () => { const result = history.find_entry_by_id(unknown_id); // Verify undefined is returned - expect(result).toBeUndefined(); + assert.ok(result === undefined); }); test('get_content returns content from entry', () => { @@ -212,7 +210,7 @@ describe('DiskfileHistory', () => { const content = history.get_content(entry.id); // Verify content was retrieved - expect(content).toBe('specific content'); + assert.strictEqual(content, 'specific content'); }); test('get_content returns null for non-existent id', () => { @@ -224,7 +222,7 @@ describe('DiskfileHistory', () => { const content = history.get_content(unknown_id); // Verify null is returned - expect(content).toBeNull(); + assert.isNull(content); }); }); @@ -236,26 +234,26 @@ describe('DiskfileHistory', () => { const newest = history.add_entry('newest content'); // Verify we have multiple entries - expect(history.entries.length).toBe(3); + assert.strictEqual(history.entries.length, 3); // Clear all except current history.clear_except_current(); // Verify only newest remains - expect(history.entries.length).toBe(1); - expect(history.entries[0]!.id).toBe(newest.id); - expect(history.entries[0]!.content).toBe('newest content'); + assert.strictEqual(history.entries.length, 1); + assert.strictEqual(history.entries[0]!.id, newest.id); + assert.strictEqual(history.entries[0]!.content, 'newest content'); }); test('clear_except_current handles empty history', () => { // Start with empty history - expect(history.entries.length).toBe(0); + assert.strictEqual(history.entries.length, 0); // Call clear - should not error history.clear_except_current(); // Should still be empty - expect(history.entries.length).toBe(0); + assert.strictEqual(history.entries.length, 0); }); test('clear_except_current with keep predicate preserves matching entries', () => { @@ -268,9 +266,9 @@ describe('DiskfileHistory', () => { history.clear_except_current((entry) => entry.is_original_state); // Verify newest and original entries were kept - expect(history.entries.length).toBe(2); - expect(history.entries[0]!.id).toBe(newest.id); - expect(history.entries[1]!.id).toBe(original.id); + assert.strictEqual(history.entries.length, 2); + assert.strictEqual(history.entries[0]!.id, newest.id); + assert.strictEqual(history.entries[1]!.id, original.id); }); test('clear_except_current with single entry does nothing', () => { @@ -281,8 +279,8 @@ describe('DiskfileHistory', () => { history.clear_except_current(); // Verify entry is still there - expect(history.entries.length).toBe(1); - expect(history.entries[0]!.id).toBe(entry.id); + assert.strictEqual(history.entries.length, 1); + assert.strictEqual(history.entries[0]!.id, entry.id); }); }); @@ -295,8 +293,8 @@ describe('DiskfileHistory', () => { const second = history.add_entry(TEST_CONTENT, {is_unsaved_edit: true}); // Both entries should be added since they represent different states - expect(history.entries.length).toBe(2); - expect(second.is_unsaved_edit).toBe(true); + assert.strictEqual(history.entries.length, 2); + assert.ok(second.is_unsaved_edit); }); test('complex editing workflow', () => { @@ -317,17 +315,17 @@ describe('DiskfileHistory', () => { const latest = history.add_entry('latest edit', {is_unsaved_edit: true}); // Verify state - expect(history.entries.length).toBe(5); - expect(history.current_entry).toEqual(latest); + assert.strictEqual(history.entries.length, 5); + assert.deepEqual(history.current_entry, latest); // Clear except saved and current history.clear_except_current((entry) => entry.is_disk_change); // Should have original, saved, and latest - expect(history.entries.length).toBe(3); - expect(history.entries[0]!.id).toBe(latest.id); - expect(history.entries[1]!.id).toBe(saved.id); - expect(history.entries[2]!.id).toBe(original.id); + assert.strictEqual(history.entries.length, 3); + assert.strictEqual(history.entries[0]!.id, latest.id); + assert.strictEqual(history.entries[1]!.id, saved.id); + assert.strictEqual(history.entries[2]!.id, original.id); }); }); }); diff --git a/src/test/diskfile_tabs.svelte.test.ts b/src/test/diskfile_tabs.svelte.test.ts index 31fee0b30..6593b37c5 100644 --- a/src/test/diskfile_tabs.svelte.test.ts +++ b/src/test/diskfile_tabs.svelte.test.ts @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import {test, expect, beforeEach, describe} from 'vitest'; +import {test, beforeEach, describe, assert} from 'vitest'; import {DiskfileTabs} from '$lib/diskfile_tabs.svelte.js'; import {DiskfileTab} from '$lib/diskfile_tab.svelte.js'; @@ -35,15 +35,15 @@ describe('DiskfileTabs', () => { describe('initialization', () => { test('creates empty tabs state', () => { - expect(tabs.selected_tab_id).toBe(null); - expect(tabs.preview_tab_id).toBe(null); - expect(tabs.tab_order).toEqual([]); - expect(tabs.items.size).toBe(0); - expect(tabs.ordered_tabs).toEqual([]); - expect(tabs.selected_tab).toBeUndefined(); - expect(tabs.preview_tab).toBeUndefined(); - expect(tabs.selected_diskfile_id).toBe(null); - expect(tabs.recently_closed_tabs).toEqual([]); + assert.isNull(tabs.selected_tab_id); + assert.isNull(tabs.preview_tab_id); + assert.deepEqual(tabs.tab_order, []); + assert.strictEqual(tabs.items.size, 0); + assert.deepEqual(tabs.ordered_tabs, []); + assert.ok(tabs.selected_tab === undefined); + assert.ok(tabs.preview_tab === undefined); + assert.isNull(tabs.selected_diskfile_id); + assert.deepEqual(tabs.recently_closed_tabs, []); }); }); @@ -51,12 +51,12 @@ describe('DiskfileTabs', () => { test('preview_diskfile creates a new preview tab', () => { const tab = tabs.preview_diskfile(TEST_DISKFILE_ID_1); - expect(tab).toBeInstanceOf(DiskfileTab); - expect(tab.diskfile_id).toBe(TEST_DISKFILE_ID_1); - expect(tabs.preview_tab_id).toBe(tab.id); - expect(tabs.selected_tab_id).toBe(tab.id); - expect(tabs.tab_order).toContain(tab.id); - expect(tabs.items.size).toBe(1); + assert.instanceOf(tab, DiskfileTab); + assert.strictEqual(tab.diskfile_id, TEST_DISKFILE_ID_1); + assert.strictEqual(tabs.preview_tab_id, tab.id); + assert.strictEqual(tabs.selected_tab_id, tab.id); + assert.include(tabs.tab_order, tab.id); + assert.strictEqual(tabs.items.size, 1); }); test('preview_diskfile reuses existing tab', () => { @@ -67,8 +67,8 @@ describe('DiskfileTabs', () => { const tab2 = tabs.preview_diskfile(TEST_DISKFILE_ID_1); // Should return the existing tab, not create a new one - expect(tab2).toBe(tab1); - expect(tabs.items.size).toBe(1); + assert.strictEqual(tab2, tab1); + assert.strictEqual(tabs.items.size, 1); }); test('preview_diskfile reuses existing preview tab for new file', () => { @@ -79,21 +79,21 @@ describe('DiskfileTabs', () => { const result = tabs.preview_diskfile(TEST_DISKFILE_ID_2); // Should reuse the same preview tab but update its content - expect(result).toBe(preview_tab); - expect(tabs.preview_tab_id).toBe(preview_tab.id); - expect(preview_tab.diskfile_id).toBe(TEST_DISKFILE_ID_2); - expect(tabs.items.size).toBe(1); + assert.strictEqual(result, preview_tab); + assert.strictEqual(tabs.preview_tab_id, preview_tab.id); + assert.strictEqual(preview_tab.diskfile_id, TEST_DISKFILE_ID_2); + assert.strictEqual(tabs.items.size, 1); }); test('open_diskfile creates a permanent tab', () => { const tab = tabs.open_diskfile(TEST_DISKFILE_ID_1); - expect(tab).toBeInstanceOf(DiskfileTab); - expect(tab.diskfile_id).toBe(TEST_DISKFILE_ID_1); - expect(tab.is_preview).toBe(false); - expect(tabs.preview_tab_id).toBe(null); - expect(tabs.selected_tab_id).toBe(tab.id); - expect(tabs.tab_order).toContain(tab.id); + assert.instanceOf(tab, DiskfileTab); + assert.strictEqual(tab.diskfile_id, TEST_DISKFILE_ID_1); + assert.ok(!tab.is_preview); + assert.isNull(tabs.preview_tab_id); + assert.strictEqual(tabs.selected_tab_id, tab.id); + assert.include(tabs.tab_order, tab.id); }); test('open_diskfile reuses existing tab', () => { @@ -104,8 +104,8 @@ describe('DiskfileTabs', () => { const tab2 = tabs.open_diskfile(TEST_DISKFILE_ID_1); // Should return the existing tab, not create a new one - expect(tab2).toBe(tab1); - expect(tabs.items.size).toBe(1); + assert.strictEqual(tab2, tab1); + assert.strictEqual(tabs.items.size, 1); }); test('open_diskfile promotes preview tab to permanent', () => { @@ -116,9 +116,9 @@ describe('DiskfileTabs', () => { const permanent_tab = tabs.open_diskfile(TEST_DISKFILE_ID_1); // Should use the same tab but make it permanent - expect(permanent_tab).toBe(preview_tab); - expect(tabs.preview_tab_id).toBe(null); - expect(tabs.items.size).toBe(1); + assert.strictEqual(permanent_tab, preview_tab); + assert.isNull(tabs.preview_tab_id); + assert.strictEqual(tabs.items.size, 1); }); test('open_diskfile repurposes existing preview tab', () => { @@ -129,39 +129,39 @@ describe('DiskfileTabs', () => { const permanent_tab = tabs.open_diskfile(TEST_DISKFILE_ID_2); // Should repurpose the preview tab - expect(permanent_tab).toBe(preview_tab); - expect(permanent_tab.diskfile_id).toBe(TEST_DISKFILE_ID_2); - expect(tabs.preview_tab_id).toBe(null); - expect(tabs.items.size).toBe(1); + assert.strictEqual(permanent_tab, preview_tab); + assert.strictEqual(permanent_tab.diskfile_id, TEST_DISKFILE_ID_2); + assert.isNull(tabs.preview_tab_id); + assert.strictEqual(tabs.items.size, 1); }); test('open_diskfile replaces preview tab resulting in 2 tabs total', () => { // Create a permanent tab first const tab1 = tabs.open_diskfile(TEST_DISKFILE_ID_1); - expect(tabs.items.size).toBe(1); - expect(tabs.preview_tab_id).toBe(null); + assert.strictEqual(tabs.items.size, 1); + assert.isNull(tabs.preview_tab_id); // Create a preview tab for a different file const preview_tab = tabs.preview_diskfile(TEST_DISKFILE_ID_2); - expect(tabs.items.size).toBe(2); - expect(tabs.preview_tab_id).toBe(preview_tab.id); - expect(preview_tab.is_preview).toBe(true); + assert.strictEqual(tabs.items.size, 2); + assert.strictEqual(tabs.preview_tab_id, preview_tab.id); + assert.ok(preview_tab.is_preview); // Open a third file, which should repurpose the preview tab const tab3 = tabs.open_diskfile(TEST_DISKFILE_ID_3); // Verify the preview tab was repurposed, not creating a third tab - expect(tabs.items.size).toBe(2); - expect(tab3).toBe(preview_tab); // Should be the same tab object - expect(tab3.diskfile_id).toBe(TEST_DISKFILE_ID_3); // But with the new diskfile id - expect(tabs.preview_tab_id).toBe(null); // No preview tab now + assert.strictEqual(tabs.items.size, 2); + assert.strictEqual(tab3, preview_tab); // Should be the same tab object + assert.strictEqual(tab3.diskfile_id, TEST_DISKFILE_ID_3); // But with the new diskfile id + assert.isNull(tabs.preview_tab_id); // No preview tab now // Verify tab ids are different - expect(tab1.id).not.toBe(tab3.id); + assert.notStrictEqual(tab1.id, tab3.id); // Verify tabs have the right content - expect(tab1.diskfile_id).toBe(TEST_DISKFILE_ID_1); - expect(tab3.diskfile_id).toBe(TEST_DISKFILE_ID_3); + assert.strictEqual(tab1.diskfile_id, TEST_DISKFILE_ID_1); + assert.strictEqual(tab3.diskfile_id, TEST_DISKFILE_ID_3); }); }); @@ -178,9 +178,9 @@ describe('DiskfileTabs', () => { const preview_tab = tabs.preview_diskfile(TEST_DISKFILE_ID_3); // Verify the order: tab1, preview_tab, tab2 - expect(tabs.tab_order[0]!).toBe(tab1.id); - expect(tabs.tab_order[1]!).toBe(preview_tab.id); - expect(tabs.tab_order[2]!).toBe(tab2.id); + assert.strictEqual(tabs.tab_order[0]!, tab1.id); + assert.strictEqual(tabs.tab_order[1]!, preview_tab.id); + assert.strictEqual(tabs.tab_order[2]!, tab2.id); }); test('positioning with additional preview tabs', () => { @@ -195,20 +195,20 @@ describe('DiskfileTabs', () => { const preview = tabs.preview_diskfile(TEST_DISKFILE_ID_3); // Expected order: tab1, preview, tab2 - expect(tabs.tab_order[0]!).toBe(tab1.id); - expect(tabs.tab_order[1]!).toBe(preview.id); - expect(tabs.tab_order[2]!).toBe(tab2.id); + assert.strictEqual(tabs.tab_order[0]!, tab1.id); + assert.strictEqual(tabs.tab_order[1]!, preview.id); + assert.strictEqual(tabs.tab_order[2]!, tab2.id); // Select tab2 and create another preview (reusing the existing one) tabs.select_tab(tab2.id); const preview2 = tabs.preview_diskfile(TEST_DISKFILE_ID_4); // The preview tab should move after tab2 - expect(tabs.tab_order[0]!).toBe(tab1.id); - expect(tabs.tab_order[1]!).toBe(tab2.id); - expect(tabs.tab_order[2]!).toBe(preview.id); - expect(preview2).toBe(preview); // Same tab instance - expect(preview2.diskfile_id).toBe(TEST_DISKFILE_ID_4); + assert.strictEqual(tabs.tab_order[0]!, tab1.id); + assert.strictEqual(tabs.tab_order[1]!, tab2.id); + assert.strictEqual(tabs.tab_order[2]!, preview.id); + assert.strictEqual(preview2, preview); // Same tab instance + assert.strictEqual(preview2.diskfile_id, TEST_DISKFILE_ID_4); }); test('preview tab positioning bug fix - selecting existing preview should not reorder', () => { @@ -228,7 +228,7 @@ describe('DiskfileTabs', () => { const preview_tab = tabs.preview_diskfile(TEST_DISKFILE_ID_3); // Order should be: tab1, preview_tab, tab2 - expect(tabs.tab_order).toEqual([tab1.id, preview_tab.id, tab2.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, preview_tab.id, tab2.id]); // Select tab2 tabs.select_tab(tab2.id); @@ -237,7 +237,7 @@ describe('DiskfileTabs', () => { tabs.select_tab(preview_tab.id); // Order should NOT change - the preview tab should stay in its position - expect(tabs.tab_order).toEqual([tab1.id, preview_tab.id, tab2.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, preview_tab.id, tab2.id]); // Select tab1 again tabs.select_tab(tab1.id); @@ -246,7 +246,7 @@ describe('DiskfileTabs', () => { tabs.select_tab(preview_tab.id); // Order should still not change - expect(tabs.tab_order).toEqual([tab1.id, preview_tab.id, tab2.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, preview_tab.id, tab2.id]); }); test('preview tab repositioning only happens when content changes', () => { @@ -260,17 +260,17 @@ describe('DiskfileTabs', () => { const preview = tabs.preview_diskfile(TEST_DISKFILE_ID_4); // Order: tab1, preview, tab2, tab3 - expect(tabs.tab_order).toEqual([tab1.id, preview.id, tab2.id, tab3.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, preview.id, tab2.id, tab3.id]); // Select tab3 tabs.select_tab(tab3.id); // Preview the same file - should NOT reposition tabs.preview_diskfile(TEST_DISKFILE_ID_4); - expect(tabs.tab_order).toEqual([tab1.id, preview.id, tab2.id, tab3.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, preview.id, tab2.id, tab3.id]); tabs.preview_diskfile(TEST_DISKFILE_ID_5); - expect(tabs.tab_order).toEqual([tab1.id, preview.id, tab2.id, tab3.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, preview.id, tab2.id, tab3.id]); }); test('reorder_tabs changes tab order', () => { @@ -280,18 +280,18 @@ describe('DiskfileTabs', () => { const tab3 = tabs.open_diskfile(TEST_DISKFILE_ID_3); // Initial order - expect(tabs.tab_order).toEqual([tab1.id, tab2.id, tab3.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, tab2.id, tab3.id]); // Reorder: move tab1 to position 2 tabs.reorder_tabs(0, 2); // New order should be: tab2, tab3, tab1 - expect(tabs.tab_order).toEqual([tab2.id, tab3.id, tab1.id]); + assert.deepEqual(tabs.tab_order, [tab2.id, tab3.id, tab1.id]); // ordered_tabs should reflect the new order - expect(tabs.ordered_tabs[0]!.id).toBe(tab2.id); - expect(tabs.ordered_tabs[1]!.id).toBe(tab3.id); - expect(tabs.ordered_tabs[2]!.id).toBe(tab1.id); + assert.strictEqual(tabs.ordered_tabs[0]!.id, tab2.id); + assert.strictEqual(tabs.ordered_tabs[1]!.id, tab3.id); + assert.strictEqual(tabs.ordered_tabs[2]!.id, tab1.id); }); }); @@ -304,10 +304,10 @@ describe('DiskfileTabs', () => { // Select the second tab tabs.select_tab(tab2.id); - expect(tabs.selected_tab_id).toBe(tab2.id); - expect(tabs.selected_tab).toBe(tab2); - expect(tabs.selected_tab?.is_selected).toBe(true); - expect(tabs.selected_diskfile_id).toBe(tab2.diskfile_id); + assert.strictEqual(tabs.selected_tab_id, tab2.id); + assert.strictEqual(tabs.selected_tab, tab2); + assert.ok(tabs.selected_tab?.is_selected); + assert.strictEqual(tabs.selected_diskfile_id, tab2.diskfile_id); }); }); @@ -319,11 +319,11 @@ describe('DiskfileTabs', () => { // Close it tabs.close_tab(tab.id); - expect(tabs.items.size).toBe(0); - expect(tabs.tab_order).not.toContain(tab.id); - expect(tabs.selected_tab_id).toBe(null); - expect(tabs.recently_closed_tabs).toHaveLength(1); - expect(tabs.recently_closed_tabs[0]!.id).toBe(tab.id); + assert.strictEqual(tabs.items.size, 0); + assert.notInclude(tabs.tab_order, tab.id); + assert.isNull(tabs.selected_tab_id); + assert.strictEqual(tabs.recently_closed_tabs.length, 1); + assert.strictEqual(tabs.recently_closed_tabs[0]!.id, tab.id); }); test('close_tab with multiple tabs selects the most recently opened tab', () => { @@ -332,16 +332,16 @@ describe('DiskfileTabs', () => { const tab2 = tabs.open_diskfile(TEST_DISKFILE_ID_2); const tab3 = tabs.open_diskfile(TEST_DISKFILE_ID_3); - expect(tabs.items.size).toBe(3); + assert.strictEqual(tabs.items.size, 3); // Select and close the middle tab tabs.select_tab(tab2.id); tabs.close_tab(tab2.id); - expect(tabs.items.size).toBe(2); - expect(tabs.selected_tab_id).toBe(tab3.id); // Should select the most recently opened (tab3) - expect(tabs.recently_closed_tabs).toHaveLength(1); - expect(tabs.recently_closed_tabs[0]!.id).toBe(tab2.id); + assert.strictEqual(tabs.items.size, 2); + assert.strictEqual(tabs.selected_tab_id, tab3.id); // Should select the most recently opened (tab3) + assert.strictEqual(tabs.recently_closed_tabs.length, 1); + assert.strictEqual(tabs.recently_closed_tabs[0]!.id, tab2.id); }); test('close_tab does nothing for non-existent tab', () => { @@ -356,9 +356,9 @@ describe('DiskfileTabs', () => { tabs.close_tab(create_uuid()); // State should be unchanged - expect(tabs.items.size).toBe(initial_size); - expect(tabs.selected_tab_id).toBe(initial_selected); - expect(tabs.recently_closed_tabs).toHaveLength(0); + assert.strictEqual(tabs.items.size, initial_size); + assert.strictEqual(tabs.selected_tab_id, initial_selected); + assert.strictEqual(tabs.recently_closed_tabs.length, 0); }); test('close_tab clears preview_tab_id if closing preview tab', () => { @@ -368,7 +368,7 @@ describe('DiskfileTabs', () => { // Close it tabs.close_tab(preview_tab.id); - expect(tabs.preview_tab_id).toBe(null); + assert.isNull(tabs.preview_tab_id); }); test('close_tab selects next tab when available', () => { @@ -384,7 +384,7 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab2.id); // Should select the tab that was after it (tab3) - expect(tabs.selected_tab_id).toBe(tab3.id); + assert.strictEqual(tabs.selected_tab_id, tab3.id); }); test('close_tab selects previous tab when closing last tab', () => { @@ -399,7 +399,7 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab2.id); // Should select the previous tab - expect(tabs.selected_tab_id).toBe(tab1.id); + assert.strictEqual(tabs.selected_tab_id, tab1.id); }); test('close_all_tabs clears all tabs and state', () => { @@ -416,20 +416,20 @@ describe('DiskfileTabs', () => { const permanent = tabs.open_diskfile(TEST_DISKFILE_ID_3); // Initial state - verify we have 3 tabs before closing - expect(tabs.items.size).toBe(3); - expect(tabs.tab_order).toHaveLength(3); - expect(tabs.preview_tab_id).toBe(null); // No preview tab now since last one is permanent - expect(tabs.selected_tab_id).toBe(permanent.id); + assert.strictEqual(tabs.items.size, 3); + assert.strictEqual(tabs.tab_order.length, 3); + assert.isNull(tabs.preview_tab_id); // No preview tab now since last one is permanent + assert.strictEqual(tabs.selected_tab_id, permanent.id); // Close all tabs.close_all_tabs(); // All state should be cleared - expect(tabs.items.size).toBe(0); - expect(tabs.tab_order).toHaveLength(0); - expect(tabs.preview_tab_id).toBe(null); - expect(tabs.selected_tab_id).toBe(null); - expect(tabs.recently_closed_tabs).toHaveLength(3); + assert.strictEqual(tabs.items.size, 0); + assert.strictEqual(tabs.tab_order.length, 0); + assert.isNull(tabs.preview_tab_id); + assert.isNull(tabs.selected_tab_id); + assert.strictEqual(tabs.recently_closed_tabs.length, 3); }); }); @@ -438,15 +438,15 @@ describe('DiskfileTabs', () => { // Create a preview tab const preview_tab = tabs.preview_diskfile(TEST_DISKFILE_ID_1); - expect(tabs.preview_tab_id).toBe(preview_tab.id); - expect(preview_tab.is_preview).toBe(true); + assert.strictEqual(tabs.preview_tab_id, preview_tab.id); + assert.ok(preview_tab.is_preview); // Promote it const result = tabs.promote_preview_to_permanent(); - expect(result).toBe(true); - expect(tabs.preview_tab_id).toBe(null); - expect(preview_tab.is_preview).toBe(false); + assert.ok(result); + assert.isNull(tabs.preview_tab_id); + assert.ok(!preview_tab.is_preview); }); test('promote_preview_to_permanent returns false if no preview tab', () => { @@ -456,19 +456,19 @@ describe('DiskfileTabs', () => { // Try to promote const result = tabs.promote_preview_to_permanent(); - expect(result).toBe(false); + assert.ok(!result); }); test('open_tab makes a preview tab permanent', () => { // Create a preview tab const preview_tab = tabs.preview_diskfile(TEST_DISKFILE_ID_1); - expect(tabs.preview_tab_id).toBe(preview_tab.id); + assert.strictEqual(tabs.preview_tab_id, preview_tab.id); // Make it permanent tabs.open_tab(preview_tab.id); - expect(tabs.preview_tab_id).toBe(null); + assert.isNull(tabs.preview_tab_id); }); test('open_tab does nothing for permanent tab', () => { @@ -476,13 +476,13 @@ describe('DiskfileTabs', () => { const tab = tabs.open_diskfile(TEST_DISKFILE_ID_1); // Initial state - expect(tabs.preview_tab_id).toBe(null); + assert.isNull(tabs.preview_tab_id); // Try to make it permanent again tabs.open_tab(tab.id); // State should be unchanged - expect(tabs.preview_tab_id).toBe(null); + assert.isNull(tabs.preview_tab_id); }); }); @@ -498,22 +498,22 @@ describe('DiskfileTabs', () => { // Reopen the last closed (tab2) tabs.reopen_last_closed_tab(); - expect(tabs.items.size).toBe(1); - expect(tabs.items.by_id.values().next().value?.diskfile_id).toBe(TEST_DISKFILE_ID_2); - expect(tabs.recently_closed_tabs).toHaveLength(1); + assert.strictEqual(tabs.items.size, 1); + assert.strictEqual(tabs.items.by_id.values().next().value?.diskfile_id, TEST_DISKFILE_ID_2); + assert.strictEqual(tabs.recently_closed_tabs.length, 1); }); test('reopen_last_closed_tab does nothing if no closed tabs', () => { // Initial state - expect(tabs.recently_closed_tabs).toHaveLength(0); - expect(tabs.items.size).toBe(0); + assert.strictEqual(tabs.recently_closed_tabs.length, 0); + assert.strictEqual(tabs.items.size, 0); // Try to reopen tabs.reopen_last_closed_tab(); // State should be unchanged - expect(tabs.recently_closed_tabs).toHaveLength(0); - expect(tabs.items.size).toBe(0); + assert.strictEqual(tabs.recently_closed_tabs.length, 0); + assert.strictEqual(tabs.items.size, 0); }); test('reopen_last_closed_tab restores selection state', () => { @@ -525,14 +525,14 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab.id); // Initial state after closing - expect(tabs.selected_tab_id).toBe(null); + assert.isNull(tabs.selected_tab_id); // Reopen it tabs.reopen_last_closed_tab(); // Tab should be reopened and selected - expect(tabs.selected_tab_id).not.toBe(null); - expect(tabs.items.size).toBe(1); + assert.notStrictEqual(tabs.selected_tab_id, null); + assert.strictEqual(tabs.items.size, 1); }); test('reopen_last_closed_tab maintains proper tab order', () => { @@ -543,25 +543,25 @@ describe('DiskfileTabs', () => { // Remember the original order const original_order = [...tabs.tab_order]; - expect(original_order).toEqual([tab1.id, tab2.id, tab3.id]); + assert.deepEqual(original_order, [tab1.id, tab2.id, tab3.id]); // Close the middle tab tabs.close_tab(tab2.id); // Make sure it's gone - expect(tabs.items.by_id.has(tab2.id)).toBe(false); - expect(tabs.tab_order).not.toContain(tab2.id); + assert.ok(!tabs.items.by_id.has(tab2.id)); + assert.notInclude(tabs.tab_order, tab2.id); // Verify the new order is tab1, tab3 - expect(tabs.tab_order).toEqual([tab1.id, tab3.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, tab3.id]); // Reopen the tab tabs.reopen_last_closed_tab(); // Verify it was added at the end of the tab order const reopened_tab_id = tabs.selected_tab_id; - expect(tabs.tab_order).toContain(reopened_tab_id); - expect(tabs.tab_order[tabs.tab_order.length - 1]).toBe(reopened_tab_id); + assert.include(tabs.tab_order, reopened_tab_id); + assert.strictEqual(tabs.tab_order[tabs.tab_order.length - 1], reopened_tab_id); }); }); @@ -573,17 +573,17 @@ describe('DiskfileTabs', () => { const tab3 = tabs.open_diskfile(TEST_DISKFILE_ID_3); // Initial history should have tab3 (most recently opened) - expect(tabs.recent_tabs[0]!.id).toBe(tab3.id); + assert.strictEqual(tabs.recent_tabs[0]!.id, tab3.id); // Select tab1, should move to front of history tabs.select_tab(tab1.id); - expect(tabs.recent_tabs[0]!.id).toBe(tab1.id); + assert.strictEqual(tabs.recent_tabs[0]!.id, tab1.id); // Select tab2, should move to front of history tabs.select_tab(tab2.id); - expect(tabs.recent_tabs[0]!.id).toBe(tab2.id); - expect(tabs.recent_tabs[1]!.id).toBe(tab1.id); - expect(tabs.recent_tabs[2]!.id).toBe(tab3.id); + assert.strictEqual(tabs.recent_tabs[0]!.id, tab2.id); + assert.strictEqual(tabs.recent_tabs[1]!.id, tab1.id); + assert.strictEqual(tabs.recent_tabs[2]!.id, tab3.id); }); test('maintains history when reopening tabs', () => { @@ -595,8 +595,8 @@ describe('DiskfileTabs', () => { tabs.reopen_last_closed_tab(); // Verify the reopened tab is in history - expect(tabs.recent_tabs).toHaveLength(1); - expect(tabs.recent_tabs[0]!.id).toBe(tabs.selected_tab_id); + assert.strictEqual(tabs.recent_tabs.length, 1); + assert.strictEqual(tabs.recent_tabs[0]!.id, tabs.selected_tab_id); }); test('limits history to max size', () => { @@ -609,7 +609,7 @@ describe('DiskfileTabs', () => { } // Verify history is limited to max size - expect(tabs.recent_tabs).toHaveLength(3); + assert.strictEqual(tabs.recent_tabs.length, 3); }); test('removes closed tabs from history', () => { @@ -618,15 +618,15 @@ describe('DiskfileTabs', () => { const tab2 = tabs.open_diskfile(TEST_DISKFILE_ID_2); // Verify both tabs are in history through their ids - expect(tabs.recent_tab_ids).toContain(tab1.id); - expect(tabs.recent_tab_ids).toContain(tab2.id); + assert.include(tabs.recent_tab_ids, tab1.id); + assert.include(tabs.recent_tab_ids, tab2.id); // Close tab1 tabs.close_tab(tab1.id); // Verify tab1 is removed from history - expect(tabs.recent_tab_ids).not.toContain(tab1.id); - expect(tabs.recent_tab_ids).toContain(tab2.id); + assert.notInclude(tabs.recent_tab_ids, tab1.id); + assert.include(tabs.recent_tab_ids, tab2.id); }); test('clears history when closing all tabs', () => { @@ -635,13 +635,13 @@ describe('DiskfileTabs', () => { tabs.open_diskfile(TEST_DISKFILE_ID_2); // Verify history is not empty - expect(tabs.recent_tabs.length).toBeGreaterThan(0); + assert.ok(tabs.recent_tabs.length > 0); // Close all tabs tabs.close_all_tabs(); // Verify history is cleared - expect(tabs.recent_tabs).toEqual([]); + assert.deepEqual(tabs.recent_tabs, []); }); }); @@ -662,7 +662,7 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab1.id); // The most recently used tab (tab3) should be selected - expect(tabs.selected_tab_id).toBe(tab3.id); + assert.strictEqual(tabs.selected_tab_id, tab3.id); }); test('falls back to next tab when no history available', () => { @@ -684,7 +684,7 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab2.id); // Should fall back to the next tab (tab3) - expect(tabs.selected_tab_id).toBe(tab3.id); + assert.strictEqual(tabs.selected_tab_id, tab3.id); }); test('selecting a non-existent tab in history is handled gracefully', () => { @@ -698,7 +698,7 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab2.id); // Should select tab1 since it's the only one left - expect(tabs.selected_tab_id).toBe(tab1.id); + assert.strictEqual(tabs.selected_tab_id, tab1.id); }); test('find_most_recent_tab correctly finds valid tabs', () => { @@ -714,7 +714,7 @@ describe('DiskfileTabs', () => { const result = tabs.find_most_recent_tab(tab2.id); // Should return tab1 - expect(result).toBe(tab1.id); + assert.strictEqual(result, tab1.id); }); test('tab history preserves references after tab modifications', () => { @@ -727,8 +727,8 @@ describe('DiskfileTabs', () => { tabs.select_tab(tab2.id); // Verify initial history state - expect(tabs.recent_tabs[0]!.id).toBe(tab2.id); - expect(tabs.recent_tabs[1]!.id).toBe(tab1.id); + assert.strictEqual(tabs.recent_tabs[0]!.id, tab2.id); + assert.strictEqual(tabs.recent_tabs[1]!.id, tab1.id); // Store tabs for reference before closing const tab1_diskfile_id = tab1.diskfile_id; @@ -737,8 +737,8 @@ describe('DiskfileTabs', () => { tabs.close_tab(tab2.id); // Check history - tab1 should still be accessible - expect(tabs.recent_tabs[0]!.id).toBe(tab1.id); - expect(tabs.recent_tabs[0]!.diskfile_id).toBe(tab1_diskfile_id); + assert.strictEqual(tabs.recent_tabs[0]!.id, tab1.id); + assert.strictEqual(tabs.recent_tabs[0]!.diskfile_id, tab1_diskfile_id); }); }); @@ -752,9 +752,9 @@ describe('DiskfileTabs', () => { const result = tabs.navigate_to_tab(tab2.id); // Should select tab2 directly - expect(result.resulting_tab_id).toBe(tab2.id); - expect(result.created_preview).toBe(false); - expect(tabs.selected_tab_id).toBe(tab2.id); + assert.strictEqual(result.resulting_tab_id, tab2.id); + assert.ok(!result.created_preview); + assert.strictEqual(tabs.selected_tab_id, tab2.id); }); test('navigate_to_tab creates preview tab for closed tab', () => { @@ -767,14 +767,14 @@ describe('DiskfileTabs', () => { const result = tabs.navigate_to_tab(tab_id); // Should create a preview tab for the same diskfile - expect(result.created_preview).toBe(true); - expect(result.resulting_tab_id).not.toBe(tab_id); // Should be a different tab id - expect(tabs.selected_tab_id).toBe(result.resulting_tab_id); - expect(tabs.preview_tab_id).toBe(result.resulting_tab_id); + assert.ok(result.created_preview); + assert.notStrictEqual(result.resulting_tab_id, tab_id); // Should be a different tab id + assert.strictEqual(tabs.selected_tab_id, result.resulting_tab_id); + assert.strictEqual(tabs.preview_tab_id, result.resulting_tab_id); // Should have the same diskfile const new_tab = tabs.items.by_id.get(result.resulting_tab_id!); - expect(new_tab?.diskfile_id).toBe(TEST_DISKFILE_ID_1); + assert.strictEqual(new_tab?.diskfile_id, TEST_DISKFILE_ID_1); }); test('navigate_to_tab creates a new preview tab for closed tab', () => { @@ -792,16 +792,19 @@ describe('DiskfileTabs', () => { const result = tabs.navigate_to_tab(closed_tab_id); // A new preview tab should be created for the closed tab's file - expect(result.created_preview).toBe(true); - expect(result.resulting_tab_id).not.toBeNull(); - expect(tabs.preview_tab_id).not.toBeNull(); + assert.ok(result.created_preview); + assert.ok(result.resulting_tab_id !== null); + assert.ok(tabs.preview_tab_id !== null); // The new preview tab should be different from the original one - expect(tabs.preview_tab_id).not.toBe(preview_tab_id); + assert.notStrictEqual(tabs.preview_tab_id, preview_tab_id); // But it should have the closed tab's diskfile if (tabs.preview_tab_id && tabs.items.by_id.get(tabs.preview_tab_id)) { - expect(tabs.items.by_id.get(tabs.preview_tab_id)?.diskfile_id).toBe(TEST_DISKFILE_ID_3); + assert.strictEqual( + tabs.items.by_id.get(tabs.preview_tab_id)?.diskfile_id, + TEST_DISKFILE_ID_3, + ); } }); @@ -813,8 +816,8 @@ describe('DiskfileTabs', () => { const result = tabs.navigate_to_tab(UuidWithDefault.parse(undefined)); // Should return null without changing selection - expect(result.resulting_tab_id).toBe(null); - expect(result.created_preview).toBe(false); + assert.isNull(result.resulting_tab_id); + assert.ok(!result.created_preview); }); test('closed tabs are remembered even after closing all tabs', () => { @@ -830,9 +833,9 @@ describe('DiskfileTabs', () => { const result = tabs.navigate_to_tab(tab1_id); // Should create a preview tab for the correct diskfile - expect(result.created_preview).toBe(true); + assert.ok(result.created_preview); const new_tab = tabs.items.by_id.get(result.resulting_tab_id!); - expect(new_tab?.diskfile_id).toBe(TEST_DISKFILE_ID_1); + assert.strictEqual(new_tab?.diskfile_id, TEST_DISKFILE_ID_1); }); }); @@ -846,44 +849,44 @@ describe('DiskfileTabs', () => { tabs.tab_order = [tab1.id]; // ordered_tabs should include both tabs - expect(tabs.ordered_tabs).toHaveLength(2); - expect(tabs.ordered_tabs[0]!.id).toBe(tab1.id); - expect(tabs.ordered_tabs[1]!.id).toBe(tab2.id); + assert.strictEqual(tabs.ordered_tabs.length, 2); + assert.strictEqual(tabs.ordered_tabs[0]!.id, tab1.id); + assert.strictEqual(tabs.ordered_tabs[1]!.id, tab2.id); }); test('complex tab workflow', () => { // Create a preview tab const preview = tabs.preview_diskfile(TEST_DISKFILE_ID_1); - expect(tabs.preview_tab_id).toBe(preview.id); - expect(tabs.selected_tab_id).toBe(preview.id); + assert.strictEqual(tabs.preview_tab_id, preview.id); + assert.strictEqual(tabs.selected_tab_id, preview.id); // Create a permanent tab const permanent = tabs.open_diskfile(TEST_DISKFILE_ID_2); - expect(tabs.preview_tab_id).toBe(null); // Preview was repurposed - expect(tabs.selected_tab_id).toBe(permanent.id); + assert.isNull(tabs.preview_tab_id); // Preview was repurposed + assert.strictEqual(tabs.selected_tab_id, permanent.id); // Create another preview tab const preview2 = tabs.preview_diskfile(TEST_DISKFILE_ID_3); - expect(tabs.preview_tab_id).toBe(preview2.id); - expect(tabs.selected_tab_id).toBe(preview2.id); + assert.strictEqual(tabs.preview_tab_id, preview2.id); + assert.strictEqual(tabs.selected_tab_id, preview2.id); // Select the permanent tab tabs.select_tab(permanent.id); - expect(tabs.selected_tab_id).toBe(permanent.id); - expect(tabs.preview_tab_id).toBe(preview2.id); // Preview status unchanged + assert.strictEqual(tabs.selected_tab_id, permanent.id); + assert.strictEqual(tabs.preview_tab_id, preview2.id); // Preview status unchanged // Close the permanent tab tabs.close_tab(permanent.id); - expect(tabs.selected_tab_id).toBe(preview2.id); // First remaining tab selected + assert.strictEqual(tabs.selected_tab_id, preview2.id); // First remaining tab selected // Promote the preview tab tabs.promote_preview_to_permanent(); - expect(tabs.preview_tab_id).toBe(null); - expect(tabs.selected_tab_id).toBe(preview2.id); + assert.isNull(tabs.preview_tab_id); + assert.strictEqual(tabs.selected_tab_id, preview2.id); // Reopen the closed tab tabs.reopen_last_closed_tab(); - expect(tabs.items.size).toBe(2); + assert.strictEqual(tabs.items.size, 2); }); test('can handle many tabs efficiently', () => { @@ -901,7 +904,7 @@ describe('DiskfileTabs', () => { } // Verify all tabs were created - expect(tabs.items.size).toBe(tab_count); + assert.strictEqual(tabs.items.size, tab_count); // Time some operations to ensure they're reasonably fast const start_reorder = performance.now(); @@ -925,8 +928,8 @@ describe('DiskfileTabs', () => { // These aren't strict assertions since timing depends on the environment // Just ensure operations complete in a reasonable time - expect(tabs.items.size).toBe(0); - expect(tabs.recently_closed_tabs.length).toBe(tab_count); + assert.strictEqual(tabs.items.size, 0); + assert.strictEqual(tabs.recently_closed_tabs.length, tab_count); }); test('preview tab lifecycle with multiple operations', () => { @@ -936,27 +939,27 @@ describe('DiskfileTabs', () => { // Preview a file const preview = tabs.preview_diskfile(TEST_DISKFILE_ID_3); - expect(preview.is_preview).toBe(true); + assert.ok(preview.is_preview); // Double-click simulation - promote to permanent tabs.open_tab(preview.id); - expect(preview.is_preview).toBe(false); - expect(tabs.preview_tab_id).toBe(null); + assert.ok(!preview.is_preview); + assert.isNull(tabs.preview_tab_id); // Create a new preview const preview2 = tabs.preview_diskfile(TEST_DISKFILE_ID_4); - expect(preview2.is_preview).toBe(true); - expect(preview2.id).not.toBe(preview.id); + assert.ok(preview2.is_preview); + assert.notStrictEqual(preview2.id, preview.id); // Close the preview tabs.close_tab(preview2.id); - expect(tabs.preview_tab_id).toBe(null); + assert.isNull(tabs.preview_tab_id); // All permanent tabs should remain - expect(tabs.items.size).toBe(3); - expect(tabs.items.by_id.has(tab1.id)).toBe(true); - expect(tabs.items.by_id.has(tab2.id)).toBe(true); - expect(tabs.items.by_id.has(preview.id)).toBe(true); + assert.strictEqual(tabs.items.size, 3); + assert.ok(tabs.items.by_id.has(tab1.id)); + assert.ok(tabs.items.by_id.has(tab2.id)); + assert.ok(tabs.items.by_id.has(preview.id)); }); test('by_diskfile_id map updates correctly', () => { @@ -965,25 +968,25 @@ describe('DiskfileTabs', () => { const tab2 = tabs.open_diskfile(TEST_DISKFILE_ID_2); // Verify map contents - expect(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_1)).toBe(tab1); - expect(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_2)).toBe(tab2); - expect(tabs.by_diskfile_id.size).toBe(2); + assert.strictEqual(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_1), tab1); + assert.strictEqual(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_2), tab2); + assert.strictEqual(tabs.by_diskfile_id.size, 2); // Create preview that reuses a tab const preview = tabs.preview_diskfile(TEST_DISKFILE_ID_3); - expect(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_3)).toBe(preview); - expect(tabs.by_diskfile_id.size).toBe(3); + assert.strictEqual(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_3), preview); + assert.strictEqual(tabs.by_diskfile_id.size, 3); // Reuse preview for different file tabs.preview_diskfile(TEST_DISKFILE_ID_4); - expect(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_3)).toBeUndefined(); - expect(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_4)).toBe(preview); - expect(tabs.by_diskfile_id.size).toBe(3); + assert.ok(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_3) === undefined); + assert.strictEqual(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_4), preview); + assert.strictEqual(tabs.by_diskfile_id.size, 3); // Close a tab tabs.close_tab(tab1.id); - expect(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_1)).toBeUndefined(); - expect(tabs.by_diskfile_id.size).toBe(2); + assert.ok(tabs.by_diskfile_id.get(TEST_DISKFILE_ID_1) === undefined); + assert.strictEqual(tabs.by_diskfile_id.size, 2); }); }); @@ -995,14 +998,14 @@ describe('DiskfileTabs', () => { const tab3 = tabs.open_diskfile(TEST_DISKFILE_ID_3); // Initial order - expect(tabs.tab_order).toEqual([tab1.id, tab2.id, tab3.id]); + assert.deepEqual(tabs.tab_order, [tab1.id, tab2.id, tab3.id]); // Use private method through public API - reorder simulates position_tab behavior tabs.reorder_tabs(2, 0); // Move tab3 to position after tab1 (index 1) - expect(tabs.tab_order[0]).toBe(tab3.id); - expect(tabs.tab_order[1]).toBe(tab1.id); - expect(tabs.tab_order[2]).toBe(tab2.id); + assert.strictEqual(tabs.tab_order[0], tab3.id); + assert.strictEqual(tabs.tab_order[1], tab1.id); + assert.strictEqual(tabs.tab_order[2], tab2.id); }); test('#update_tab_history maintains correct order and size', () => { @@ -1022,19 +1025,19 @@ describe('DiskfileTabs', () => { tabs.select_tab(tab4.id); // History should only contain last 3 - expect(tabs.recent_tab_ids).toHaveLength(3); - expect(tabs.recent_tab_ids[0]!).toBe(tab4.id); - expect(tabs.recent_tab_ids[1]!).toBe(tab3.id); - expect(tabs.recent_tab_ids[2]!).toBe(tab2.id); - expect(tabs.recent_tab_ids).not.toContain(tab1.id); + assert.strictEqual(tabs.recent_tab_ids.length, 3); + assert.strictEqual(tabs.recent_tab_ids[0]!, tab4.id); + assert.strictEqual(tabs.recent_tab_ids[1]!, tab3.id); + assert.strictEqual(tabs.recent_tab_ids[2]!, tab2.id); + assert.notInclude(tabs.recent_tab_ids, tab1.id); // Select an existing tab in history tabs.select_tab(tab2.id); // Should move to front - expect(tabs.recent_tab_ids[0]!).toBe(tab2.id); - expect(tabs.recent_tab_ids[1]!).toBe(tab4.id); - expect(tabs.recent_tab_ids[2]!).toBe(tab3.id); + assert.strictEqual(tabs.recent_tab_ids[0]!, tab2.id); + assert.strictEqual(tabs.recent_tab_ids[1]!, tab4.id); + assert.strictEqual(tabs.recent_tab_ids[2]!, tab3.id); }); }); @@ -1047,22 +1050,22 @@ describe('DiskfileTabs', () => { // Every tab in tab_order should exist in items for (const tab_id of tabs.tab_order) { - expect(tabs.items.by_id.has(tab_id)).toBe(true); + assert.ok(tabs.items.by_id.has(tab_id)); } // Close a tab tabs.close_tab(tab2.id); // Check consistency again - expect(tabs.tab_order).not.toContain(tab2.id); - expect(tabs.items.by_id.has(tab2.id)).toBe(false); + assert.notInclude(tabs.tab_order, tab2.id); + assert.ok(!tabs.items.by_id.has(tab2.id)); // Reopen a tab tabs.reopen_last_closed_tab(); // Check consistency once more for (const tab_id of tabs.tab_order) { - expect(tabs.items.by_id.has(tab_id)).toBe(true); + assert.ok(tabs.items.by_id.has(tab_id)); } }); @@ -1072,21 +1075,21 @@ describe('DiskfileTabs', () => { const preview = tabs.preview_diskfile(TEST_DISKFILE_ID_2); // Check derived properties - expect(tabs.selected_tab).toBe(preview); - expect(tabs.preview_tab).toBe(preview); - expect(tabs.selected_diskfile_id).toBe(TEST_DISKFILE_ID_2); + assert.strictEqual(tabs.selected_tab, preview); + assert.strictEqual(tabs.preview_tab, preview); + assert.strictEqual(tabs.selected_diskfile_id, TEST_DISKFILE_ID_2); // Select different tab tabs.select_tab(tab1.id); // Check updated derived properties - expect(tabs.selected_tab).toBe(tab1); - expect(tabs.selected_diskfile_id).toBe(TEST_DISKFILE_ID_1); - expect(tabs.preview_tab).toBe(preview); // Preview unchanged + assert.strictEqual(tabs.selected_tab, tab1); + assert.strictEqual(tabs.selected_diskfile_id, TEST_DISKFILE_ID_1); + assert.strictEqual(tabs.preview_tab, preview); // Preview unchanged // Promote preview tabs.promote_preview_to_permanent(); - expect(tabs.preview_tab).toBeUndefined(); + assert.ok(tabs.preview_tab === undefined); }); }); }); diff --git a/src/test/indexed_collection.svelte.base.test.ts b/src/test/indexed_collection.svelte.base.test.ts index 78c4b4780..7dfecb091 100644 --- a/src/test/indexed_collection.svelte.base.test.ts +++ b/src/test/indexed_collection.svelte.base.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe} from 'vitest'; +import {test, assert, describe} from 'vitest'; import {z} from 'zod'; import {IndexedCollection} from '$lib/indexed_collection.svelte.js'; @@ -61,19 +59,19 @@ describe('IndexedCollection - Base Functionality', () => { collection.add(item2); // Check size and content - expect(collection.size).toBe(2); + assert.strictEqual(collection.size, 2); // Use id-based comparison with by_id.values() - expect(has_item_with_id(collection.by_id.values(), item1)).toBe(true); - expect(has_item_with_id(collection.by_id.values(), item2)).toBe(true); + assert.ok(has_item_with_id(collection.by_id.values(), item1)); + assert.ok(has_item_with_id(collection.by_id.values(), item2)); // Test retrieval by id - expect(collection.get(item1.id)?.id).toBe(item1.id); + assert.strictEqual(collection.get(item1.id)?.id, item1.id); // Test removal - expect(collection.remove(item1.id)).toBe(true); - expect(collection.size).toBe(1); - expect(collection.get(item1.id)).toBeUndefined(); - expect(collection.get(item2.id)?.id).toBe(item2.id); + assert.ok(collection.remove(item1.id)); + assert.strictEqual(collection.size, 1); + assert.isUndefined(collection.get(item1.id)); + assert.strictEqual(collection.get(item2.id)?.id, item2.id); }); test('single index operations', () => { @@ -97,22 +95,22 @@ describe('IndexedCollection - Base Functionality', () => { collection.add(item3); // Test lookup by single index - expect(collection.by_optional('by_text', 'a1')?.id).toBe(item1.id); - expect(collection.by_optional('by_text', 'a2')?.id).toBe(item2.id); - expect(collection.by_optional('by_text', 'a3')?.id).toBe(item3.id); - expect(collection.by_optional('by_text', 'missing')).toBeUndefined(); + assert.strictEqual(collection.by_optional('by_text', 'a1')?.id, item1.id); + assert.strictEqual(collection.by_optional('by_text', 'a2')?.id, item2.id); + assert.strictEqual(collection.by_optional('by_text', 'a3')?.id, item3.id); + assert.isUndefined(collection.by_optional('by_text', 'missing')); // Test the non-optional version that throws - expect(() => collection.by('by_text', 'missing')).toThrow(); - expect(collection.by('by_text', 'a1').id).toBe(item1.id); + assert.throws(() => collection.by('by_text', 'missing')); + assert.strictEqual(collection.by('by_text', 'a1').id, item1.id); // Test query method - expect(collection.query('by_text', 'a1').id).toBe(item1.id); + assert.strictEqual(collection.query('by_text', 'a1').id, item1.id); // Test index update on removal collection.remove(item2.id); - expect(collection.by_optional('by_text', 'a2')).toBeUndefined(); - expect(collection.size).toBe(2); + assert.isUndefined(collection.by_optional('by_text', 'a2')); + assert.strictEqual(collection.size, 2); }); }); @@ -140,24 +138,24 @@ describe('IndexedCollection - Index Types', () => { collection.add(item4); // Test multi-index lookup - expect(collection.where('by_category', 'c1')).toHaveLength(2); + assert.strictEqual(collection.where('by_category', 'c1').length, 2); const c1_items = collection.where('by_category', 'c1'); - expect(c1_items.some((item) => item.id === item1.id)).toBe(true); - expect(c1_items.some((item) => item.id === item2.id)).toBe(true); + assert.ok(c1_items.some((item) => item.id === item1.id)); + assert.ok(c1_items.some((item) => item.id === item2.id)); - expect(collection.where('by_category', 'c2')).toHaveLength(2); + assert.strictEqual(collection.where('by_category', 'c2').length, 2); const c2_items = collection.where('by_category', 'c2'); - expect(c2_items.some((item) => item.id === item3.id)).toBe(true); - expect(c2_items.some((item) => item.id === item4.id)).toBe(true); + assert.ok(c2_items.some((item) => item.id === item3.id)); + assert.ok(c2_items.some((item) => item.id === item4.id)); // Test first/latest with limit - expect(collection.first('by_category', 'c1', 1)).toHaveLength(1); - expect(collection.latest('by_category', 'c2', 1)).toHaveLength(1); + assert.strictEqual(collection.first('by_category', 'c1', 1).length, 1); + assert.strictEqual(collection.latest('by_category', 'c2', 1).length, 1); // Test index update on removal collection.remove(item1.id); - expect(collection.where('by_category', 'c1')).toHaveLength(1); - expect(collection.where('by_category', 'c1')[0]!.id).toBe(item2.id); + assert.strictEqual(collection.where('by_category', 'c1').length, 1); + assert.strictEqual(collection.where('by_category', 'c1')[0]!.id, item2.id); }); test('derived index operations', () => { @@ -194,33 +192,33 @@ describe('IndexedCollection - Index Types', () => { // Check derived index const high_numbers = collection.derived_index('high_numbers'); - expect(high_numbers).toHaveLength(3); + assert.strictEqual(high_numbers.length, 3); // Compare by id instead of reference - expect(high_numbers[0]!.id).toBe(high_item.id); // Highest number first (10) - expect(high_numbers[1]!.id).toBe(medium_item.id); // Second number (8) - expect(high_numbers[2]!.id).toBe(threshold_item.id); // Third number (6) - expect(high_numbers.some((item) => item.id === low_item.id)).toBe(false); // Low number excluded (3) + assert.strictEqual(high_numbers[0]!.id, high_item.id); // Highest number first (10) + assert.strictEqual(high_numbers[1]!.id, medium_item.id); // Second number (8) + assert.strictEqual(high_numbers[2]!.id, threshold_item.id); // Third number (6) + assert.ok(!high_numbers.some((item) => item.id === low_item.id)); // Low number excluded (3) // Test direct access via get_index const high_numbers_via_index = collection.get_index('high_numbers'); - expect(high_numbers_via_index).toEqual(high_numbers); + assert.deepEqual(high_numbers_via_index, high_numbers); // Test incremental update const new_high_item = create_item('a5', 'c1', [], 9); collection.add(new_high_item); const updated_high_numbers = collection.derived_index('high_numbers'); - expect(updated_high_numbers).toHaveLength(4); - expect(updated_high_numbers[0]!.id).toBe(high_item.id); // 10 - expect(updated_high_numbers[1]!.id).toBe(new_high_item.id); // 9 - expect(updated_high_numbers[2]!.id).toBe(medium_item.id); // 8 - expect(updated_high_numbers[3]!.id).toBe(threshold_item.id); // 6 + assert.strictEqual(updated_high_numbers.length, 4); + assert.strictEqual(updated_high_numbers[0]!.id, high_item.id); // 10 + assert.strictEqual(updated_high_numbers[1]!.id, new_high_item.id); // 9 + assert.strictEqual(updated_high_numbers[2]!.id, medium_item.id); // 8 + assert.strictEqual(updated_high_numbers[3]!.id, threshold_item.id); // 6 // Test removal from derived index collection.remove(high_item.id); const numbers_after_removal = collection.derived_index('high_numbers'); - expect(numbers_after_removal).toHaveLength(3); - expect(numbers_after_removal[0]!.id).toBe(new_high_item.id); // Now highest number + assert.strictEqual(numbers_after_removal.length, 3); + assert.strictEqual(numbers_after_removal[0]!.id, new_high_item.id); // Now highest number }); test('function indexes', () => { @@ -261,14 +259,14 @@ describe('IndexedCollection - Index Types', () => { const range_function = collection.get_index<(range: string) => Array>('by_range'); // Test function index queries - expect(range_function('high')).toHaveLength(2); - expect(range_function('medium')).toHaveLength(2); - expect(range_function('low')).toHaveLength(2); + assert.strictEqual(range_function('high').length, 2); + assert.strictEqual(range_function('medium').length, 2); + assert.strictEqual(range_function('low').length, 2); // Test using the query method - expect(collection.query, string>('by_range', 'high')).toHaveLength(2); - expect(collection.query, string>('by_range', 'medium')).toHaveLength(2); - expect(collection.query, string>('by_range', 'low')).toHaveLength(2); + assert.strictEqual(collection.query, string>('by_range', 'high').length, 2); + assert.strictEqual(collection.query, string>('by_range', 'medium').length, 2); + assert.strictEqual(collection.query, string>('by_range', 'low').length, 2); }); }); @@ -318,20 +316,20 @@ describe('IndexedCollection - Advanced Features', () => { collection.add_many([high_number_item, mid_number_item, low_number_item, top_number_item]); // Test single index lookup - expect(collection.by_optional('by_text', 'a1')?.id).toBe(high_number_item.id); + assert.strictEqual(collection.by_optional('by_text', 'a1')?.id, high_number_item.id); // Test multi index lookup - expect(collection.where('by_category', 'c1')).toHaveLength(3); - expect( + assert.strictEqual(collection.where('by_category', 'c1').length, 3); + assert.ok( collection.where('by_listitem', 'l1').some((item) => item.id === high_number_item.id), - ).toBe(true); + ); // Test derived index const high_numbers = collection.derived_index('recent_high_numbers'); - expect(high_numbers).toHaveLength(2); - expect(high_numbers.some((item) => item.id === high_number_item.id)).toBe(true); - expect(high_numbers.some((item) => item.id === top_number_item.id)).toBe(true); - expect(high_numbers.some((item) => item.id === mid_number_item.id)).toBe(false); // score 7 is too low + assert.strictEqual(high_numbers.length, 2); + assert.ok(high_numbers.some((item) => item.id === high_number_item.id)); + assert.ok(high_numbers.some((item) => item.id === top_number_item.id)); + assert.ok(!high_numbers.some((item) => item.id === mid_number_item.id)); // score 7 is too low }); test('complex data structures', () => { @@ -389,17 +387,17 @@ describe('IndexedCollection - Advanced Features', () => { unique_values: Set; }>('stats'); - expect(stats.count).toBe(2); - expect(stats.average).toBe(15); - expect(stats.unique_values.size).toBe(2); - expect(stats.unique_values.has('c1')).toBe(true); + assert.strictEqual(stats.count, 2); + assert.strictEqual(stats.average, 15); + assert.strictEqual(stats.unique_values.size, 2); + assert.ok(stats.unique_values.has('c1')); // Test updating the complex structure collection.add(create_item('a3', 'c1', [], 30)); - expect(stats.count).toBe(3); - expect(stats.average).toBe(20); - expect(stats.unique_values.size).toBe(2); + assert.strictEqual(stats.count, 3); + assert.strictEqual(stats.average, 20); + assert.strictEqual(stats.unique_values.size, 2); }); test('batch operations', () => { @@ -426,26 +424,26 @@ describe('IndexedCollection - Advanced Features', () => { collection.add_many(items); // Verify all items were added - expect(collection.size).toBe(5); - expect(collection.where('by_category', 'c1').length).toBe(3); - expect(collection.where('by_category', 'c2').length).toBe(2); + assert.strictEqual(collection.size, 5); + assert.strictEqual(collection.where('by_category', 'c1').length, 3); + assert.strictEqual(collection.where('by_category', 'c2').length, 2); // Test removing multiple items at once const ids_to_remove = [items[0]!.id, items[2]!.id, items[4]!.id]; const removed_count = collection.remove_many(ids_to_remove); - expect(removed_count).toBe(3); - expect(collection.size).toBe(2); + assert.strictEqual(removed_count, 3); + assert.strictEqual(collection.size, 2); // Verify specific items were removed - expect(collection.has(items[0]!.id)).toBe(false); - expect(collection.has(items[1]!.id)).toBe(true); - expect(collection.has(items[2]!.id)).toBe(false); - expect(collection.has(items[3]!.id)).toBe(true); - expect(collection.has(items[4]!.id)).toBe(false); + assert.ok(!collection.has(items[0]!.id)); + assert.ok(collection.has(items[1]!.id)); + assert.ok(!collection.has(items[2]!.id)); + assert.ok(collection.has(items[3]!.id)); + assert.ok(!collection.has(items[4]!.id)); // Verify indexes were properly updated - expect(collection.where('by_category', 'c1').length).toBe(1); - expect(collection.where('by_category', 'c2').length).toBe(1); + assert.strictEqual(collection.where('by_category', 'c1').length, 1); + assert.strictEqual(collection.where('by_category', 'c2').length, 1); }); }); diff --git a/src/test/indexed_collection.svelte.edge_cases.test.ts b/src/test/indexed_collection.svelte.edge_cases.test.ts index b71921379..47aad28ca 100644 --- a/src/test/indexed_collection.svelte.edge_cases.test.ts +++ b/src/test/indexed_collection.svelte.edge_cases.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, vi} from 'vitest'; +import {test, describe, vi, assert} from 'vitest'; import {z} from 'zod'; import {IndexedCollection} from '$lib/indexed_collection.svelte.js'; @@ -14,8 +12,6 @@ import { } from '$lib/indexed_collection_helpers.svelte.js'; import {create_uuid, Uuid} from '$lib/zod_helpers.js'; -/* eslint-disable @typescript-eslint/no-empty-function */ - // Mock item type that implements IndexedItem interface TestItem { id: Uuid; @@ -70,30 +66,30 @@ describe('IndexedCollection - Edge Cases', () => { // Test retrieving with null values const null_item = collection.by_optional('by_number_a', null); - expect(null_item).toBeDefined(); - expect(null_item!.string_a).toBe('a2'); + assert.isDefined(null_item); + assert.strictEqual(null_item.string_a, 'a2'); // Test filtering with non-existing value - expect(collection.by_optional('by_number_a', 999)).toBeUndefined(); + assert.ok(collection.by_optional('by_number_a', 999) === undefined); // Test multi-index with shared array values const tag1_items = collection.where('by_array_a', 'tag1'); - expect(tag1_items.length).toBe(2); - expect(tag1_items.map((i) => i.string_a).sort()).toEqual(['a1', 'a4']); + assert.strictEqual(tag1_items.length, 2); + assert.deepEqual(tag1_items.map((i) => i.string_a).sort(), ['a1', 'a4']); // Item with empty array should be excluded from by_array_a index - expect(collection.where('by_array_a', undefined)).toHaveLength(0); + assert.strictEqual(collection.where('by_array_a', undefined).length, 0); // Test removing an item with null value collection.remove(item2.id); - expect(collection.by_optional('by_number_a', null)).toBeUndefined(); + assert.ok(collection.by_optional('by_number_a', null) === undefined); // Add another item with null value const item5 = create_test_item('a5', null, ['tag5']); collection.add(item5); const null_item_after = collection.by_optional('by_number_a', null); - expect(null_item_after).toBeDefined(); - expect(null_item_after!.string_a).toBe('a5'); + assert.isDefined(null_item_after); + assert.strictEqual(null_item_after.string_a, 'a5'); }); test('handling duplicates in single indexes', () => { @@ -127,18 +123,18 @@ describe('IndexedCollection - Edge Cases', () => { collection.add(item2); // Second 'a' item - should overwrite item1 in the index // Check that the latest addition wins for duplicate keys - expect(collection.by_optional('by_prefix', 'a')?.string_a).toBe('a456'); - expect(collection.by_optional('by_prefix', 'b')?.string_a).toBe('b789'); + assert.strictEqual(collection.by_optional('by_prefix', 'a')?.string_a, 'a456'); + assert.strictEqual(collection.by_optional('by_prefix', 'b')?.string_a, 'b789'); // Test what happens when removing an item that was overwritten in the index collection.remove(item2.id); // Remove the winning item // The index should now revert to the first item with the same key - expect(collection.by_optional('by_prefix', 'a')?.string_a).toBe('a123'); + assert.strictEqual(collection.by_optional('by_prefix', 'a')?.string_a, 'a123'); // Check that removing all items with the same key clears the index entry collection.remove(item1.id); - expect(collection.by_optional('by_prefix', 'a')).toBeUndefined(); + assert.ok(collection.by_optional('by_prefix', 'a') === undefined); console_warn_spy.mockRestore(); }); @@ -193,13 +189,14 @@ describe('IndexedCollection - Edge Cases', () => { console.log(`Time to add 100 items with 3 indexes: ${end_time - start_time}ms`); // Verify all indexes were created correctly - expect(collection.size).toBe(100); - expect(Object.keys(collection.indexes).length).toBe(3); + assert.strictEqual(collection.size, 100); + assert.strictEqual(Object.keys(collection.indexes).length, 3); // Test various queries against the indexes - expect(collection.by_optional('by_string_a', 'item23')?.number_a).toBe(23); - expect(collection.where('by_array_a', 'tag5').length).toBe(10); // 10% of items have tag5 - expect(collection.derived_index('filtered_items').length).toBe( + assert.strictEqual(collection.by_optional('by_string_a', 'item23')?.number_a, 23); + assert.strictEqual(collection.where('by_array_a', 'tag5').length, 10); // 10% of items have tag5 + assert.strictEqual( + collection.derived_index('filtered_items').length, test_batch.filter((i) => i.boolean_a && i.number_a !== null).length, ); @@ -210,7 +207,7 @@ describe('IndexedCollection - Edge Cases', () => { const remove_end = performance.now(); console.log(`Time to remove 50 items: ${remove_end - remove_start}ms`); - expect(collection.size).toBe(50); + assert.strictEqual(collection.size, 50); }); test('error handling for invalid index type access', () => { @@ -234,13 +231,13 @@ describe('IndexedCollection - Edge Cases', () => { collection.add(create_test_item('a1', 1, ['tag1'])); // Test accessing indexes with wrong methods - expect(() => { + assert.throws(() => { collection.where('by_string_a', 'a1'); // Using multi-index method on single index - }).toThrow(); // Should throw error about index type mismatch + }); // Should throw error about index type mismatch - expect(() => { + assert.throws(() => { collection.by('by_array_a', 'tag1'); // Using single-index method on multi-index - }).toThrow(); // Should throw error about index type mismatch + }); // Should throw error about index type mismatch }); test('handling invalid queries with schema validation', () => { @@ -264,15 +261,15 @@ describe('IndexedCollection - Edge Cases', () => { collection.add(create_test_item('a3', null)); // Null value // Test valid query - expect(collection.by_optional('by_number_a', 5)?.string_a).toBe('a1'); + assert.strictEqual(collection.by_optional('by_number_a', 5)?.string_a, 'a1'); // Test queries that violate schema collection.query('by_number_a', -10); // Negative number, should log validation error - expect(console_error_spy).toHaveBeenCalled(); + assert.ok(console_error_spy.mock.calls.length > 0); console_error_spy.mockClear(); collection.query('by_number_a', null); // Null, should log validation error - expect(console_error_spy).toHaveBeenCalled(); + assert.ok(console_error_spy.mock.calls.length > 0); console_error_spy.mockRestore(); }); @@ -340,7 +337,7 @@ describe('IndexedCollection - Edge Cases', () => { // Add test items and verify custom handlers const item1 = create_test_item('x1'); collection.add(item1); - expect(onadd_fn).toHaveBeenCalled(); + assert.ok(onadd_fn.mock.calls.length > 0); const item2 = create_test_item('y2'); collection.add(item2); @@ -350,19 +347,19 @@ describe('IndexedCollection - Edge Cases', () => { // Search functions should work const x_results = search_fn('x'); - expect(x_results.length).toBe(1); + assert.strictEqual(x_results.length, 1); const x_result_0 = x_results[0]; - expect(x_result_0).toBeDefined(); - expect(x_result_0!.string_a).toBe('x1'); - expect(compute_fn).toHaveBeenLastCalledWith('x'); + assert.isDefined(x_result_0); + assert.strictEqual(x_result_0.string_a, 'x1'); + assert.deepEqual(compute_fn.mock.calls[compute_fn.mock.calls.length - 1], ['x']); // Test removing an item triggers onremove collection.remove(item1.id); - expect(onremove_fn).toHaveBeenCalled(); + assert.ok(onremove_fn.mock.calls.length > 0); // Search function should be updated const no_results = search_fn('x'); - expect(no_results.length).toBe(0); + assert.strictEqual(no_results.length, 0); }); test('custom complex index behaviors', () => { @@ -416,7 +413,7 @@ describe('IndexedCollection - Edge Cases', () => { for (const value of item.array_a) { stats.array_a_frequency[value]--; if (stats.array_a_frequency[value] === 0) { - delete stats.array_a_frequency[value]; // eslint-disable-line @typescript-eslint/no-dynamic-delete + delete stats.array_a_frequency[value]; } } return stats; @@ -443,11 +440,11 @@ describe('IndexedCollection - Edge Cases', () => { array_a_frequency: Record; }>('stats'); - expect(stats.count).toBe(3); - expect(stats.boolean_a_true_count).toBe(2); - expect(stats.boolean_a_false_count).toBe(1); - expect(stats.sum_number_a).toBe(60); - expect(stats.array_a_frequency).toEqual({ + assert.strictEqual(stats.count, 3); + assert.strictEqual(stats.boolean_a_true_count, 2); + assert.strictEqual(stats.boolean_a_false_count, 1); + assert.strictEqual(stats.sum_number_a, 60); + assert.deepEqual(stats.array_a_frequency, { tag1: 2, tag2: 2, tag3: 2, @@ -456,25 +453,25 @@ describe('IndexedCollection - Edge Cases', () => { // Test incremental update - add an item collection.add(create_test_item('a4', 40, ['tag1', 'tag4'], false)); - expect(stats.count).toBe(4); - expect(stats.boolean_a_true_count).toBe(2); - expect(stats.boolean_a_false_count).toBe(2); - expect(stats.sum_number_a).toBe(100); - expect(stats.array_a_frequency.tag1).toBe(3); - expect(stats.array_a_frequency.tag4).toBe(1); + assert.strictEqual(stats.count, 4); + assert.strictEqual(stats.boolean_a_true_count, 2); + assert.strictEqual(stats.boolean_a_false_count, 2); + assert.strictEqual(stats.sum_number_a, 100); + assert.strictEqual(stats.array_a_frequency.tag1, 3); + assert.strictEqual(stats.array_a_frequency.tag4, 1); // Test incremental update - remove an item // Store the item reference first to ensure it exists const item1_ref = collection.by_optional('by_string_a', 'a1'); - expect(item1_ref).toBeDefined(); // Make sure we found it - collection.remove(item1_ref!.id); - - expect(stats.count).toBe(3); - expect(stats.boolean_a_true_count).toBe(1); - expect(stats.boolean_a_false_count).toBe(2); - expect(stats.sum_number_a).toBe(90); - expect(stats.array_a_frequency.tag1).toBe(2); - expect(stats.array_a_frequency.tag2).toBe(1); + assert.isDefined(item1_ref); // Make sure we found it + collection.remove(item1_ref.id); + + assert.strictEqual(stats.count, 3); + assert.strictEqual(stats.boolean_a_true_count, 1); + assert.strictEqual(stats.boolean_a_false_count, 2); + assert.strictEqual(stats.sum_number_a, 90); + assert.strictEqual(stats.array_a_frequency.tag1, 2); + assert.strictEqual(stats.array_a_frequency.tag2, 1); }); test('multi-index array instance consistency', () => { @@ -503,12 +500,12 @@ describe('IndexedCollection - Edge Cases', () => { const false_items_after = collection.where('by_boolean_a', false); // Should be the same array instances - expect(true_items_before).toBe(true_items_after); - expect(false_items_before).toBe(false_items_after); + assert.strictEqual(true_items_before, true_items_after); + assert.strictEqual(false_items_before, false_items_after); // Arrays should have the correct content - expect(true_items_after.length).toBe(1); - expect(false_items_after.length).toBe(1); + assert.strictEqual(true_items_after.length, 1); + assert.strictEqual(false_items_after.length, 1); }); test('multi-index reactivity behavior outside reactive context', () => { @@ -534,19 +531,19 @@ describe('IndexedCollection - Edge Cases', () => { const large_items = $derived(collection.where('by_number_group', 'large')); // Initial state - expect(small_items.length).toBe(0); - expect(medium_items.length).toBe(0); - expect(large_items.length).toBe(0); + assert.strictEqual(small_items.length, 0); + assert.strictEqual(medium_items.length, 0); + assert.strictEqual(large_items.length, 0); // Add items collection.add(create_test_item('a1', 5)); - expect(small_items.length).toBe(1); + assert.strictEqual(small_items.length, 1); collection.add(create_test_item('a2', 25)); - expect(medium_items.length).toBe(1); + assert.strictEqual(medium_items.length, 1); collection.add(create_test_item('a3', 75)); - expect(large_items.length).toBe(1); + assert.strictEqual(large_items.length, 1); // Add multiple items collection.add_many([ @@ -554,9 +551,9 @@ describe('IndexedCollection - Edge Cases', () => { create_test_item('a5', 35), create_test_item('a6', 100), ]); - expect(small_items.length).toBe(2); - expect(medium_items.length).toBe(2); - expect(large_items.length).toBe(2); + assert.strictEqual(small_items.length, 2); + assert.strictEqual(medium_items.length, 2); + assert.strictEqual(large_items.length, 2); }); test('multi-index maintains same array with complex extractors', () => { @@ -592,18 +589,18 @@ describe('IndexedCollection - Edge Cases', () => { collection.add_many([item1, item2, item3]); // Verify state - expect(tag1_items.length).toBe(2); // item1, item3 - expect(tag2_items.length).toBe(2); // item1, item2 - expect(conditional_tag1_items.length).toBe(2); // item1, item3 (both have boolean_a true) + assert.strictEqual(tag1_items.length, 2); // item1, item3 + assert.strictEqual(tag2_items.length, 2); // item1, item2 + assert.strictEqual(conditional_tag1_items.length, 2); // item1, item3 (both have boolean_a true) // Get references again - should be same instances const tag1_items_2 = $derived(collection.where('by_tags', 'tag1')); const tag2_items_2 = $derived(collection.where('by_tags', 'tag2')); const conditional_tag1_items_2 = $derived(collection.where('by_conditional_tags', 'tag1')); - expect(tag1_items).toBe(tag1_items_2); - expect(tag2_items).toBe(tag2_items_2); - expect(conditional_tag1_items).toBe(conditional_tag1_items_2); + assert.strictEqual(tag1_items, tag1_items_2); + assert.strictEqual(tag2_items, tag2_items_2); + assert.strictEqual(conditional_tag1_items, conditional_tag1_items_2); }); test('multi-index sort functionality', () => { @@ -629,24 +626,36 @@ describe('IndexedCollection - Edge Cases', () => { const true_items = collection.where('by_boolean_sorted', true); // Verify initial sort order - expect(true_items.map((i) => i.string_a)).toEqual(['a2', 'a3', 'a1']); + assert.deepEqual( + true_items.map((i) => i.string_a), + ['a2', 'a3', 'a1'], + ); // Add new item that should be inserted in middle const item4 = create_test_item('a4', 25, [], true); collection.add(item4); // Verify array maintains sort order - expect(true_items.map((i) => i.string_a)).toEqual(['a2', 'a3', 'a4', 'a1']); + assert.deepEqual( + true_items.map((i) => i.string_a), + ['a2', 'a3', 'a4', 'a1'], + ); // Add item at beginning const item5 = create_test_item('a5', 5, [], true); collection.add(item5); - expect(true_items.map((i) => i.string_a)).toEqual(['a5', 'a2', 'a3', 'a4', 'a1']); + assert.deepEqual( + true_items.map((i) => i.string_a), + ['a5', 'a2', 'a3', 'a4', 'a1'], + ); // Remove middle item collection.remove(item3.id); - expect(true_items.map((i) => i.string_a)).toEqual(['a5', 'a2', 'a4', 'a1']); + assert.deepEqual( + true_items.map((i) => i.string_a), + ['a5', 'a2', 'a4', 'a1'], + ); }); test('multi-index empty bucket behavior', () => { @@ -663,16 +672,16 @@ describe('IndexedCollection - Edge Cases', () => { // Get initial reference const category_x = $derived(collection.where('by_category', 'x')); - expect(category_x.length).toBe(0); + assert.strictEqual(category_x.length, 0); // Add item to create bucket const item1 = create_test_item('x_1', 1); collection.add(item1); - expect(category_x.length).toBe(1); + assert.strictEqual(category_x.length, 1); // Get new reference - should be same instance const category_x_after = $derived(collection.where('by_category', 'x')); - expect(category_x).toBe(category_x_after); + assert.strictEqual(category_x, category_x_after); // Add more items collection.add_many([ @@ -681,14 +690,14 @@ describe('IndexedCollection - Edge Cases', () => { create_test_item('y_1', 4), ]); - expect(category_x.length).toBe(3); + assert.strictEqual(category_x.length, 3); // Remove all x items const x_ids = category_x.map((item) => item.id); collection.remove_many(x_ids); // After removal, the same array is empty - expect(category_x_after.length).toBe(0); + assert.strictEqual(category_x_after.length, 0); }); test('multi-index behavior with bucket deletion and recreation', () => { @@ -705,21 +714,21 @@ describe('IndexedCollection - Edge Cases', () => { // Get reference before adding any items const a_items_1 = $derived(collection.where('by_prefix', 'a')); - expect(a_items_1.length).toBe(0); + assert.strictEqual(a_items_1.length, 0); // Add and remove items const item1 = create_test_item('a1', 1); collection.add(item1); - expect(a_items_1.length).toBe(1); + assert.strictEqual(a_items_1.length, 1); collection.remove(item1.id); // The array is now empty - expect(a_items_1.length).toBe(0); + assert.strictEqual(a_items_1.length, 0); // Add items again - updates the same array const item2 = create_test_item('a2', 2); collection.add(item2); - expect(a_items_1.length).toBe(1); // both references see the update + assert.strictEqual(a_items_1.length, 1); // both references see the update }); test('multi-index with undefined extractor results', () => { @@ -747,12 +756,12 @@ describe('IndexedCollection - Edge Cases', () => { ]); const even_items = collection.where('by_even_numbers', 'even'); - expect(even_items.length).toBe(2); - expect(even_items.map((i) => i.string_a).sort()).toEqual(['a1', 'a3']); + assert.strictEqual(even_items.length, 2); + assert.deepEqual(even_items.map((i) => i.string_a).sort(), ['a1', 'a3']); // Undefined key should return empty array const undefined_items = collection.where('by_even_numbers', undefined); - expect(undefined_items.length).toBe(0); + assert.strictEqual(undefined_items.length, 0); }); test('multi-index preserves array reference consistency', () => { @@ -770,8 +779,8 @@ describe('IndexedCollection - Edge Cases', () => { // Step 1: Get initial empty array references const cat_a = $derived(collection.where('by_category', 'a')); const cat_b = $derived(collection.where('by_category', 'b')); - expect(cat_a.length).toBe(0); - expect(cat_b.length).toBe(0); + assert.strictEqual(cat_a.length, 0); + assert.strictEqual(cat_b.length, 0); // Step 2: Add items collection.add_many([ @@ -781,25 +790,25 @@ describe('IndexedCollection - Edge Cases', () => { ]); // Step 3: Verify same array instances are updated - expect(cat_a.length).toBe(2); - expect(cat_b.length).toBe(1); - expect(collection.where('by_category', 'a')).toBe(cat_a); - expect(collection.where('by_category', 'b')).toBe(cat_b); + assert.strictEqual(cat_a.length, 2); + assert.strictEqual(cat_b.length, 1); + assert.strictEqual(collection.where('by_category', 'a'), cat_a); + assert.strictEqual(collection.where('by_category', 'b'), cat_b); // Step 4: Remove some items const a_1 = cat_a.find((i) => i.string_a === 'a_1'); collection.remove(a_1!.id); - expect(cat_a.length).toBe(1); + assert.strictEqual(cat_a.length, 1); // Step 5: Remove all items from a category const a_2 = cat_a.find((i) => i.string_a === 'a_2'); collection.remove(a_2!.id); // Array is now empty but still exists - expect(cat_a.length).toBe(0); + assert.strictEqual(cat_a.length, 0); // Step 6: Verify we still get the same instance const cat_a_new = collection.where('by_category', 'a'); - expect(cat_a_new.length).toBe(0); + assert.strictEqual(cat_a_new.length, 0); }); test('reactive context tracking with $derived', () => { @@ -819,8 +828,8 @@ describe('IndexedCollection - Edge Cases', () => { const false_count = $derived(collection.where('by_boolean_a', false).length); // Initial state - expect(true_count).toBe(0); - expect(false_count).toBe(0); + assert.strictEqual(true_count, 0); + assert.strictEqual(false_count, 0); // Add items collection.add_many([ @@ -830,15 +839,15 @@ describe('IndexedCollection - Edge Cases', () => { ]); // Derived values should update - expect(true_count).toBe(2); - expect(false_count).toBe(1); + assert.strictEqual(true_count, 2); + assert.strictEqual(false_count, 1); // Remove an item const items = collection.where('by_boolean_a', true); collection.remove(items[0]!.id); - expect(true_count).toBe(1); - expect(false_count).toBe(1); + assert.strictEqual(true_count, 1); + assert.strictEqual(false_count, 1); }); test('multi-index with $state creates reactive arrays', () => { @@ -857,7 +866,7 @@ describe('IndexedCollection - Edge Cases', () => { const tag1_derived_length = $derived(collection.where('by_tags', 'tag1').length); // Initial state - expect(tag1_derived_length).toBe(0); + assert.strictEqual(tag1_derived_length, 0); // Add items collection.add_many([ @@ -866,13 +875,13 @@ describe('IndexedCollection - Edge Cases', () => { ]); // Derived value should update - expect(tag1_derived_length).toBe(2); + assert.strictEqual(tag1_derived_length, 2); // Remove an item const tag1_items = collection.where('by_tags', 'tag1'); collection.remove(tag1_items[0]!.id); // Derived value should update again - expect(tag1_derived_length).toBe(1); + assert.strictEqual(tag1_derived_length, 1); }); }); diff --git a/src/test/indexed_collection.svelte.optimization.test.ts b/src/test/indexed_collection.svelte.optimization.test.ts index a8cfc601c..c9bf1c625 100644 --- a/src/test/indexed_collection.svelte.optimization.test.ts +++ b/src/test/indexed_collection.svelte.optimization.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, vi, describe} from 'vitest'; +import {test, assert, describe, vi} from 'vitest'; import {z} from 'zod'; import {IndexedCollection} from '$lib/indexed_collection.svelte.js'; @@ -58,8 +56,8 @@ describe('IndexedCollection - Optimization Tests', () => { }); // Verify compute was called exactly once during initialization - expect(compute_spy).toHaveBeenCalledTimes(1); - expect(collection.size).toBe(2); + assert.strictEqual(compute_spy.mock.calls.length, 1); + assert.strictEqual(collection.size, 2); }); test('incremental updates avoid recomputing entire index', () => { @@ -97,23 +95,23 @@ describe('IndexedCollection - Optimization Tests', () => { }); // Verify compute was called exactly once during initialization - expect(compute_spy).toHaveBeenCalledTimes(1); + assert.strictEqual(compute_spy.mock.calls.length, 1); // Add more items and check that compute isn't called again collection.add(create_item('string_a3', 'string_b3', [], 20)); collection.add(create_item('string_a4', 'string_b4', [], 8)); // Compute should still have been called only once - expect(compute_spy).toHaveBeenCalledTimes(1); + assert.strictEqual(compute_spy.mock.calls.length, 1); // onadd should have been called twice - once for each new item - expect(onadd_spy).toHaveBeenCalledTimes(2); + assert.strictEqual(onadd_spy.mock.calls.length, 2); // Check that the index was correctly updated const high_number = collection.derived_index('high_number'); - expect(high_number.length).toBe(2); - expect(high_number.some((item) => item.string_a === 'string_a1')).toBe(true); - expect(high_number.some((item) => item.string_a === 'string_a3')).toBe(true); + assert.strictEqual(high_number.length, 2); + assert.ok(high_number.some((item) => item.string_a === 'string_a1')); + assert.ok(high_number.some((item) => item.string_a === 'string_a3')); }); test('batch operations are more efficient', () => { @@ -159,7 +157,7 @@ describe('IndexedCollection - Optimization Tests', () => { const batch_time = end_time - start_time; // Verify onadd was called for each item - expect(onadd_spy).toHaveBeenCalledTimes(100); + assert.strictEqual(onadd_spy.mock.calls.length, 100); // Reset the spy for individual adds onadd_spy.mockClear(); @@ -179,7 +177,7 @@ describe('IndexedCollection - Optimization Tests', () => { const individual_time = individual_end - individual_start; // Verify onadd was called for each item - expect(onadd_spy).toHaveBeenCalledTimes(100); + assert.strictEqual(onadd_spy.mock.calls.length, 100); // This test is somewhat approximative but helps validate the efficiency // We're not making a strict assertion on performance as it can vary between environments @@ -220,10 +218,10 @@ describe('IndexedCollection - Optimization Tests', () => { const number_fn = collection.get_index<(threshold: string) => Array>('by_min_number'); // These should return different filtered subsets without storing separate copies - expect(number_fn('10').length).not.toBe(number_fn('50').length); - expect(number_fn('0').length).toBe(20); // All items - expect(number_fn('50').length).toBe(10); // Half the items - expect(number_fn('90').length).toBe(2); // Just the highest values + assert.ok(number_fn('10').length !== number_fn('50').length); + assert.strictEqual(number_fn('0').length, 20); // All items + assert.strictEqual(number_fn('50').length, 10); // Half the items + assert.strictEqual(number_fn('90').length, 2); // Just the highest values }); test('memory usage with large datasets', () => { @@ -252,11 +250,11 @@ describe('IndexedCollection - Optimization Tests', () => { // Verify the index contains the expected number of categories const b_index = collection.get_index>>('by_string_b'); // console.log(`b_index`, $state.snapshot(b_index)); - expect(b_index.size).toBe(10); // 10 unique categories + assert.strictEqual(b_index.size, 10); // 10 unique categories // Verify each category has the right number of items for (let i = 0; i < 10; i++) { - expect(collection.where('by_string_b', `string_b${i}`).length).toBe(100); // 1000 items / 10 categories = 100 per category + assert.strictEqual(collection.where('by_string_b', `string_b${i}`).length, 100); // 1000 items / 10 categories = 100 per category } }); }); diff --git a/src/test/indexed_collection.svelte.queries.test.ts b/src/test/indexed_collection.svelte.queries.test.ts index 6eda98b1a..1b18d6db5 100644 --- a/src/test/indexed_collection.svelte.queries.test.ts +++ b/src/test/indexed_collection.svelte.queries.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, beforeEach} from 'vitest'; +import {test, describe, beforeEach, assert} from 'vitest'; import {z} from 'zod'; import {IndexedCollection} from '$lib/indexed_collection.svelte.js'; @@ -218,93 +216,108 @@ describe('IndexedCollection - Query Capabilities', () => { test('basic query operations', () => { // Single index direct lookup - expect(collection.by_optional('by_string_a', 'a1'.toLowerCase())).toBe(items[0]); - expect(collection.by_optional('by_string_b', 'b1')).toBeDefined(); + assert.strictEqual(collection.by_optional('by_string_a', 'a1'.toLowerCase()), items[0]); + assert.ok(collection.by_optional('by_string_b', 'b1') !== undefined); // Multi index direct lookup - expect(collection.where('by_string_c', 'c1')).toHaveLength(2); - expect(collection.where('by_number_a', 5)).toHaveLength(2); - expect(collection.where('by_boolean_a', 'y')).toHaveLength(3); + assert.strictEqual(collection.where('by_string_c', 'c1').length, 2); + assert.strictEqual(collection.where('by_number_a', 5).length, 2); + assert.strictEqual(collection.where('by_boolean_a', 'y').length, 3); // Non-existent values - expect(collection.by_optional('by_string_a', 'nonexistent')).toBeUndefined(); - expect(collection.where('by_string_c', 'nonexistent')).toHaveLength(0); + assert.ok(collection.by_optional('by_string_a', 'nonexistent') === undefined); + assert.strictEqual(collection.where('by_string_c', 'nonexistent').length, 0); }); test('case sensitivity in queries', () => { // Case insensitive string_a lookup (extractor converts to lowercase) - expect(collection.by_optional('by_string_a', 'a1'.toLowerCase())).toBe(items[0]); - expect(collection.by_optional('by_string_a', 'A1'.toLowerCase())).toBe(items[0]); + assert.strictEqual(collection.by_optional('by_string_a', 'a1'.toLowerCase()), items[0]); + assert.strictEqual(collection.by_optional('by_string_a', 'A1'.toLowerCase()), items[0]); // Case sensitive string_b lookup (no conversion in extractor) - expect(collection.by_optional('by_string_b', 'B1')).toBeUndefined(); - expect(collection.by_optional('by_string_b', 'b1')).toBeDefined(); + assert.ok(collection.by_optional('by_string_b', 'B1') === undefined); + assert.ok(collection.by_optional('by_string_b', 'b1') !== undefined); }); test('compound queries combining indexes', () => { // Find c1 items with string_b=b1 const c1_items = collection.where('by_string_c', 'c1'); const b1_c1_items = c1_items.filter((item) => item.string_b === 'b1'); - expect(b1_c1_items).toHaveLength(1); - expect(b1_c1_items[0]!.string_a).toBe('a1'); + assert.strictEqual(b1_c1_items.length, 1); + assert.strictEqual(b1_c1_items[0]!.string_a, 'a1'); // Find boolean_a=true items with number_a=5 const boolean_a_true_items = collection.where('by_boolean_a', 'y'); const high_value_boolean_a_true = boolean_a_true_items.filter((item) => item.number_a === 5); - expect(high_value_boolean_a_true).toHaveLength(2); - expect(high_value_boolean_a_true.map((i) => i.string_a)).toContain('a2'); - expect(high_value_boolean_a_true.map((i) => i.string_a)).toContain('b2'); + assert.strictEqual(high_value_boolean_a_true.length, 2); + assert.include( + high_value_boolean_a_true.map((i) => i.string_a), + 'a2', + ); + assert.include( + high_value_boolean_a_true.map((i) => i.string_a), + 'b2', + ); }); test('queries with array values', () => { // Query by array_a (checks if any tag matches) const tag1_items = collection.where('by_array_a', 'tag1'); - expect(tag1_items).toHaveLength(3); - expect(tag1_items.map((i) => i.string_a)).toContain('a1'); - expect(tag1_items.map((i) => i.string_a)).toContain('a2'); - expect(tag1_items.map((i) => i.string_a)).toContain('b2'); + assert.strictEqual(tag1_items.length, 3); + assert.include( + tag1_items.map((i) => i.string_a), + 'a1', + ); + assert.include( + tag1_items.map((i) => i.string_a), + 'a2', + ); + assert.include( + tag1_items.map((i) => i.string_a), + 'b2', + ); // Multiple tags intersection (using multiple queries) const tag2_items = collection.where('by_array_a', 'tag2'); const tag2_and_tag3_items = tag2_items.filter((item) => item.array_a.includes('tag3')); - expect(tag2_and_tag3_items).toHaveLength(1); - expect(tag2_and_tag3_items[0]!.string_a).toBe('a1'); + assert.strictEqual(tag2_and_tag3_items.length, 1); + assert.strictEqual(tag2_and_tag3_items[0]!.string_a, 'a1'); }); test('derived index queries', () => { // Test the recent_boolean_a_true derived index const recent_boolean_a_true = collection.derived_index('recent_boolean_a_true'); - expect(recent_boolean_a_true).toHaveLength(3); // All boolean_a=true items + assert.strictEqual(recent_boolean_a_true.length, 3); // All boolean_a=true items // Verify order (most recent first) const rbt0 = recent_boolean_a_true[0]; const rbt1 = recent_boolean_a_true[1]; const rbt2 = recent_boolean_a_true[2]; - expect(rbt0).toBeDefined(); - expect(rbt1).toBeDefined(); - expect(rbt2).toBeDefined(); - expect(rbt0!.string_a).toBe('b2'); // 3 days ago - expect(rbt1!.string_a).toBe('a1'); // 10 days ago - expect(rbt2!.string_a).toBe('a2'); // 20 days ago + assert.isDefined(rbt0); + assert.isDefined(rbt1); + assert.isDefined(rbt2); + assert.strictEqual(rbt0.string_a, 'b2'); // 3 days ago + assert.strictEqual(rbt1.string_a, 'a1'); // 10 days ago + assert.strictEqual(rbt2.string_a, 'a2'); // 20 days ago // Test the high_number_a derived index which should include all items with number_a >= 4 const high_number_a = collection.derived_index('high_number_a'); - expect(high_number_a).toHaveLength(4); - expect(high_number_a.map((i) => i.string_a).sort()).toEqual(['a1', 'a2', 'b1', 'b2'].sort()); + assert.strictEqual(high_number_a.length, 4); + assert.deepEqual(high_number_a.map((i) => i.string_a).sort(), ['a1', 'a2', 'b1', 'b2'].sort()); }); test('first/latest with multi-index', () => { // Get first c1 item const first_c1 = collection.first('by_string_c', 'c1', 1); - expect(first_c1).toHaveLength(1); + assert.strictEqual(first_c1.length, 1); const first_c1_item = first_c1[0]; - expect(first_c1_item).toBeDefined(); + assert.isDefined(first_c1_item); // Get latest c2 item const latest_c2 = collection.latest('by_string_c', 'c2', 1); - expect(latest_c2).toHaveLength(1); + assert.strictEqual(latest_c2.length, 1); const latest_c2_item = latest_c2[0]; - expect(latest_c2_item).toBeDefined(); + assert.isDefined(latest_c2_item); }); test('time-based queries', () => { @@ -315,15 +328,21 @@ describe('IndexedCollection - Query Capabilities', () => { const items_this_year_count = collection.values.filter( (item) => item.date_a.getFullYear() === current_year, ).length; - expect(this_year_items.length).toBe(items_this_year_count); + assert.strictEqual(this_year_items.length, items_this_year_count); // More complex date range query - last 7 days const now = Date.now(); const recent_items = collection.values.filter( (item) => item.date_a.getTime() > now - 1000 * 60 * 60 * 24 * 7, ); - expect(recent_items.map((i) => i.string_a)).toContain('b1'); // 5 days ago - expect(recent_items.map((i) => i.string_a)).toContain('b2'); // 3 days ago + assert.include( + recent_items.map((i) => i.string_a), + 'b1', + ); // 5 days ago + assert.include( + recent_items.map((i) => i.string_a), + 'b2', + ); // 3 days ago }); test('adding items affects derived queries correctly', () => { @@ -342,46 +361,46 @@ describe('IndexedCollection - Query Capabilities', () => { // Check that it appears at the top of the recent_boolean_a_true list const recent_boolean_a_true = collection.derived_index('recent_boolean_a_true'); - expect(recent_boolean_a_true[0]!.id).toBe(new_item.id); + assert.strictEqual(recent_boolean_a_true[0]!.id, new_item.id); // Check that it appears in high_number_a const high_number_a = collection.derived_index('high_number_a'); - expect(has_item_with_id(high_number_a, new_item)).toBe(true); + assert.ok(has_item_with_id(high_number_a, new_item)); }); test('removing items updates derived queries', () => { // Remove the most recent boolean_a=true item const item_to_remove = items[4]; // b2 (most recent boolean_a=true) - expect(item_to_remove).toBeDefined(); + assert.isDefined(item_to_remove); - collection.remove(item_to_remove!.id); + collection.remove(item_to_remove.id); // Check that recent_boolean_a_true updates correctly const recent_boolean_a_true = collection.derived_index('recent_boolean_a_true'); - expect(recent_boolean_a_true).toHaveLength(2); + assert.strictEqual(recent_boolean_a_true.length, 2); const rbt0 = recent_boolean_a_true[0]; const rbt1 = recent_boolean_a_true[1]; - expect(rbt0).toBeDefined(); - expect(rbt1).toBeDefined(); - expect(rbt0!.string_a).toBe('a1'); - expect(rbt1!.string_a).toBe('a2'); + assert.isDefined(rbt0); + assert.isDefined(rbt1); + assert.strictEqual(rbt0.string_a, 'a1'); + assert.strictEqual(rbt1.string_a, 'a2'); // Check that high_number_a updates correctly const high_number_a = collection.derived_index('high_number_a'); - expect(high_number_a).not.toContain(item_to_remove); - expect(high_number_a).toHaveLength(3); // Started with 4, removed 1 + assert.notInclude(high_number_a, item_to_remove); + assert.strictEqual(high_number_a.length, 3); // Started with 4, removed 1 }); test('dynamic ordering of query results', () => { // Get all items and sort by number_a (highest first) const sorted_by_number_a = collection.values.slice().sort((a, b) => b.number_a - a.number_a); - expect(sorted_by_number_a[0]!.number_a).toBe(5); + assert.strictEqual(sorted_by_number_a[0]!.number_a, 5); // Sort by creation time (newest first) const sorted_by_time = collection.values .slice() .sort((a, b) => b.date_a.getTime() - a.date_a.getTime()); - expect(sorted_by_time[0]!.string_a).toBe('b2'); // 3 days ago + assert.strictEqual(sorted_by_time[0]!.string_a, 'b2'); // 3 days ago }); }); @@ -436,33 +455,33 @@ describe('IndexedCollection - Search Patterns', () => { test('word-based search', () => { // Find items with "alpha" in string_a const alpha_items = collection.where('by_word', 'alpha'); - expect(alpha_items).toHaveLength(2); + assert.strictEqual(alpha_items.length, 2); // Find items with "beta" in string_a const beta_items = collection.where('by_word', 'beta'); - expect(beta_items).toHaveLength(2); + assert.strictEqual(beta_items.length, 2); // Find items with both "alpha" and "beta" (intersection) const alpha_beta_items = alpha_items.filter((item) => item.string_a.toLowerCase().includes('beta'), ); - expect(alpha_beta_items).toHaveLength(1); - expect(alpha_beta_items[0]!.string_a).toBe('alpha beta gamma'); + assert.strictEqual(alpha_beta_items.length, 1); + assert.strictEqual(alpha_beta_items[0]!.string_a, 'alpha beta gamma'); }); test('range-based categorization', () => { // Find high-number_a items const high_number_a = collection.where('by_number_a_range', 'high'); - expect(high_number_a).toHaveLength(1); - expect(high_number_a[0]!.number_a).toBe(5); + assert.strictEqual(high_number_a.length, 1); + assert.strictEqual(high_number_a[0]!.number_a, 5); // Find mid-number_a items const mid_number_a = collection.where('by_number_a_range', 'mid'); - expect(mid_number_a).toHaveLength(2); + assert.strictEqual(mid_number_a.length, 2); // Find low-number_a items const low_number_a = collection.where('by_number_a_range', 'low'); - expect(low_number_a).toHaveLength(1); - expect(low_number_a[0]!.number_a).toBe(2); + assert.strictEqual(low_number_a.length, 1); + assert.strictEqual(low_number_a[0]!.number_a, 2); }); }); diff --git a/src/test/indexed_collection.svelte.schema_validation.test.ts b/src/test/indexed_collection.svelte.schema_validation.test.ts index 0c1a4aba5..30b05f97d 100644 --- a/src/test/indexed_collection.svelte.schema_validation.test.ts +++ b/src/test/indexed_collection.svelte.schema_validation.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, vi} from 'vitest'; +import {test, assert, describe, vi} from 'vitest'; import {z} from 'zod'; import {IndexedCollection} from '$lib/indexed_collection.svelte.js'; @@ -14,8 +12,6 @@ import { } from '$lib/indexed_collection_helpers.svelte.js'; import {create_uuid, Uuid} from '$lib/zod_helpers.js'; -/* eslint-disable @typescript-eslint/no-empty-function */ - // Mock item type that implements IndexedItem interface TestItem { id: Uuid; @@ -84,11 +80,11 @@ describe('IndexedCollection - Schema Validation', () => { // Test query with valid email const query_result = collection.query('by_string_b', 'a1@zzz.software'); - expect(query_result.string_a).toBe('a1'); + assert.strictEqual(query_result.string_a, 'a1'); // Get single index and check schema validation passed const email_index = collection.single_index('by_string_b'); - expect(email_index.size).toBe(2); + assert.strictEqual(email_index.size, 2); }); test('multi index properly validates input and output', () => { @@ -123,19 +119,19 @@ describe('IndexedCollection - Schema Validation', () => { // Test range query validation const mid_items = collection.query, string>('by_number_range', 'mid'); - expect(mid_items.length).toBe(1); - expect(mid_items[0]!.string_a).toBe('a2'); + assert.strictEqual(mid_items.length, 1); + assert.strictEqual(mid_items[0]!.string_a, 'a2'); // Test array index const item2_matches = collection.query, string>('by_array', 'item2'); - expect(item2_matches.length).toBe(2); - expect(item2_matches.some((item) => item.string_a === 'a1')).toBe(true); - expect(item2_matches.some((item) => item.string_a === 'a3')).toBe(true); + assert.strictEqual(item2_matches.length, 2); + assert.ok(item2_matches.some((item) => item.string_a === 'a1')); + assert.ok(item2_matches.some((item) => item.string_a === 'a3')); const item3_matches = collection.query, string>('by_array', 'item3'); - expect(item3_matches.length).toBe(2); - expect(item3_matches.some((item) => item.string_a === 'a2')).toBe(true); - expect(item3_matches.some((item) => item.string_a === 'a4')).toBe(true); + assert.strictEqual(item3_matches.length, 2); + assert.ok(item3_matches.some((item) => item.string_a === 'a2')); + assert.ok(item3_matches.some((item) => item.string_a === 'a4')); // Restore console.error console_error_spy.mockRestore(); @@ -171,12 +167,12 @@ describe('IndexedCollection - Schema Validation', () => { // Check derived index correctness const flagged_adults = collection.derived_index('flagged_adults'); - expect(flagged_adults.length).toBe(1); - expect(flagged_adults[0]!.string_a).toBe('a1'); + assert.strictEqual(flagged_adults.length, 1); + assert.strictEqual(flagged_adults[0]!.string_a, 'a1'); // Add another qualifying item and verify index updates collection.add(create_item('a5', 'b5@test.com', 40, true)); - expect(collection.derived_index('flagged_adults').length).toBe(2); + assert.strictEqual(collection.derived_index('flagged_adults').length, 2); }); test('dynamic index validates complex query parameters', () => { @@ -235,13 +231,13 @@ describe('IndexedCollection - Schema Validation', () => { // Test number range query const young_range = search_fn({min_number: 18, max_number: 30}); - expect(young_range.length).toBe(2); - expect(young_range.map((item) => item.string_a).sort()).toEqual(['a1', 'a3']); + assert.strictEqual(young_range.length, 2); + assert.deepEqual(young_range.map((item) => item.string_a).sort(), ['a1', 'a3']); // Test flag with specific array values const flagged_with_item1 = search_fn({only_flagged: true, array_values: ['item1']}); - expect(flagged_with_item1.length).toBe(2); - expect(flagged_with_item1.map((item) => item.string_a).sort()).toEqual(['a1', 'a4']); + assert.strictEqual(flagged_with_item1.length, 2); + assert.deepEqual(flagged_with_item1.map((item) => item.string_a).sort(), ['a1', 'a4']); // Test items over 30 that are flagged with specific array values const high_number_with_item3 = search_fn({ @@ -249,15 +245,15 @@ describe('IndexedCollection - Schema Validation', () => { only_flagged: true, array_values: ['item3'], }); - expect(high_number_with_item3.length).toBe(2); - expect(high_number_with_item3.map((item) => item.string_a).sort()).toEqual(['a2', 'a4']); + assert.strictEqual(high_number_with_item3.length, 2); + assert.deepEqual(high_number_with_item3.map((item) => item.string_a).sort(), ['a2', 'a4']); // Test using query method const with_item5 = collection.query, ItemQuery>('item_search', { array_values: ['item5'], }); - expect(with_item5.length).toBe(1); - expect(with_item5[0]!.string_a).toBe('a4'); + assert.strictEqual(with_item5.length, 1); + assert.strictEqual(with_item5[0]!.string_a, 'a4'); }); test('schema validation errors are properly handled', () => { @@ -286,16 +282,20 @@ describe('IndexedCollection - Schema Validation', () => { // Try querying with invalid email format collection.query('by_string_b', 'not-an-email'); - expect(console_error_spy).toHaveBeenCalledWith( - expect.stringContaining('Query validation failed for index by_string_b'), - expect.anything(), + assert.ok( + console_error_spy.mock.calls.some( + ([msg]) => + typeof msg === 'string' && msg.includes('Query validation failed for index by_string_b'), + ), ); // Try querying with out-of-range number collection.query('by_number', 5); - expect(console_error_spy).toHaveBeenCalledWith( - expect.stringContaining('Query validation failed for index by_number'), - expect.anything(), + assert.ok( + console_error_spy.mock.calls.some( + ([msg]) => + typeof msg === 'string' && msg.includes('Query validation failed for index by_number'), + ), ); console_error_spy.mockRestore(); @@ -330,7 +330,7 @@ describe('IndexedCollection - Schema Validation', () => { collection.query('by_number', 5); // Verify no validation errors were logged - expect(console_error_spy).not.toHaveBeenCalled(); + assert.strictEqual(console_error_spy.mock.calls.length, 0); console_error_spy.mockRestore(); }); @@ -367,11 +367,11 @@ describe('IndexedCollection - Schema Validation', () => { collection.add(item2); // Test lookup by nested property - use by_optional instead of where for single index - expect(collection.by_optional('by_nested_option', 'x')?.string_a).toBe('a1'); - expect(collection.by_optional('by_nested_option', 'y')?.string_a).toBe('a2'); + assert.strictEqual(collection.by_optional('by_nested_option', 'x')?.string_a, 'a1'); + assert.strictEqual(collection.by_optional('by_nested_option', 'y')?.string_a, 'a2'); // Test compound key lookup - expect(collection.where('by_compound', 'a1-x').length).toBe(1); - expect(collection.where('by_compound', 'a2-y').length).toBe(1); + assert.strictEqual(collection.where('by_compound', 'a1-x').length, 1); + assert.strictEqual(collection.where('by_compound', 'a2-y').length, 1); }); }); diff --git a/src/test/list_helpers.test.ts b/src/test/list_helpers.test.ts index abddac52b..07dc25007 100644 --- a/src/test/list_helpers.test.ts +++ b/src/test/list_helpers.test.ts @@ -1,6 +1,4 @@ -// @slop Claude Opus 4 - -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {reorder_list, to_reordered_list} from '$lib/list_helpers.js'; // Test constants @@ -13,99 +11,99 @@ describe('reorder_list', () => { test('moves an item forward in the array', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 1, 3); - expect(array).toEqual(['a', 'c', 'd', 'b', 'e']); + assert.deepEqual(array, ['a', 'c', 'd', 'b', 'e']); }); test('moves an item backward in the array', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 3, 1); - expect(array).toEqual(['a', 'd', 'b', 'c', 'e']); + assert.deepEqual(array, ['a', 'd', 'b', 'c', 'e']); }); test('does nothing when from_index equals to_index', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 2, 2); - expect(array).toEqual(SAMPLE_ARRAY); + assert.deepEqual(array, SAMPLE_ARRAY); }); // Edge cases test('moves first item to the end', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 0, 5); - expect(array).toEqual(['b', 'c', 'd', 'e', 'a']); + assert.deepEqual(array, ['b', 'c', 'd', 'e', 'a']); }); test('moves first item one position forward', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 0, 1); - expect(array).toEqual(['b', 'a', 'c', 'd', 'e']); + assert.deepEqual(array, ['b', 'a', 'c', 'd', 'e']); }); test('moves last item to the beginning', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 4, 0); - expect(array).toEqual(['e', 'a', 'b', 'c', 'd']); + assert.deepEqual(array, ['e', 'a', 'b', 'c', 'd']); }); test('moves last item one position backward', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 4, 3); - expect(array).toEqual(['a', 'b', 'c', 'e', 'd']); + assert.deepEqual(array, ['a', 'b', 'c', 'e', 'd']); }); test('handles single item array correctly', () => { const array = [...SINGLE_ITEM_ARRAY]; reorder_list(array, 0, 0); - expect(array).toEqual(SINGLE_ITEM_ARRAY); + assert.deepEqual(array, SINGLE_ITEM_ARRAY); }); test('handles empty array correctly', () => { const array = [...EMPTY_ARRAY]; reorder_list(array, 0, 0); - expect(array).toEqual(EMPTY_ARRAY); + assert.deepEqual(array, EMPTY_ARRAY); }); // Error cases - testing that array remains unchanged with invalid indices test('handles negative from_index by leaving array unchanged', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, -1, 2); - expect(array).toEqual(SAMPLE_ARRAY); + assert.deepEqual(array, SAMPLE_ARRAY); }); test('handles out of bounds from_index by leaving array unchanged', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 10, 2); - expect(array).toEqual(SAMPLE_ARRAY); + assert.deepEqual(array, SAMPLE_ARRAY); }); test('handles negative to_index by leaving array unchanged', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 2, -1); - expect(array).toEqual(SAMPLE_ARRAY); + assert.deepEqual(array, SAMPLE_ARRAY); }); test('handles out of bounds to_index by leaving array unchanged', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 2, 10); - expect(array).toEqual(SAMPLE_ARRAY); + assert.deepEqual(array, SAMPLE_ARRAY); }); test('moves item to exact length boundary', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 2, array.length); - expect(array).toEqual(['a', 'b', 'd', 'e', 'c']); + assert.deepEqual(array, ['a', 'b', 'd', 'e', 'c']); }); test('handles adjacent indices correctly when moving forward', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 1, 2); - expect(array).toEqual(['a', 'c', 'b', 'd', 'e']); + assert.deepEqual(array, ['a', 'c', 'b', 'd', 'e']); }); test('handles adjacent indices correctly when moving backward', () => { const array = [...SAMPLE_ARRAY]; reorder_list(array, 2, 1); - expect(array).toEqual(['a', 'c', 'b', 'd', 'e']); + assert.deepEqual(array, ['a', 'c', 'b', 'd', 'e']); }); }); @@ -114,107 +112,107 @@ describe('to_reordered_list', () => { test('creates new array with item moved forward', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 1, 3); - expect(result).toEqual(['a', 'c', 'd', 'b', 'e']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['a', 'c', 'd', 'b', 'e']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('creates new array with item moved backward', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 3, 1); - expect(result).toEqual(['a', 'd', 'b', 'c', 'e']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['a', 'd', 'b', 'c', 'e']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('returns original array when from_index equals to_index', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 2, 2); - expect(result).toBe(original); // Same reference, not just equal + assert.strictEqual(result, original); // Same reference, not just equal }); // Edge cases test('creates new array with first item moved to end', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 0, 5); - expect(result).toEqual(['b', 'c', 'd', 'e', 'a']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['b', 'c', 'd', 'e', 'a']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('creates new array with first item moved one position forward', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 0, 1); - expect(result).toEqual(['b', 'a', 'c', 'd', 'e']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['b', 'a', 'c', 'd', 'e']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('creates new array with last item moved to beginning', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 4, 0); - expect(result).toEqual(['e', 'a', 'b', 'c', 'd']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['e', 'a', 'b', 'c', 'd']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('creates new array with last item moved one position backward', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 4, 3); - expect(result).toEqual(['a', 'b', 'c', 'e', 'd']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['a', 'b', 'c', 'e', 'd']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('handles single item array correctly', () => { const original = [...SINGLE_ITEM_ARRAY]; const result = to_reordered_list(original, 0, 0); - expect(result).toBe(original); // Same reference + assert.strictEqual(result, original); // Same reference }); test('handles empty array correctly', () => { const original = [...EMPTY_ARRAY]; const result = to_reordered_list(original, 0, 0); - expect(result).toBe(original); // Same reference + assert.strictEqual(result, original); // Same reference }); // Error cases - testing that original array is returned with invalid indices test('handles negative from_index by returning original array', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, -1, 2); - expect(result).toBe(original); + assert.strictEqual(result, original); }); test('handles out of bounds from_index by returning original array', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 10, 2); - expect(result).toBe(original); + assert.strictEqual(result, original); }); test('handles negative to_index by returning original array', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 2, -1); - expect(result).toBe(original); + assert.strictEqual(result, original); }); test('handles out of bounds to_index by returning original array', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 2, 10); - expect(result).toBe(original); + assert.strictEqual(result, original); }); test('creates new array with item moved to exact length boundary', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 2, original.length); - expect(result).toEqual(['a', 'b', 'd', 'e', 'c']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['a', 'b', 'd', 'e', 'c']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('creates new array with adjacent indices correctly when moving forward', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 1, 2); - expect(result).toEqual(['a', 'c', 'b', 'd', 'e']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['a', 'c', 'b', 'd', 'e']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); test('creates new array with adjacent indices correctly when moving backward', () => { const original = [...SAMPLE_ARRAY]; const result = to_reordered_list(original, 2, 1); - expect(result).toEqual(['a', 'c', 'b', 'd', 'e']); - expect(original).toEqual(SAMPLE_ARRAY); // Original unchanged + assert.deepEqual(result, ['a', 'c', 'b', 'd', 'e']); + assert.deepEqual(original, SAMPLE_ARRAY); // Original unchanged }); }); diff --git a/src/test/ollama.svelte.test.ts b/src/test/ollama.svelte.test.ts index 5d65c9622..5a3a6f328 100644 --- a/src/test/ollama.svelte.test.ts +++ b/src/test/ollama.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop claude_sonnet_4 - // @vitest-environment jsdom -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {Ollama} from '$lib/ollama.svelte.js'; import {Frontend} from '$lib/frontend.svelte.js'; @@ -23,18 +21,18 @@ describe('Ollama', () => { const app = create_test_app(); const ollama = new Ollama({app}); - expect(ollama.host).toBe(OLLAMA_URL); - expect(ollama.list_status).toBe('initial'); - expect(ollama.available).toBe(false); - expect(ollama.models.length).toBeTypeOf('number'); + assert.strictEqual(ollama.host, OLLAMA_URL); + assert.strictEqual(ollama.list_status, 'initial'); + assert.ok(!ollama.available); + assert.typeOf(ollama.models.length, 'number'); }); test('should track pending and completed actions', () => { const app = create_test_app(); const ollama = new Ollama({app}); - expect(ollama.pending_actions).toHaveLength(0); - expect(ollama.completed_actions).toHaveLength(0); + assert.strictEqual(ollama.pending_actions.length, 0); + assert.strictEqual(ollama.completed_actions.length, 0); // Add a pending action app.actions.add_from_json({ @@ -55,8 +53,8 @@ describe('Ollama', () => { }, }); - expect(ollama.pending_actions).toHaveLength(1); - expect(ollama.completed_actions).toHaveLength(0); + assert.strictEqual(ollama.pending_actions.length, 1); + assert.strictEqual(ollama.completed_actions.length, 0); // Add a completed action app.actions.add_from_json({ @@ -77,8 +75,8 @@ describe('Ollama', () => { }, }); - expect(ollama.pending_actions).toHaveLength(1); - expect(ollama.completed_actions).toHaveLength(1); + assert.strictEqual(ollama.pending_actions.length, 1); + assert.strictEqual(ollama.completed_actions.length, 1); }); test('should derive models from app.models', () => { @@ -92,11 +90,11 @@ describe('Ollama', () => { const ollama = new Ollama({app}); - expect(ollama.models).toHaveLength(2); - expect(ollama.models.length).toBe(2); - expect(ollama.model_names).toContain('llama3.2:1b'); - expect(ollama.model_names).toContain('gemma3:1b'); - expect(ollama.model_names).not.toContain('gpt-4'); + assert.strictEqual(ollama.models.length, 2); + assert.strictEqual(ollama.models.length, 2); + assert.include(ollama.model_names, 'llama3.2:1b'); + assert.include(ollama.model_names, 'gemma3:1b'); + assert.notInclude(ollama.model_names, 'gpt-4'); }); test('should update derived state correctly', () => { @@ -110,8 +108,8 @@ describe('Ollama', () => { const ollama = new Ollama({app}); ollama.list_status = 'success'; - expect(ollama.available).toBe(true); - expect(ollama.models.length).toBe(2); + assert.ok(ollama.available); + assert.strictEqual(ollama.models.length, 2); }); test('should clear model details', () => { @@ -129,15 +127,15 @@ describe('Ollama', () => { const ollama = new Ollama({app}); const model = app.models.find_by_name('test_model'); - expect(model).toBeDefined(); - expect(model!.ollama_show_response_loaded).toBe(true); - expect(model!.ollama_show_response).toEqual({license: 'MIT'}); + assert.isDefined(model); + assert.ok(model.ollama_show_response_loaded); + assert.deepEqual(model.ollama_show_response, {license: 'MIT'}); - ollama.clear_model_details(model!); + ollama.clear_model_details(model); - expect(model!.ollama_show_response).toBeUndefined(); - expect(model!.ollama_show_response_loaded).toBe(false); - expect(model!.ollama_show_response_error).toBeUndefined(); + assert.ok(model.ollama_show_response === undefined); + assert.ok(!model.ollama_show_response_loaded); + assert.ok(model.ollama_show_response_error === undefined); }); test('should handle model_by_name map', () => { @@ -151,22 +149,22 @@ describe('Ollama', () => { const ollama = new Ollama({app}); - expect(ollama.model_by_name.size).toBe(2); - expect(ollama.model_by_name.get('test1')?.name).toBe('test1'); - expect(ollama.model_by_name.get('test2')?.name).toBe('test2'); - expect(ollama.model_by_name.has('other')).toBe(false); + assert.strictEqual(ollama.model_by_name.size, 2); + assert.strictEqual(ollama.model_by_name.get('test1')?.name, 'test1'); + assert.strictEqual(ollama.model_by_name.get('test2')?.name, 'test2'); + assert.ok(!ollama.model_by_name.has('other')); }); test('should initialize ps state correctly', () => { const app = create_test_app(); const ollama = new Ollama({app}); - expect(ollama.ps_response).toBeNull(); - expect(ollama.ps_status).toBe('initial'); - expect(ollama.ps_error).toBeNull(); - expect(ollama.ps_polling_enabled).toBe(false); - expect(ollama.running_models).toEqual([]); - expect(ollama.running_model_names.size).toBe(0); + assert.isNull(ollama.ps_response); + assert.strictEqual(ollama.ps_status, 'initial'); + assert.isNull(ollama.ps_error); + assert.ok(!ollama.ps_polling_enabled); + assert.deepEqual(ollama.running_models, []); + assert.strictEqual(ollama.running_model_names.size, 0); }); test('should derive running models from ps response', () => { @@ -197,15 +195,15 @@ describe('Ollama', () => { ], }; - expect(ollama.running_models).toHaveLength(2); - expect(ollama.running_models[0]!.name).toBe('llama3.2:1b'); - expect(ollama.running_models[0]!.size_vram).toBe(1024 * 1024 * 1024); - expect(ollama.running_models[1]!.name).toBe('gemma:2b'); - expect(ollama.running_models[1]!.size_vram).toBe(2 * 1024 * 1024 * 1024); + assert.strictEqual(ollama.running_models.length, 2); + assert.strictEqual(ollama.running_models[0]!.name, 'llama3.2:1b'); + assert.strictEqual(ollama.running_models[0]!.size_vram, 1024 * 1024 * 1024); + assert.strictEqual(ollama.running_models[1]!.name, 'gemma:2b'); + assert.strictEqual(ollama.running_models[1]!.size_vram, 2 * 1024 * 1024 * 1024); - expect(ollama.running_model_names.has('llama3.2:1b')).toBe(true); - expect(ollama.running_model_names.has('gemma:2b')).toBe(true); - expect(ollama.running_model_names.has('other')).toBe(false); + assert.ok(ollama.running_model_names.has('llama3.2:1b')); + assert.ok(ollama.running_model_names.has('gemma:2b')); + assert.ok(!ollama.running_model_names.has('other')); }); test('should handle ps polling state', () => { @@ -214,19 +212,19 @@ describe('Ollama', () => { // Start polling ollama.start_ps_polling(); - expect(ollama.ps_polling_enabled).toBe(true); + assert.ok(ollama.ps_polling_enabled); // Starting again should be safe ollama.start_ps_polling(); - expect(ollama.ps_polling_enabled).toBe(true); + assert.ok(ollama.ps_polling_enabled); // Stop polling ollama.stop_ps_polling(); - expect(ollama.ps_polling_enabled).toBe(false); + assert.ok(!ollama.ps_polling_enabled); // Stopping again should be safe ollama.stop_ps_polling(); - expect(ollama.ps_polling_enabled).toBe(false); + assert.ok(!ollama.ps_polling_enabled); }); test('should filter ollama actions correctly', () => { @@ -288,10 +286,19 @@ describe('Ollama', () => { }, }); - expect(ollama.actions).toHaveLength(2); - expect(ollama.actions.map((a) => a.method)).toContain('ollama_pull'); - expect(ollama.actions.map((a) => a.method)).toContain('ollama_list'); - expect(ollama.actions.map((a) => a.method)).not.toContain('completion_create'); + assert.strictEqual(ollama.actions.length, 2); + assert.include( + ollama.actions.map((a) => a.method), + 'ollama_pull', + ); + assert.include( + ollama.actions.map((a) => a.method), + 'ollama_list', + ); + assert.notInclude( + ollama.actions.map((a) => a.method), + 'completion_create', + ); }); test('should filter read operations when show_read_actions is false', () => { @@ -336,13 +343,13 @@ describe('Ollama', () => { }); // With show_read_actions = false (default) - expect(ollama.show_read_actions).toBe(false); - expect(ollama.filtered_actions).toHaveLength(1); - expect(ollama.filtered_actions[0]!.method).toBe('ollama_pull'); + assert.ok(!ollama.show_read_actions); + assert.strictEqual(ollama.filtered_actions.length, 1); + assert.strictEqual(ollama.filtered_actions[0]!.method, 'ollama_pull'); // With show_read_actions = true ollama.show_read_actions = true; - expect(ollama.filtered_actions).toHaveLength(2); + assert.strictEqual(ollama.filtered_actions.length, 2); }); test('should handle action progress tracking', () => { @@ -368,8 +375,8 @@ describe('Ollama', () => { }, }); - expect(ollama.pending_actions).toHaveLength(1); - expect(ollama.pending_actions[0]!.action_event_data?.progress).toEqual({ + assert.strictEqual(ollama.pending_actions.length, 1); + assert.deepEqual(ollama.pending_actions[0]!.action_event_data?.progress, { status: 'downloading', completed: 50, total: 100, @@ -394,12 +401,12 @@ describe('Ollama', () => { // Update progress through the action event action_event.update_progress({status: 'downloading', completed: 75, total: 100}); - expect(action.action_event_data?.progress).toEqual({ + assert.deepEqual(action.action_event_data?.progress, { status: 'downloading', completed: 75, total: 100, }); - expect(ollama.pending_actions[0]!.action_event_data?.progress).toEqual({ + assert.deepEqual(ollama.pending_actions[0]!.action_event_data?.progress, { status: 'downloading', completed: 75, total: 100, @@ -410,10 +417,10 @@ describe('Ollama', () => { const app = create_test_app(); const ollama = new Ollama({app}); - expect(ollama.actions).toHaveLength(0); - expect(ollama.pending_actions).toHaveLength(0); - expect(ollama.completed_actions).toHaveLength(0); - expect(ollama.filtered_actions).toHaveLength(0); + assert.strictEqual(ollama.actions.length, 0); + assert.strictEqual(ollama.pending_actions.length, 0); + assert.strictEqual(ollama.completed_actions.length, 0); + assert.strictEqual(ollama.filtered_actions.length, 0); }); test('should handle failed actions', () => { @@ -438,9 +445,9 @@ describe('Ollama', () => { }, }); - expect(ollama.pending_actions).toHaveLength(0); - expect(ollama.completed_actions).toHaveLength(1); - expect(ollama.completed_actions[0]!.action_event_data?.step).toBe('failed'); + assert.strictEqual(ollama.pending_actions.length, 0); + assert.strictEqual(ollama.completed_actions.length, 1); + assert.strictEqual(ollama.completed_actions[0]!.action_event_data?.step, 'failed'); }); test('should only include ollama provider models', () => { @@ -453,9 +460,9 @@ describe('Ollama', () => { const ollama = new Ollama({app}); - expect(ollama.models).toHaveLength(1); - expect(ollama.models[0]!.name).toBe('ollama_model'); - expect(ollama.models[0]!.provider_name).toBe('ollama'); + assert.strictEqual(ollama.models.length, 1); + assert.strictEqual(ollama.models[0]!.name, 'ollama_model'); + assert.strictEqual(ollama.models[0]!.provider_name, 'ollama'); }); test('should handle ps response with empty models array', () => { @@ -464,7 +471,7 @@ describe('Ollama', () => { ollama.ps_response = {models: []}; - expect(ollama.running_models).toHaveLength(0); - expect(ollama.running_model_names.size).toBe(0); + assert.strictEqual(ollama.running_models.length, 0); + assert.strictEqual(ollama.running_model_names.size, 0); }); }); diff --git a/src/test/part.svelte.base.test.ts b/src/test/part.svelte.base.test.ts index d00e94722..93d605676 100644 --- a/src/test/part.svelte.base.test.ts +++ b/src/test/part.svelte.base.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, beforeEach} from 'vitest'; +import {test, assert, describe, beforeEach} from 'vitest'; import {Part, TextPart, DiskfilePart} from '$lib/part.svelte.js'; import {create_uuid, get_datetime_now} from '$lib/zod_helpers.js'; @@ -43,34 +41,34 @@ describe('Part base class functionality', () => { for (const part of [text_part, diskfile_part]) { part.add_attribute({key: 'test-attr', value: 'test-value'}); - expect(part.attributes).toHaveLength(1); + assert.strictEqual(part.attributes.length, 1); let first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected first attribute'); - expect(first_attr.key).toBe('test-attr'); - expect(first_attr.value).toBe('test-value'); + assert.strictEqual(first_attr.key, 'test-attr'); + assert.strictEqual(first_attr.value, 'test-value'); const attr_id = first_attr.id; const updated = part.update_attribute(attr_id, {value: 'updated-value'}); - expect(updated).toBe(true); + assert.ok(updated); first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected attribute after update'); - expect(first_attr.key).toBe('test-attr'); - expect(first_attr.value).toBe('updated-value'); + assert.strictEqual(first_attr.key, 'test-attr'); + assert.strictEqual(first_attr.value, 'updated-value'); part.update_attribute(attr_id, {key: 'updated-key', value: 'updated-value-2'}); first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected attribute after second update'); - expect(first_attr.key).toBe('updated-key'); - expect(first_attr.value).toBe('updated-value-2'); + assert.strictEqual(first_attr.key, 'updated-key'); + assert.strictEqual(first_attr.value, 'updated-value-2'); part.remove_attribute(attr_id); - expect(part.attributes).toHaveLength(0); + assert.strictEqual(part.attributes.length, 0); const non_existent_update = part.update_attribute(create_uuid(), { value: 'test', }); - expect(non_existent_update).toBe(false); + assert.ok(!non_existent_update); } }); @@ -82,14 +80,14 @@ describe('Part base class functionality', () => { }); // Test initial derivations - expect(text_part.length).toBe(TEST_CONTENT.BASIC.length); - expect(text_part.token_count).toBe(estimate_token_count(TEST_CONTENT.BASIC)); + assert.strictEqual(text_part.length, TEST_CONTENT.BASIC.length); + assert.strictEqual(text_part.token_count, estimate_token_count(TEST_CONTENT.BASIC)); // Test derivations after content change text_part.content = TEST_CONTENT.SECONDARY; - expect(text_part.length).toBe(TEST_CONTENT.SECONDARY.length); - expect(text_part.token_count).toBe(estimate_token_count(TEST_CONTENT.SECONDARY)); + assert.strictEqual(text_part.length, TEST_CONTENT.SECONDARY.length); + assert.strictEqual(text_part.token_count, estimate_token_count(TEST_CONTENT.SECONDARY)); }); }); @@ -107,15 +105,15 @@ describe('Part factory method', () => { name: 'Diskfile Part', }); - expect(text_part).toBeInstanceOf(TextPart); - expect(text_part.type).toBe('text'); - expect(text_part.name).toBe('Text Part'); - expect(text_part.content).toBe(TEST_CONTENT.BASIC); + assert.instanceOf(text_part, TextPart); + assert.strictEqual(text_part.type, 'text'); + assert.strictEqual(text_part.name, 'Text Part'); + assert.strictEqual(text_part.content, TEST_CONTENT.BASIC); - expect(diskfile_part).toBeInstanceOf(DiskfilePart); - expect(diskfile_part.type).toBe('diskfile'); - expect(diskfile_part.name).toBe('Diskfile Part'); - expect(diskfile_part.path).toBe(TEST_PATH); + assert.instanceOf(diskfile_part, DiskfilePart); + assert.strictEqual(diskfile_part.type, 'diskfile'); + assert.strictEqual(diskfile_part.name, 'Diskfile Part'); + assert.strictEqual(diskfile_part.path, TEST_PATH); }); test('Part.create throws error for unknown part type', () => { @@ -123,7 +121,7 @@ describe('Part factory method', () => { type: 'unknown' as const, }; - expect(() => Part.create(app, invalid_json as any)).toThrow('Unreachable case: unknown'); + assert.throws(() => Part.create(app, invalid_json as any), /Unreachable case: unknown/); }); test('Part.create throws error for missing type field', () => { @@ -131,8 +129,9 @@ describe('Part factory method', () => { name: 'Test', }; - expect(() => Part.create(app, invalid_json as any)).toThrow( - 'Missing required "type" field in part JSON', + assert.throws( + () => Part.create(app, invalid_json as any), + /Missing required "type" field in part JSON/, ); }); }); @@ -145,16 +144,16 @@ describe('TextPart specific behavior', () => { content: TEST_CONTENT.BASIC, }); - expect(part.type).toBe('text'); - expect(part.content).toBe(TEST_CONTENT.BASIC); + assert.strictEqual(part.type, 'text'); + assert.strictEqual(part.content, TEST_CONTENT.BASIC); // Test update method part.content = TEST_CONTENT.SECONDARY; - expect(part.content).toBe(TEST_CONTENT.SECONDARY); + assert.strictEqual(part.content, TEST_CONTENT.SECONDARY); // Test direct property assignment part.content = TEST_CONTENT.EMPTY; - expect(part.content).toBe(TEST_CONTENT.EMPTY); + assert.strictEqual(part.content, TEST_CONTENT.EMPTY); }); test('TextPart serialization and deserialization', () => { @@ -187,22 +186,22 @@ describe('TextPart specific behavior', () => { const restored = app.cell_registry.instantiate('TextPart', json); // Verify all properties were preserved - expect(restored.id).toBe(test_id); - expect(restored.created).toBe(test_date); - expect(restored.content).toBe(TEST_CONTENT.BASIC); - expect(restored.name).toBe('Test part'); - expect(restored.has_xml_tag).toBe(true); - expect(restored.xml_tag_name).toBe('test'); - expect(restored.start).toBe(5); - expect(restored.end).toBe(15); - expect(restored.enabled).toBe(false); - expect(restored.title).toBe('Test Title'); - expect(restored.summary).toBe('Test Summary'); - expect(restored.attributes).toHaveLength(1); + assert.strictEqual(restored.id, test_id); + assert.strictEqual(restored.created, test_date); + assert.strictEqual(restored.content, TEST_CONTENT.BASIC); + assert.strictEqual(restored.name, 'Test part'); + assert.ok(restored.has_xml_tag); + assert.strictEqual(restored.xml_tag_name, 'test'); + assert.strictEqual(restored.start, 5); + assert.strictEqual(restored.end, 15); + assert.ok(!restored.enabled); + assert.strictEqual(restored.title, 'Test Title'); + assert.strictEqual(restored.summary, 'Test Summary'); + assert.strictEqual(restored.attributes.length, 1); const restored_attr = restored.attributes[0]; if (!restored_attr) throw new Error('Expected restored attribute'); - expect(restored_attr.key).toBe('class'); - expect(restored_attr.value).toBe('highlight'); + assert.strictEqual(restored_attr.key, 'class'); + assert.strictEqual(restored_attr.value, 'highlight'); }); test('TextPart cloning creates independent copy', () => { @@ -217,21 +216,21 @@ describe('TextPart specific behavior', () => { const clone = original.clone(); // Verify initial state is the same except id - expect(clone.id).not.toBe(original.id); - expect(clone.content).toBe(original.content); - expect(clone.name).toBe(original.name); + assert.ok(clone.id !== original.id); + assert.strictEqual(clone.content, original.content); + assert.strictEqual(clone.name, original.name); // Modify clone clone.content = TEST_CONTENT.SECONDARY; clone.name = 'Modified'; // Verify original remains unchanged - expect(original.content).toBe(TEST_CONTENT.BASIC); - expect(original.name).toBe('Original'); + assert.strictEqual(original.content, TEST_CONTENT.BASIC); + assert.strictEqual(original.name, 'Original'); // Verify clone has new values - expect(clone.content).toBe(TEST_CONTENT.SECONDARY); - expect(clone.name).toBe('Modified'); + assert.strictEqual(clone.content, TEST_CONTENT.SECONDARY); + assert.strictEqual(clone.name, 'Modified'); }); }); @@ -251,17 +250,17 @@ describe('DiskfilePart specific behavior', () => { }); // Test basic properties - expect(part.type).toBe('diskfile'); - expect(part.path).toBe(TEST_PATH); - expect(part.diskfile).toEqual(diskfile); - expect(part.content).toBe(TEST_CONTENT.BASIC); + assert.strictEqual(part.type, 'diskfile'); + assert.strictEqual(part.path, TEST_PATH); + assert.deepEqual(part.diskfile, diskfile); + assert.strictEqual(part.content, TEST_CONTENT.BASIC); // Update content through part part.content = TEST_CONTENT.SECONDARY; // Verify both part and diskfile were updated - expect(part.content).toBe(TEST_CONTENT.SECONDARY); - expect(part.diskfile?.content).toBe(TEST_CONTENT.SECONDARY); + assert.strictEqual(part.content, TEST_CONTENT.SECONDARY); + assert.strictEqual(part.diskfile?.content, TEST_CONTENT.SECONDARY); }); test('DiskfilePart handles null path properly', () => { @@ -270,9 +269,9 @@ describe('DiskfilePart specific behavior', () => { path: null, }); - expect(part.path).toBeNull(); - expect(part.diskfile).toBeNull(); - expect(part.content).toBeUndefined(); + assert.isNull(part.path); + assert.isNull(part.diskfile); + assert.isUndefined(part.content); }); test('DiskfilePart handles changing path', () => { @@ -298,14 +297,14 @@ describe('DiskfilePart specific behavior', () => { path: path1, }); - expect(part.path).toBe(path1); - expect(part.content).toBe('File 1 content'); + assert.strictEqual(part.path, path1); + assert.strictEqual(part.content, 'File 1 content'); // Change path to reference second file part.path = path2; - expect(part.path).toBe(path2); - expect(part.content).toBe('File 2 content'); + assert.strictEqual(part.path, path2); + assert.strictEqual(part.content, 'File 2 content'); }); }); @@ -325,11 +324,11 @@ describe('Common part behavior across types', () => { end: 20, }); - expect(text_part.start).toBe(5); - expect(text_part.end).toBe(10); + assert.strictEqual(text_part.start, 5); + assert.strictEqual(text_part.end, 10); - expect(diskfile_part.start).toBe(15); - expect(diskfile_part.end).toBe(20); + assert.strictEqual(diskfile_part.start, 15); + assert.strictEqual(diskfile_part.end, 20); text_part.start = 6; text_part.end = 11; @@ -337,11 +336,11 @@ describe('Common part behavior across types', () => { diskfile_part.start = 16; diskfile_part.end = 21; - expect(text_part.start).toBe(6); - expect(text_part.end).toBe(11); + assert.strictEqual(text_part.start, 6); + assert.strictEqual(text_part.end, 11); - expect(diskfile_part.start).toBe(16); - expect(diskfile_part.end).toBe(21); + assert.strictEqual(diskfile_part.start, 16); + assert.strictEqual(diskfile_part.end, 21); }); test('XML tag properties work across part types', () => { @@ -357,30 +356,30 @@ describe('Common part behavior across types', () => { xml_tag_name: 'file-tag', }); - expect(text_part.has_xml_tag).toBe(true); - expect(text_part.xml_tag_name).toBe('text-tag'); + assert.ok(text_part.has_xml_tag); + assert.strictEqual(text_part.xml_tag_name, 'text-tag'); - expect(diskfile_part.has_xml_tag).toBe(true); - expect(diskfile_part.xml_tag_name).toBe('file-tag'); + assert.ok(diskfile_part.has_xml_tag); + assert.strictEqual(diskfile_part.xml_tag_name, 'file-tag'); text_part.has_xml_tag = false; text_part.xml_tag_name = ''; diskfile_part.xml_tag_name = 'updated-file-tag'; - expect(text_part.has_xml_tag).toBe(false); - expect(text_part.xml_tag_name).toBe(''); + assert.ok(!text_part.has_xml_tag); + assert.strictEqual(text_part.xml_tag_name, ''); - expect(diskfile_part.has_xml_tag).toBe(true); - expect(diskfile_part.xml_tag_name).toBe('updated-file-tag'); + assert.ok(diskfile_part.has_xml_tag); + assert.strictEqual(diskfile_part.xml_tag_name, 'updated-file-tag'); }); test('has_xml_tag defaults correctly for each part type', () => { const text_part = app.cell_registry.instantiate('TextPart'); const diskfile_part = app.cell_registry.instantiate('DiskfilePart'); - expect(text_part.has_xml_tag).toBe(false); - expect(diskfile_part.has_xml_tag).toBe(true); + assert.ok(!text_part.has_xml_tag); + assert.ok(diskfile_part.has_xml_tag); const custom_text_part = app.cell_registry.instantiate('TextPart', { has_xml_tag: true, @@ -389,7 +388,7 @@ describe('Common part behavior across types', () => { has_xml_tag: false, }); - expect(custom_text_part.has_xml_tag).toBe(true); - expect(custom_diskfile_part.has_xml_tag).toBe(false); + assert.ok(custom_text_part.has_xml_tag); + assert.ok(!custom_diskfile_part.has_xml_tag); }); }); diff --git a/src/test/part.svelte.diskfile.test.ts b/src/test/part.svelte.diskfile.test.ts index 19e9534d5..3cec01ce2 100644 --- a/src/test/part.svelte.diskfile.test.ts +++ b/src/test/part.svelte.diskfile.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, beforeEach} from 'vitest'; +import {test, describe, beforeEach, assert} from 'vitest'; import {create_uuid, get_datetime_now} from '$lib/zod_helpers.js'; import {Frontend} from '$lib/frontend.svelte.js'; @@ -87,15 +85,15 @@ describe('DiskfilePart initialization', () => { path, }); - expect(part.type).toBe('diskfile'); - expect(part.path).toBe(path); - expect(part.name).toBe(''); - expect(part.enabled).toBe(true); - expect(part.has_xml_tag).toBe(true); - expect(part.xml_tag_name).toBe(''); - expect(part.attributes).toEqual([]); - expect(part.start).toBeNull(); - expect(part.end).toBeNull(); + assert.strictEqual(part.type, 'diskfile'); + assert.strictEqual(part.path, path); + assert.strictEqual(part.name, ''); + assert.ok(part.enabled); + assert.ok(part.has_xml_tag); + assert.strictEqual(part.xml_tag_name, ''); + assert.deepEqual(part.attributes, []); + assert.isNull(part.start); + assert.isNull(part.end); }); test('initializes from json with complete properties', () => { @@ -119,22 +117,22 @@ describe('DiskfilePart initialization', () => { attributes: [{id: create_uuid(), key: 'format', value: 'json'}], }); - expect(part.id).toBe(test_id); - expect(part.created).toBe(test_date); - expect(part.path).toBe(test_path); - expect(part.name).toBe('Config file'); - expect(part.has_xml_tag).toBe(true); - expect(part.xml_tag_name).toBe('config'); - expect(part.title).toBe('Configuration'); - expect(part.summary).toBe('System configuration file'); - expect(part.start).toBe(5); - expect(part.end).toBe(20); - expect(part.enabled).toBe(false); - expect(part.attributes).toHaveLength(1); + assert.strictEqual(part.id, test_id); + assert.strictEqual(part.created, test_date); + assert.strictEqual(part.path, test_path); + assert.strictEqual(part.name, 'Config file'); + assert.ok(part.has_xml_tag); + assert.strictEqual(part.xml_tag_name, 'config'); + assert.strictEqual(part.title, 'Configuration'); + assert.strictEqual(part.summary, 'System configuration file'); + assert.strictEqual(part.start, 5); + assert.strictEqual(part.end, 20); + assert.ok(!part.enabled); + assert.strictEqual(part.attributes.length, 1); const first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected first attribute'); - expect(first_attr.key).toBe('format'); - expect(first_attr.value).toBe('json'); + assert.strictEqual(first_attr.key, 'format'); + assert.strictEqual(first_attr.value, 'json'); }); test('initializes with null path', () => { @@ -143,9 +141,9 @@ describe('DiskfilePart initialization', () => { path: null, }); - expect(part.path).toBeNull(); - expect(part.diskfile).toBeNull(); - expect(part.content).toBeUndefined(); + assert.isNull(part.path); + assert.isNull(part.diskfile); + assert.ok(part.content === undefined); }); }); @@ -159,8 +157,8 @@ describe('DiskfilePart content access', () => { path, }); - expect(part.content).toBe(content); - expect(part.diskfile).toEqual(test_diskfiles.get(path)); + assert.strictEqual(part.content, content); + assert.deepEqual(part.diskfile, test_diskfiles.get(path)); }); test('content setter updates diskfile content', () => { @@ -174,15 +172,15 @@ describe('DiskfilePart content access', () => { }); // Verify initial state - expect(part.content).toBe(initial_content); + assert.strictEqual(part.content, initial_content); // Update content part.content = updated_content; // Verify diskfile was updated - get it fresh from zzz const diskfile = app.diskfiles.get_by_path(path); - expect(diskfile?.content).toBe(updated_content); - expect(part.content).toBe(updated_content); + assert.strictEqual(diskfile?.content, updated_content); + assert.strictEqual(part.content, updated_content); }); test('assigning part content updates diskfile content', () => { @@ -196,15 +194,15 @@ describe('DiskfilePart content access', () => { }); // Verify initial state - expect(part.content).toBe(initial_content); + assert.strictEqual(part.content, initial_content); // Update content using assignment part.content = updated_content; // Verify diskfile was updated - get it fresh from zzz const diskfile = app.diskfiles.get_by_path(path); - expect(diskfile?.content).toBe(updated_content); - expect(part.content).toBe(updated_content); + assert.strictEqual(diskfile?.content, updated_content); + assert.strictEqual(part.content, updated_content); }); test('content is undefined when diskfile not found', () => { @@ -215,8 +213,8 @@ describe('DiskfilePart content access', () => { path, }); - expect(part.diskfile).toBeUndefined(); - expect(part.content).toBeUndefined(); + assert.ok(part.diskfile === undefined); + assert.ok(part.content === undefined); }); test('setting content to null logs error in development', () => { @@ -242,11 +240,11 @@ describe('DiskfilePart content access', () => { console.error = original_console_error; // Verify error was logged - expect(error_called).toBe(true); + assert.ok(error_called); // Verify diskfile content was not changed const diskfile = test_diskfiles.get(path); - expect(diskfile?.content).toBe(TEST_CONTENT.BASIC); + assert.strictEqual(diskfile?.content, TEST_CONTENT.BASIC); }); }); @@ -262,15 +260,15 @@ describe('DiskfilePart reactive properties', () => { }); // Verify initial state - expect(part.content).toBe(initial_content); - expect(part.length).toBe(initial_content.length); + assert.strictEqual(part.content, initial_content); + assert.strictEqual(part.length, initial_content.length); // Update diskfile content directly part.diskfile!.content = updated_content; // Verify derived properties update - expect(part.content).toBe(updated_content); - expect(part.length).toBe(updated_content.length); + assert.strictEqual(part.content, updated_content); + assert.strictEqual(part.length, updated_content.length); }); test('derived properties update when path changes', () => { @@ -283,14 +281,14 @@ describe('DiskfilePart reactive properties', () => { }); // Verify initial state - expect(part.content).toBe(TEST_CONTENT.BASIC); + assert.strictEqual(part.content, TEST_CONTENT.BASIC); // Change path part.path = path2; // Verify derived properties update - expect(part.content).toBe(TEST_CONTENT.CONFIG); - expect(part.diskfile).toEqual(test_diskfiles.get(path2)); + assert.strictEqual(part.content, TEST_CONTENT.CONFIG); + assert.deepEqual(part.diskfile, test_diskfiles.get(path2)); }); }); @@ -312,15 +310,15 @@ describe('DiskfilePart serialization', () => { const json = part.to_json(); - expect(json.id).toBe(test_id); - expect(json.type).toBe('diskfile'); - expect(json.created).toBe(created); - expect(json.path).toBe(path); - expect(json.name).toBe('Test file'); - expect(json.start).toBe(10); - expect(json.end).toBe(20); - expect(json.has_xml_tag).toBe(true); - expect(json.enabled).toBe(true); + assert.strictEqual(json.id, test_id); + assert.strictEqual(json.type, 'diskfile'); + assert.strictEqual(json.created, created); + assert.strictEqual(json.path, path); + assert.strictEqual(json.name, 'Test file'); + assert.strictEqual(json.start, 10); + assert.strictEqual(json.end, 20); + assert.ok(json.has_xml_tag); + assert.ok(json.enabled); }); test('clone creates independent copy with same path', () => { @@ -336,18 +334,18 @@ describe('DiskfilePart serialization', () => { const clone = original.clone(); // Verify they have same initial values except id - expect(clone.id).not.toBe(original.id); - expect(clone.path).toBe(original_path); - expect(clone.name).toBe('Original name'); + assert.notStrictEqual(clone.id, original.id); + assert.strictEqual(clone.path, original_path); + assert.strictEqual(clone.name, 'Original name'); // Verify they're independent objects clone.path = modified_path; clone.name = 'Modified name'; - expect(original.path).toBe(original_path); - expect(original.name).toBe('Original name'); - expect(clone.path).toBe(modified_path); - expect(clone.name).toBe('Modified name'); + assert.strictEqual(original.path, original_path); + assert.strictEqual(original.name, 'Original name'); + assert.strictEqual(clone.path, modified_path); + assert.strictEqual(clone.name, 'Modified name'); }); }); @@ -360,9 +358,9 @@ describe('DiskfilePart edge cases', () => { path, }); - expect(part.path).toBe(path); - expect(part.content).toBe(TEST_CONTENT.BASIC); - expect(part.diskfile).toEqual(test_diskfiles.get(path)); + assert.strictEqual(part.path, path); + assert.strictEqual(part.content, TEST_CONTENT.BASIC); + assert.deepEqual(part.diskfile, test_diskfiles.get(path)); }); test('handles empty content', () => { @@ -375,9 +373,9 @@ describe('DiskfilePart edge cases', () => { path, }); - expect(part.content).toBe(''); - expect(part.length).toBe(0); - expect(part.token_count).toBe(0); + assert.strictEqual(part.content, ''); + assert.strictEqual(part.length, 0); + assert.strictEqual(part.token_count, 0); }); test('handles binary file content', () => { @@ -391,8 +389,8 @@ describe('DiskfilePart edge cases', () => { path, }); - expect(part.content).toBe(binary_content); - expect(part.length).toBe(binary_content.length); + assert.strictEqual(part.content, binary_content); + assert.strictEqual(part.length, binary_content.length); }); test('handles changing from null path to valid path', () => { @@ -402,18 +400,18 @@ describe('DiskfilePart edge cases', () => { }); // Verify initial state - expect(part.path).toBeNull(); - expect(part.diskfile).toBeNull(); - expect(part.content).toBeUndefined(); + assert.isNull(part.path); + assert.isNull(part.diskfile); + assert.ok(part.content === undefined); // Set to valid path const path = TEST_PATHS.BASIC; part.path = path; // Verify properties updated - expect(part.path).toBe(path); - expect(part.diskfile?.id).toBe(test_diskfiles.get(path)?.id); - expect(part.content).toBe(TEST_CONTENT.BASIC); + assert.strictEqual(part.path, path); + assert.strictEqual((part as any).diskfile?.id, test_diskfiles.get(path)?.id); + assert.strictEqual(part.content, TEST_CONTENT.BASIC); }); test('handles changing from valid path to null path', () => { @@ -424,16 +422,16 @@ describe('DiskfilePart edge cases', () => { }); // Verify initial state - expect(part.path).toBe(path); - expect(part.diskfile?.id).toBe(test_diskfiles.get(path)?.id); + assert.strictEqual(part.path, path); + assert.strictEqual((part as any).diskfile?.id, test_diskfiles.get(path)?.id); // Set to null path part.path = null; // Verify properties updated - expect(part.path).toBeNull(); - expect(part.diskfile).toBeNull(); - expect(part.content).toBeUndefined(); + assert.isNull(part.path); + assert.isNull(part.diskfile); + assert.ok(part.content === undefined); }); }); @@ -446,29 +444,29 @@ describe('DiskfilePart attribute management', () => { // Add attribute part.add_attribute({key: 'mime-type', value: 'text/plain'}); - expect(part.attributes).toHaveLength(1); + assert.strictEqual(part.attributes.length, 1); let first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected first attribute'); - expect(first_attr.key).toBe('mime-type'); - expect(first_attr.value).toBe('text/plain'); + assert.strictEqual(first_attr.key, 'mime-type'); + assert.strictEqual(first_attr.value, 'text/plain'); const attr_id = first_attr.id; // Update attribute const updated = part.update_attribute(attr_id, {value: 'application/text'}); - expect(updated).toBe(true); + assert.ok(updated); first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected attribute after update'); - expect(first_attr.key).toBe('mime-type'); - expect(first_attr.value).toBe('application/text'); + assert.strictEqual(first_attr.key, 'mime-type'); + assert.strictEqual(first_attr.value, 'application/text'); // Remove attribute part.remove_attribute(attr_id); - expect(part.attributes).toHaveLength(0); + assert.strictEqual(part.attributes.length, 0); // Attempting to update non-existent attribute returns false const fake_update = part.update_attribute(create_uuid(), {key: 'test', value: 'test'}); - expect(fake_update).toBe(false); + assert.ok(!fake_update); }); test('updates attribute key and value together', () => { @@ -484,11 +482,11 @@ describe('DiskfilePart attribute management', () => { // Update both key and value const updated = part.update_attribute(attr_id, {key: 'data-type', value: 'important'}); - expect(updated).toBe(true); + assert.ok(updated); const updated_attr = part.attributes[0]; if (!updated_attr) throw new Error('Expected attribute after update'); - expect(updated_attr.key).toBe('data-type'); - expect(updated_attr.value).toBe('important'); + assert.strictEqual(updated_attr.key, 'data-type'); + assert.strictEqual(updated_attr.value, 'important'); }); test('attributes are preserved when serializing to JSON', () => { @@ -502,22 +500,22 @@ describe('DiskfilePart attribute management', () => { const json = part.to_json(); - expect(json.attributes).toHaveLength(2); + assert.strictEqual(json.attributes.length, 2); const json_attr0 = json.attributes[0]; const json_attr1 = json.attributes[1]; if (!json_attr0 || !json_attr1) throw new Error('Expected both attributes in JSON'); - expect(json_attr0.key).toBe('data-test'); - expect(json_attr1.key).toBe('class'); + assert.strictEqual(json_attr0.key, 'data-test'); + assert.strictEqual(json_attr1.key, 'class'); // Verify they're properly restored const new_part = app.cell_registry.instantiate('DiskfilePart', json); - expect(new_part.attributes).toHaveLength(2); + assert.strictEqual(new_part.attributes.length, 2); const new_attr0 = new_part.attributes[0]; const new_attr1 = new_part.attributes[1]; if (!new_attr0 || !new_attr1) throw new Error('Expected both attributes in restored part'); - expect(new_attr0.key).toBe('data-test'); - expect(new_attr1.key).toBe('class'); + assert.strictEqual(new_attr0.key, 'data-test'); + assert.strictEqual(new_attr1.key, 'class'); }); }); @@ -530,8 +528,8 @@ describe('DiskfilePart position markers', () => { end: 25, }); - expect(part.start).toBe(10); - expect(part.end).toBe(25); + assert.strictEqual(part.start, 10); + assert.strictEqual(part.end, 25); }); test('start and end positions can be updated', () => { @@ -541,14 +539,14 @@ describe('DiskfilePart position markers', () => { }); // Initial values are null - expect(part.start).toBeNull(); - expect(part.end).toBeNull(); + assert.isNull(part.start); + assert.isNull(part.end); // Update positions part.start = 5; part.end = 15; - expect(part.start).toBe(5); - expect(part.end).toBe(15); + assert.strictEqual(part.start, 5); + assert.strictEqual(part.end, 15); }); }); diff --git a/src/test/part.svelte.text.test.ts b/src/test/part.svelte.text.test.ts index bcabbcb17..094b6feb0 100644 --- a/src/test/part.svelte.text.test.ts +++ b/src/test/part.svelte.text.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, beforeEach} from 'vitest'; +import {test, describe, beforeEach, assert} from 'vitest'; import {estimate_token_count} from '$lib/helpers.js'; import {create_uuid, get_datetime_now} from '$lib/zod_helpers.js'; @@ -40,17 +38,17 @@ describe('TextPart initialization', () => { test('creates with default values when no options provided', () => { const part = app.cell_registry.instantiate('TextPart'); - expect(part.type).toBe('text'); - expect(part.content).toBe(TEST_CONTENT.EMPTY); - expect(part.length).toBe(TEST_CONTENT.EMPTY.length); - expect(part.token_count).toBe(0); - expect(part.name).toBe(''); - expect(part.enabled).toBe(true); - expect(part.has_xml_tag).toBe(false); - expect(part.xml_tag_name).toBe(''); - expect(part.attributes).toEqual([]); - expect(part.start).toBeNull(); - expect(part.end).toBeNull(); + assert.strictEqual(part.type, 'text'); + assert.strictEqual(part.content, TEST_CONTENT.EMPTY); + assert.strictEqual(part.length, TEST_CONTENT.EMPTY.length); + assert.strictEqual(part.token_count, 0); + assert.strictEqual(part.name, ''); + assert.ok(part.enabled); + assert.ok(!part.has_xml_tag); + assert.strictEqual(part.xml_tag_name, ''); + assert.deepEqual(part.attributes, []); + assert.isNull(part.start); + assert.isNull(part.end); }); test('initializes with direct content property', () => { @@ -60,9 +58,9 @@ describe('TextPart initialization', () => { content, }); - expect(part.content).toBe(content); - expect(part.length).toBe(content.length); - expect(part.token_count).toBe(estimate_token_count(content)); + assert.strictEqual(part.content, content); + assert.strictEqual(part.length, content.length); + assert.strictEqual(part.token_count, estimate_token_count(content)); }); test('initializes from json with complete properties', () => { @@ -85,22 +83,22 @@ describe('TextPart initialization', () => { attributes: [{id: create_uuid(), key: 'attr1', value: 'value1'}], }); - expect(part.id).toBe(test_id); - expect(part.created).toBe(test_date); - expect(part.content).toBe('Json content'); - expect(part.name).toBe('Test name'); - expect(part.has_xml_tag).toBe(true); - expect(part.xml_tag_name).toBe('test-element'); - expect(part.title).toBe('Test Title'); - expect(part.summary).toBe('Test summary text'); - expect(part.start).toBe(5); - expect(part.end).toBe(20); - expect(part.enabled).toBe(false); - expect(part.attributes).toHaveLength(1); + assert.strictEqual(part.id, test_id); + assert.strictEqual(part.created, test_date); + assert.strictEqual(part.content, 'Json content'); + assert.strictEqual(part.name, 'Test name'); + assert.ok(part.has_xml_tag); + assert.strictEqual(part.xml_tag_name, 'test-element'); + assert.strictEqual(part.title, 'Test Title'); + assert.strictEqual(part.summary, 'Test summary text'); + assert.strictEqual(part.start, 5); + assert.strictEqual(part.end, 20); + assert.ok(!part.enabled); + assert.strictEqual(part.attributes.length, 1); const first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected first attribute'); - expect(first_attr.key).toBe('attr1'); - expect(first_attr.value).toBe('value1'); + assert.strictEqual(first_attr.key, 'attr1'); + assert.strictEqual(first_attr.value, 'value1'); }); }); @@ -112,18 +110,18 @@ describe('TextPart reactive properties', () => { }); // Verify initial state - expect(part.content).toBe(TEST_CONTENT.INITIAL); - expect(part.length).toBe(TEST_CONTENT.INITIAL.length); + assert.strictEqual(part.content, TEST_CONTENT.INITIAL); + assert.strictEqual(part.length, TEST_CONTENT.INITIAL.length); const initial_token_count = part.token_count; // Change content part.content = TEST_CONTENT.NEW_CONTENT; // Verify derived properties update automatically - expect(part.content).toBe(TEST_CONTENT.NEW_CONTENT); - expect(part.length).toBe(TEST_CONTENT.NEW_CONTENT.length); - expect(part.token_count).not.toBe(initial_token_count); - expect(part.token_count).toEqual(estimate_token_count(TEST_CONTENT.NEW_CONTENT)); + assert.strictEqual(part.content, TEST_CONTENT.NEW_CONTENT); + assert.strictEqual(part.length, TEST_CONTENT.NEW_CONTENT.length); + assert.notStrictEqual(part.token_count, initial_token_count); + assert.deepEqual(part.token_count, estimate_token_count(TEST_CONTENT.NEW_CONTENT)); }); test('length is zero when content is empty', () => { @@ -132,14 +130,14 @@ describe('TextPart reactive properties', () => { content: TEST_CONTENT.EMPTY, }); - expect(part.content).toBe(TEST_CONTENT.EMPTY); - expect(part.length).toBe(TEST_CONTENT.EMPTY.length); + assert.strictEqual(part.content, TEST_CONTENT.EMPTY); + assert.strictEqual(part.length, TEST_CONTENT.EMPTY.length); part.content = TEST_CONTENT.SOMETHING; - expect(part.length).toBe(TEST_CONTENT.SOMETHING.length); + assert.strictEqual(part.length, TEST_CONTENT.SOMETHING.length); part.content = TEST_CONTENT.EMPTY; - expect(part.length).toBe(TEST_CONTENT.EMPTY.length); + assert.strictEqual(part.length, TEST_CONTENT.EMPTY.length); }); }); @@ -160,15 +158,15 @@ describe('TextPart serialization', () => { const json = part.to_json(); - expect(json.id).toBe(test_id); - expect(json.type).toBe('text'); - expect(json.created).toBe(created); - expect(json.content).toBe('Test content'); - expect(json.name).toBe('Test part'); - expect(json.start).toBe(10); - expect(json.end).toBe(20); - expect(json.has_xml_tag).toBe(false); - expect(json.enabled).toBe(true); + assert.strictEqual(json.id, test_id); + assert.strictEqual(json.type, 'text'); + assert.strictEqual(json.created, created); + assert.strictEqual(json.content, 'Test content'); + assert.strictEqual(json.name, 'Test part'); + assert.strictEqual(json.start, 10); + assert.strictEqual(json.end, 20); + assert.ok(!json.has_xml_tag); + assert.ok(json.enabled); }); test('clone creates independent copy with same values', () => { @@ -190,18 +188,18 @@ describe('TextPart serialization', () => { const clone = original.clone(); // Verify they have same initial values except id - expect(clone.id).not.toBe(original.id); - expect(clone.content).toBe(ORIGINAL.CONTENT); - expect(clone.name).toBe(ORIGINAL.NAME); + assert.notStrictEqual(clone.id, original.id); + assert.strictEqual(clone.content, ORIGINAL.CONTENT); + assert.strictEqual(clone.name, ORIGINAL.NAME); // Verify they're independent objects clone.content = MODIFIED.CONTENT; clone.name = MODIFIED.NAME; - expect(original.content).toBe(ORIGINAL.CONTENT); - expect(original.name).toBe(ORIGINAL.NAME); - expect(clone.content).toBe(MODIFIED.CONTENT); - expect(clone.name).toBe(MODIFIED.NAME); + assert.strictEqual(original.content, ORIGINAL.CONTENT); + assert.strictEqual(original.name, ORIGINAL.NAME); + assert.strictEqual(clone.content, MODIFIED.CONTENT); + assert.strictEqual(clone.name, MODIFIED.NAME); }); }); @@ -213,13 +211,13 @@ describe('TextPart content modification', () => { }); // Initial state - expect(part.content).toBe(TEST_CONTENT.INITIAL); + assert.strictEqual(part.content, TEST_CONTENT.INITIAL); // Update content using assignment part.content = TEST_CONTENT.NEW_CONTENT; // Verify content was updated - expect(part.content).toBe(TEST_CONTENT.NEW_CONTENT); + assert.strictEqual(part.content, TEST_CONTENT.NEW_CONTENT); }); test('content setter directly updates content', () => { @@ -229,13 +227,13 @@ describe('TextPart content modification', () => { }); // Initial state - expect(part.content).toBe(TEST_CONTENT.INITIAL); + assert.strictEqual(part.content, TEST_CONTENT.INITIAL); // Update content using setter part.content = TEST_CONTENT.NEW_CONTENT; // Verify content was updated - expect(part.content).toBe(TEST_CONTENT.NEW_CONTENT); + assert.strictEqual(part.content, TEST_CONTENT.NEW_CONTENT); }); }); @@ -246,9 +244,9 @@ describe('TextPart content edge cases', () => { content: TEST_CONTENT.LONG, }); - expect(part.content).toBe(TEST_CONTENT.LONG); - expect(part.length).toBe(TEST_CONTENT.LONG.length); - expect(part.token_count).toBeGreaterThan(0); + assert.strictEqual(part.content, TEST_CONTENT.LONG); + assert.strictEqual(part.length, TEST_CONTENT.LONG.length); + assert.ok(part.token_count! > 0); }); test('handles unicode characters correctly', () => { @@ -257,9 +255,9 @@ describe('TextPart content edge cases', () => { content: TEST_CONTENT.UNICODE, }); - expect(part.content).toBe(TEST_CONTENT.UNICODE); - expect(part.length).toBe(TEST_CONTENT.UNICODE.length); - expect(part.token_count).toEqual(estimate_token_count(TEST_CONTENT.UNICODE)); + assert.strictEqual(part.content, TEST_CONTENT.UNICODE); + assert.strictEqual(part.length, TEST_CONTENT.UNICODE.length); + assert.deepEqual(part.token_count, estimate_token_count(TEST_CONTENT.UNICODE)); }); test('handles special characters correctly', () => { @@ -268,9 +266,9 @@ describe('TextPart content edge cases', () => { content: TEST_CONTENT.SPECIAL_CHARS, }); - expect(part.content).toBe(TEST_CONTENT.SPECIAL_CHARS); - expect(part.length).toBe(TEST_CONTENT.SPECIAL_CHARS.length); - expect(part.token_count).toEqual(estimate_token_count(TEST_CONTENT.SPECIAL_CHARS)); + assert.strictEqual(part.content, TEST_CONTENT.SPECIAL_CHARS); + assert.strictEqual(part.length, TEST_CONTENT.SPECIAL_CHARS.length); + assert.deepEqual(part.token_count, estimate_token_count(TEST_CONTENT.SPECIAL_CHARS)); }); test('handles code and markup content correctly', () => { @@ -279,9 +277,9 @@ describe('TextPart content edge cases', () => { content: TEST_CONTENT.CODE, }); - expect(part.content).toBe(TEST_CONTENT.CODE); - expect(part.length).toBe(TEST_CONTENT.CODE.length); - expect(part.token_count).toEqual(estimate_token_count(TEST_CONTENT.CODE)); + assert.strictEqual(part.content, TEST_CONTENT.CODE); + assert.strictEqual(part.length, TEST_CONTENT.CODE.length); + assert.deepEqual(part.token_count, estimate_token_count(TEST_CONTENT.CODE)); }); }); @@ -294,29 +292,29 @@ describe('TextPart attribute management', () => { // Add attribute part.add_attribute({key: 'class', value: 'highlight'}); - expect(part.attributes).toHaveLength(1); + assert.strictEqual(part.attributes.length, 1); let first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected first attribute'); - expect(first_attr.key).toBe('class'); - expect(first_attr.value).toBe('highlight'); + assert.strictEqual(first_attr.key, 'class'); + assert.strictEqual(first_attr.value, 'highlight'); const attr_id = first_attr.id; // Update attribute const updated = part.update_attribute(attr_id, {value: 'special-highlight'}); - expect(updated).toBe(true); + assert.ok(updated); first_attr = part.attributes[0]; if (!first_attr) throw new Error('Expected attribute after update'); - expect(first_attr.key).toBe('class'); - expect(first_attr.value).toBe('special-highlight'); + assert.strictEqual(first_attr.key, 'class'); + assert.strictEqual(first_attr.value, 'special-highlight'); // Remove attribute part.remove_attribute(attr_id); - expect(part.attributes).toHaveLength(0); + assert.strictEqual(part.attributes.length, 0); // Attempting to update non-existent attribute returns false const fake_update = part.update_attribute(create_uuid(), {key: 'test', value: 'test'}); - expect(fake_update).toBe(false); + assert.ok(!fake_update); }); test('updates attribute key and value together', () => { @@ -329,11 +327,11 @@ describe('TextPart attribute management', () => { // Update both key and value const updated = part.update_attribute(attr_id, {key: 'data-type', value: 'important'}); - expect(updated).toBe(true); + assert.ok(updated); const updated_attr = part.attributes[0]; if (!updated_attr) throw new Error('Expected attribute after update'); - expect(updated_attr.key).toBe('data-type'); - expect(updated_attr.value).toBe('important'); + assert.strictEqual(updated_attr.key, 'data-type'); + assert.strictEqual(updated_attr.value, 'important'); }); test('attributes are preserved when serializing to JSON', () => { @@ -347,22 +345,22 @@ describe('TextPart attribute management', () => { const json = part.to_json(); - expect(json.attributes).toHaveLength(2); + assert.strictEqual(json.attributes.length, 2); const json_attr0 = json.attributes[0]; const json_attr1 = json.attributes[1]; if (!json_attr0 || !json_attr1) throw new Error('Expected both attributes in JSON'); - expect(json_attr0.key).toBe('data-test'); - expect(json_attr1.key).toBe('class'); + assert.strictEqual(json_attr0.key, 'data-test'); + assert.strictEqual(json_attr1.key, 'class'); // Verify they're properly restored const new_part = app.cell_registry.instantiate('TextPart', json); - expect(new_part.attributes).toHaveLength(2); + assert.strictEqual(new_part.attributes.length, 2); const new_attr0 = new_part.attributes[0]; const new_attr1 = new_part.attributes[1]; if (!new_attr0 || !new_attr1) throw new Error('Expected both attributes in restored part'); - expect(new_attr0.key).toBe('data-test'); - expect(new_attr1.key).toBe('class'); + assert.strictEqual(new_attr0.key, 'data-test'); + assert.strictEqual(new_attr1.key, 'class'); }); }); @@ -379,7 +377,7 @@ describe('TextPart instance management', () => { // Verify it's in the registry const retrieved_part = app.parts.items.by_id.get(part.id); - expect(retrieved_part).toBe(part); + assert.strictEqual(retrieved_part, part); }); test('part is removed from registry when requested', () => { @@ -392,13 +390,13 @@ describe('TextPart instance management', () => { app.parts.items.add(part); // Verify it's in the registry - expect(app.parts.items.by_id.get(part.id)).toBe(part); + assert.strictEqual(app.parts.items.by_id.get(part.id), part); // Remove from registry app.parts.items.remove(part.id); // Verify it's gone - expect(app.parts.items.by_id.get(part.id)).toBeUndefined(); + assert.ok(app.parts.items.by_id.get(part.id) === undefined); }); }); @@ -411,8 +409,8 @@ describe('TextPart start and end position markers', () => { end: 25, }); - expect(part.start).toBe(10); - expect(part.end).toBe(25); + assert.strictEqual(part.start, 10); + assert.strictEqual(part.end, 25); }); test('start and end positions can be updated', () => { @@ -422,15 +420,15 @@ describe('TextPart start and end position markers', () => { }); // Initial values are null - expect(part.start).toBeNull(); - expect(part.end).toBeNull(); + assert.isNull(part.start); + assert.isNull(part.end); // Update positions part.start = 5; part.end = 15; - expect(part.start).toBe(5); - expect(part.end).toBe(15); + assert.strictEqual(part.start, 5); + assert.strictEqual(part.end, 15); }); test('positions are preserved when serializing and deserializing', () => { @@ -448,7 +446,7 @@ describe('TextPart start and end position markers', () => { const new_part = app.cell_registry.instantiate('TextPart', json); // Verify positions were preserved - expect(new_part.start).toBe(8); - expect(new_part.end).toBe(30); + assert.strictEqual(new_part.start, 8); + assert.strictEqual(new_part.end, 30); }); }); diff --git a/src/test/poller.svelte.test.ts b/src/test/poller.svelte.test.ts index f2cae906e..1f80eab7d 100644 --- a/src/test/poller.svelte.test.ts +++ b/src/test/poller.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop claude_sonnet_4 - // @vitest-environment jsdom -import {test, expect, describe, vi, beforeEach, afterEach} from 'vitest'; +import {test, describe, vi, beforeEach, afterEach, assert} from 'vitest'; import {Poller} from '$lib/poller.svelte.js'; @@ -20,7 +18,7 @@ describe('Poller', () => { const poll_fn = vi.fn(); const poller = new Poller({poll_fn}); - expect(poller.active).toBe(false); + assert.ok(!poller.active); }); test('should start polling with immediate execution by default', () => { @@ -29,12 +27,12 @@ describe('Poller', () => { poller.start(); - expect(poller.active).toBe(true); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.ok(poller.active); + assert.strictEqual(poll_fn.mock.calls.length, 1); // Advance time to trigger interval vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should start polling without immediate execution when configured', () => { @@ -43,12 +41,12 @@ describe('Poller', () => { poller.start(); - expect(poller.active).toBe(true); - expect(poll_fn).not.toHaveBeenCalled(); + assert.ok(poller.active); + assert.strictEqual(poll_fn.mock.calls.length, 0); // Advance time to trigger interval vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.strictEqual(poll_fn.mock.calls.length, 1); }); test('should use custom interval', () => { @@ -59,15 +57,15 @@ describe('Poller', () => { // Advance by less than interval vi.advanceTimersByTime(4_000); - expect(poll_fn).not.toHaveBeenCalled(); + assert.strictEqual(poll_fn.mock.calls.length, 0); // Advance to interval vi.advanceTimersByTime(1_000); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.strictEqual(poll_fn.mock.calls.length, 1); // Advance by another interval vi.advanceTimersByTime(5_000); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should stop polling', () => { @@ -75,15 +73,15 @@ describe('Poller', () => { const poller = new Poller({poll_fn}); poller.start(); - expect(poller.active).toBe(true); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.ok(poller.active); + assert.strictEqual(poll_fn.mock.calls.length, 1); poller.stop(); - expect(poller.active).toBe(false); + assert.ok(!poller.active); // Should not poll after stopping vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.strictEqual(poll_fn.mock.calls.length, 1); }); test('should handle multiple starts safely', () => { @@ -94,12 +92,12 @@ describe('Poller', () => { poller.start(); poller.start(); - expect(poller.active).toBe(true); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.ok(poller.active); + assert.strictEqual(poll_fn.mock.calls.length, 1); // Should only have one interval running vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should handle multiple stops safely', () => { @@ -111,7 +109,7 @@ describe('Poller', () => { poller.stop(); poller.stop(); - expect(poller.active).toBe(false); + assert.ok(!poller.active); }); test('should handle async poll functions', () => { @@ -120,11 +118,11 @@ describe('Poller', () => { poller.start(); - expect(poll_fn).toHaveBeenCalledTimes(1); - expect(poller.active).toBe(true); + assert.strictEqual(poll_fn.mock.calls.length, 1); + assert.ok(poller.active); vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should handle poll function errors gracefully', () => { @@ -138,16 +136,13 @@ describe('Poller', () => { poller.start(); - expect(poll_fn).toHaveBeenCalledTimes(1); - expect(console_error_spy).toHaveBeenCalledWith( - '[poller] poll function error:', - expect.any(Error), - ); - expect(poller.active).toBe(true); + assert.strictEqual(poll_fn.mock.calls.length, 1); + assert.ok(console_error_spy.mock.calls.length > 0); + assert.ok(poller.active); // Should continue polling despite error vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should handle async poll function errors gracefully', () => { @@ -156,12 +151,12 @@ describe('Poller', () => { poller.start(); - expect(poll_fn).toHaveBeenCalledTimes(1); - expect(poller.active).toBe(true); + assert.strictEqual(poll_fn.mock.calls.length, 1); + assert.ok(poller.active); // Should continue polling despite async error (error handling is async) vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should set interval and restart if active', () => { @@ -169,18 +164,18 @@ describe('Poller', () => { const poller = new Poller({poll_fn, interval: Poller.DEFAULT_INTERVAL, immediate: false}); poller.start(); - expect(poller.active).toBe(true); + assert.ok(poller.active); // Change interval while active poller.set_interval(5_000); - expect(poller.active).toBe(true); + assert.ok(poller.active); // Should use new interval vi.advanceTimersByTime(5_000); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.strictEqual(poll_fn.mock.calls.length, 1); vi.advanceTimersByTime(5_000); - expect(poll_fn).toHaveBeenCalledTimes(2); + assert.strictEqual(poll_fn.mock.calls.length, 2); }); test('should set interval without restarting if inactive', () => { @@ -189,12 +184,12 @@ describe('Poller', () => { // Set interval while inactive poller.set_interval(5_000); - expect(poller.active).toBe(false); + assert.ok(!poller.active); // Start and verify new interval is used poller.start(); vi.advanceTimersByTime(5_000); - expect(poll_fn).toHaveBeenCalledTimes(2); // immediate + first interval + assert.strictEqual(poll_fn.mock.calls.length, 2); // immediate + first interval }); test('should be no-op when setting same interval', () => { @@ -208,8 +203,8 @@ describe('Poller', () => { poller.set_interval(Poller.DEFAULT_INTERVAL); // Should not have restarted - expect(poll_fn.mock.calls.length).toBe(initial_call_count); - expect(poller.active).toBe(true); + assert.strictEqual(poll_fn.mock.calls.length, initial_call_count); + assert.ok(poller.active); }); test('should dispose and stop polling', () => { @@ -217,14 +212,14 @@ describe('Poller', () => { const poller = new Poller({poll_fn}); poller.start(); - expect(poller.active).toBe(true); + assert.ok(poller.active); poller.dispose(); - expect(poller.active).toBe(false); + assert.ok(!poller.active); // Should not poll after disposal vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.strictEqual(poll_fn.mock.calls.length, 1); }); test('should handle restart scenario', () => { @@ -233,16 +228,16 @@ describe('Poller', () => { // Start, stop, start cycle poller.start(); - expect(poller.active).toBe(true); + assert.ok(poller.active); poller.stop(); - expect(poller.active).toBe(false); + assert.ok(!poller.active); poller.start(); - expect(poller.active).toBe(true); + assert.ok(poller.active); // Verify polling works after restart vi.advanceTimersByTime(Poller.DEFAULT_INTERVAL); - expect(poll_fn).toHaveBeenCalledTimes(1); + assert.strictEqual(poll_fn.mock.calls.length, 1); }); }); diff --git a/src/test/popover.svelte.test.ts b/src/test/popover.svelte.test.ts deleted file mode 100644 index 8bae96a58..000000000 --- a/src/test/popover.svelte.test.ts +++ /dev/null @@ -1,1103 +0,0 @@ -// @slop Claude Sonnet 4 - -// @vitest-environment jsdom - -import {describe, test, expect, vi, beforeEach, afterEach} from 'vitest'; - -import {Popover} from '$lib/popover.svelte.js'; -import type {Position} from '$lib/position_helpers.js'; - -// Helper functions for testing -const create_elements = (): { - container: HTMLElement; - trigger: HTMLElement; - content: HTMLElement; - body: HTMLElement; -} => { - const container = document.createElement('div'); - container.classList.add('container'); - - const trigger = document.createElement('button'); - trigger.textContent = 'Trigger Button'; - container.appendChild(trigger); - - const content = document.createElement('div'); - content.textContent = 'Popover Content'; - container.appendChild(content); - - document.body.appendChild(container); - - return {container, trigger, content, body: document.body}; -}; - -const create_mock_event = (type: string, target?: HTMLElement): Event => { - const event = new Event(type, {bubbles: true, cancelable: true}); - if (target) { - Object.defineProperty(event, 'target', {value: target}); - } - return event; -}; - -// Helper for checking style values that handles browser normalization -const check_style = (element: HTMLElement, prop: string, expected: string): void => { - const value = element.style.getPropertyValue(prop); - // Handle empty string vs 'auto' case - if (expected === 'auto' && value === '') { - return; - } - // Handle '0' vs '0px' case - if (expected === '0' && value === '0px') { - return; - } - expect(value).toBe(expected); -}; - -describe('Popover', () => { - // Define shared variables - let elements: ReturnType; - let popover: Popover; - let cleanup_actions: Array<() => void>; - - beforeEach(() => { - elements = create_elements(); - popover = new Popover(); - cleanup_actions = []; - }); - - afterEach(() => { - // Clean up all actions registered during the test - for (const cleanup of cleanup_actions) { - cleanup(); - } - - // Clean up DOM after each test - if (elements.body.contains(elements.container)) { - elements.body.removeChild(elements.container); - } - }); - - // Helper to register attachments for automatic cleanup - const register_attachment = (cleanup: (() => void) | void): void => { - if (cleanup) { - cleanup_actions.push(cleanup); - } - }; - - describe('constructor', () => { - test('creates with default values', () => { - expect(popover.visible).toBe(false); - expect(popover.position).toBe('bottom'); - expect(popover.align).toBe('center'); - expect(popover.offset).toBe('0'); - expect(popover.disable_outside_click).toBe(false); - expect(popover.popover_class).toBe(''); - }); - - test('accepts custom parameters', () => { - const onshow = vi.fn(); - const onhide = vi.fn(); - - popover = new Popover({ - position: 'top', - align: 'start', - offset: '16px', - disable_outside_click: true, - popover_class: 'test-class', - onshow, - onhide, - }); - - expect(popover.position).toBe('top'); - expect(popover.align).toBe('start'); - expect(popover.offset).toBe('16px'); - expect(popover.disable_outside_click).toBe(true); - expect(popover.popover_class).toBe('test-class'); - }); - }); - - describe('visibility methods', () => { - test('show() makes popover visible and calls onshow callback', () => { - const onshow = vi.fn(); - popover = new Popover({onshow}); - - expect(popover.visible).toBe(false); - - popover.show(); - - expect(popover.visible).toBe(true); - expect(onshow).toHaveBeenCalledTimes(1); - - // Showing when already visible should not call onshow again - popover.show(); - expect(onshow).toHaveBeenCalledTimes(1); - }); - - test('hide() hides popover and calls onhide callback', () => { - const onhide = vi.fn(); - popover = new Popover({onhide}); - - // Set visible manually first - popover.visible = true; - - popover.hide(); - - expect(popover.visible).toBe(false); - expect(onhide).toHaveBeenCalledTimes(1); - - // Hiding when already hidden should not call onhide again - popover.hide(); - expect(onhide).toHaveBeenCalledTimes(1); - }); - - test('toggle() toggles visibility state', () => { - const onshow = vi.fn(); - const onhide = vi.fn(); - popover = new Popover({onshow, onhide}); - - // Initially hidden - expect(popover.visible).toBe(false); - - // First toggle should show - popover.toggle(); - expect(popover.visible).toBe(true); - expect(onshow).toHaveBeenCalledTimes(1); - expect(onhide).not.toHaveBeenCalled(); - - // Second toggle should hide - popover.toggle(); - expect(popover.visible).toBe(false); - expect(onshow).toHaveBeenCalledTimes(1); - expect(onhide).toHaveBeenCalledTimes(1); - }); - }); - - describe('update()', () => { - test('changes configuration completely', () => { - popover = new Popover({ - position: 'left', - align: 'end', - popover_class: 'old-class', - }); - - const new_onshow = vi.fn(); - const new_onhide = vi.fn(); - - // Update with new parameters - popover.update({ - position: 'right', - align: 'start', - offset: '20px', - disable_outside_click: true, - popover_class: 'new-class', - onshow: new_onshow, - onhide: new_onhide, - }); - - expect(popover.position).toBe('right'); - expect(popover.align).toBe('start'); - expect(popover.offset).toBe('20px'); - expect(popover.disable_outside_click).toBe(true); - expect(popover.popover_class).toBe('new-class'); - - // Test the new callbacks work - popover.show(); - expect(new_onshow).toHaveBeenCalled(); - - popover.hide(); - expect(new_onhide).toHaveBeenCalled(); - }); - - test('handles partial updates correctly', () => { - popover = new Popover({ - position: 'left', - align: 'end', - offset: '10px', - }); - - // Update only some parameters - popover.update({ - position: 'right', - // Align should remain 'end' - // Offset should remain '10px' - }); - - expect(popover.position).toBe('right'); - expect(popover.align).toBe('end'); - expect(popover.offset).toBe('10px'); - }); - }); - - describe('actions', () => { - describe('trigger attachment', () => { - test('attaches click handler to show/hide popover', () => { - const {trigger} = elements; - - // Set up trigger attachment - register_attachment(popover.trigger()(trigger)); - - // Initial state - expect(popover.visible).toBe(false); - - // Simulate click to show - trigger.click(); - expect(popover.visible).toBe(true); - - // Simulate another click to hide - trigger.click(); - expect(popover.visible).toBe(false); - }); - - test('accepts parameters', () => { - const {trigger} = elements; - - // Set up trigger attachment with params - register_attachment( - popover.trigger({ - position: 'right', - align: 'start', - })(trigger), - ); - - // Check params were applied - expect(popover.position).toBe('right'); - expect(popover.align).toBe('start'); - }); - - test('sets proper aria attributes', () => { - const {trigger, content} = elements; - - // Set up attachments - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Check for aria-expanded on trigger - expect(trigger.getAttribute('aria-expanded')).toBe('false'); - - // Show popover - popover.show(); - - // aria-expanded should update - expect(trigger.getAttribute('aria-expanded')).toBe('true'); - - // Hide popover - popover.hide(); - - // aria-expanded should update back - expect(trigger.getAttribute('aria-expanded')).toBe('false'); - }); - }); - - describe('content attachment', () => { - test('applies position styles and classes', () => { - const {content} = elements; - - // Set up content attachment - register_attachment( - popover.content({ - position: 'bottom', - align: 'start', - offset: '15px', - popover_class: 'test-popover', - })(content), - ); - - // Check styles were applied - expect(content.style.position).toBe('absolute'); - expect(content.style.zIndex).toBe('10'); - expect(content.classList.contains('test-popover')).toBe(true); - - // Initial state - content shouldn't be visible, but we don't check display style - // since we might want to allow animations - expect(popover.visible).toBe(false); - - // Make visible - popover.show(); - expect(popover.visible).toBe(true); - }); - - test('updates styles when parameters change', () => { - const {content} = elements; - - // Set up content attachment - register_attachment( - popover.content({ - position: 'bottom', - align: 'start', - popover_class: 'test-popover', - })(content), - ); - - // Since attachments are reactive, updating the popover instance should trigger re-evaluation - popover.update({ - position: 'right', - align: 'center', - popover_class: 'updated-class', - }); - - // Check class was updated - expect(content.classList.contains('test-popover')).toBe(false); - expect(content.classList.contains('updated-class')).toBe(true); - }); - }); - - describe('container attachment', () => { - test('registers container element for positioning', () => { - const {container, trigger, content} = elements; - - // Set up all attachments - register_attachment(popover.container(container)); - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show popover - popover.show(); - - // Basic check that content is visible - expect(content.style.display).not.toBe('none'); - }); - }); - }); - - describe('positioning', () => { - test.each([ - {position: 'left', align: 'start'}, - {position: 'left', align: 'center'}, - {position: 'left', align: 'end'}, - {position: 'right', align: 'start'}, - {position: 'right', align: 'center'}, - {position: 'right', align: 'end'}, - {position: 'top', align: 'start'}, - {position: 'top', align: 'center'}, - {position: 'top', align: 'end'}, - {position: 'bottom', align: 'start'}, - {position: 'bottom', align: 'center'}, - {position: 'bottom', align: 'end'}, - ] as const)('applies correct styles for %s/%s', ({position, align}) => { - const {content} = elements; - - // Apply position and alignment - register_attachment(popover.content({position, align})(content)); - - // Show popover - popover.show(); - - // Ensure some key styles are set based on position and alignment - if (position === 'left' || position === 'right') { - if (align === 'center') { - expect(content.style.transform).toMatch(/translateY/); - } else { - // For start/end alignment, one of top/bottom should be set - const has_position = content.style.top || content.style.bottom; - expect(has_position).toBeTruthy(); - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } else if (position === 'top' || position === 'bottom') { - if (align === 'center') { - expect(content.style.transform).toMatch(/translateX/); - } else { - // For start/end alignment, one of left/right should be set - const has_position = content.style.left || content.style.right; - expect(has_position).toBeTruthy(); - } - } - }); - - describe('detailed positioning', () => { - test('verifies left position styles with various alignments', () => { - const {content} = elements; - - // Test left + start - register_attachment(popover.content({position: 'left', align: 'start'})(content)); - check_style(content, 'right', '100%'); - check_style(content, 'left', 'auto'); - check_style(content, 'top', '0'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'right'); - cleanup_actions.pop()?.(); - - // Test left + center - register_attachment(popover.content({position: 'left', align: 'center'})(content)); - check_style(content, 'right', '100%'); - check_style(content, 'left', 'auto'); - check_style(content, 'top', '50%'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'transform', 'translateY(-50%)'); - check_style(content, 'transform-origin', 'right'); - cleanup_actions.pop()?.(); - - // Test left + end - register_attachment(popover.content({position: 'left', align: 'end'})(content)); - check_style(content, 'right', '100%'); - check_style(content, 'left', 'auto'); - check_style(content, 'top', 'auto'); - check_style(content, 'bottom', '0'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'right'); - cleanup_actions.pop()?.(); - - // Test left with offset - register_attachment( - popover.content({position: 'left', align: 'start', offset: '10px'})(content), - ); - check_style(content, 'right', 'calc(100% + 10px)'); - }); - - test('verifies right position styles with various alignments', () => { - const {content} = elements; - - // Test right + start - register_attachment(popover.content({position: 'right', align: 'start'})(content)); - check_style(content, 'left', '100%'); - check_style(content, 'right', 'auto'); - check_style(content, 'top', '0'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'left'); - cleanup_actions.pop()?.(); - - // Test right + center - register_attachment(popover.content({position: 'right', align: 'center'})(content)); - check_style(content, 'left', '100%'); - check_style(content, 'right', 'auto'); - check_style(content, 'top', '50%'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'transform', 'translateY(-50%)'); - check_style(content, 'transform-origin', 'left'); - cleanup_actions.pop()?.(); - - // Test right + end - register_attachment(popover.content({position: 'right', align: 'end'})(content)); - check_style(content, 'left', '100%'); - check_style(content, 'right', 'auto'); - check_style(content, 'top', 'auto'); - check_style(content, 'bottom', '0'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'left'); - cleanup_actions.pop()?.(); - - // Test right with offset - register_attachment( - popover.content({position: 'right', align: 'start', offset: '10px'})(content), - ); - check_style(content, 'left', 'calc(100% + 10px)'); - }); - - test('verifies top position styles with various alignments', () => { - const {content} = elements; - - // Test top + start - register_attachment(popover.content({position: 'top', align: 'start'})(content)); - check_style(content, 'bottom', '100%'); - check_style(content, 'top', 'auto'); - check_style(content, 'left', '0'); - check_style(content, 'right', 'auto'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'bottom'); - cleanup_actions.pop()?.(); - - // Test top + center - register_attachment(popover.content({position: 'top', align: 'center'})(content)); - check_style(content, 'bottom', '100%'); - check_style(content, 'top', 'auto'); - check_style(content, 'left', '50%'); - check_style(content, 'right', 'auto'); - check_style(content, 'transform', 'translateX(-50%)'); - check_style(content, 'transform-origin', 'bottom'); - cleanup_actions.pop()?.(); - - // Test top + end - register_attachment(popover.content({position: 'top', align: 'end'})(content)); - check_style(content, 'bottom', '100%'); - check_style(content, 'top', 'auto'); - check_style(content, 'left', 'auto'); - check_style(content, 'right', '0'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'bottom'); - cleanup_actions.pop()?.(); - - // Test top with offset - register_attachment( - popover.content({position: 'top', align: 'start', offset: '10px'})(content), - ); - check_style(content, 'bottom', 'calc(100% + 10px)'); - }); - - test('verifies bottom position styles with various alignments', () => { - const {content} = elements; - - // Test bottom + start - register_attachment(popover.content({position: 'bottom', align: 'start'})(content)); - check_style(content, 'top', '100%'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'left', '0'); - check_style(content, 'right', 'auto'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'top'); - cleanup_actions.pop()?.(); - - // Test bottom + center - register_attachment(popover.content({position: 'bottom', align: 'center'})(content)); - check_style(content, 'top', '100%'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'left', '50%'); - check_style(content, 'right', 'auto'); - check_style(content, 'transform', 'translateX(-50%)'); - check_style(content, 'transform-origin', 'top'); - cleanup_actions.pop()?.(); - - // Test bottom + end - register_attachment(popover.content({position: 'bottom', align: 'end'})(content)); - check_style(content, 'top', '100%'); - check_style(content, 'bottom', 'auto'); - check_style(content, 'left', 'auto'); - check_style(content, 'right', '0'); - check_style(content, 'transform', ''); - check_style(content, 'transform-origin', 'top'); - cleanup_actions.pop()?.(); - - // Test bottom with offset - register_attachment( - popover.content({position: 'bottom', align: 'start', offset: '10px'})(content), - ); - check_style(content, 'top', 'calc(100% + 10px)'); - }); - - test('verifies center position styles', () => { - const {content} = elements; - - register_attachment(popover.content({position: 'center'})(content)); - check_style(content, 'top', '50%'); - check_style(content, 'left', '50%'); - check_style(content, 'transform', 'translate(-50%, -50%)'); - check_style(content, 'transform-origin', 'center'); - cleanup_actions.pop()?.(); - - // Center ignores alignment and offset - register_attachment( - popover.content({position: 'center', align: 'start', offset: '10px'})(content), - ); - check_style(content, 'top', '50%'); - check_style(content, 'left', '50%'); - check_style(content, 'transform', 'translate(-50%, -50%)'); - }); - - test('verifies overlay position styles', () => { - const {content} = elements; - - register_attachment(popover.content({position: 'overlay'})(content)); - check_style(content, 'top', '0'); - check_style(content, 'left', '0'); - check_style(content, 'width', '100%'); - check_style(content, 'height', '100%'); - check_style(content, 'transform-origin', 'center'); - cleanup_actions.pop()?.(); - - // Overlay ignores alignment and offset - register_attachment( - popover.content({position: 'overlay', align: 'end', offset: '10px'})(content), - ); - check_style(content, 'top', '0'); - check_style(content, 'left', '0'); - check_style(content, 'width', '100%'); - check_style(content, 'height', '100%'); - }); - - test('updating position and offset dynamically updates styles', () => { - const {content} = elements; - popover = new Popover({ - position: 'bottom', - align: 'center', - offset: '0', - }); - - // Initial setup - register_attachment(popover.content()(content)); - check_style(content, 'top', '100%'); - check_style(content, 'left', '50%'); - - // With attachments being reactive, we need to recreate them to pick up new params - cleanup_actions.pop()?.(); - - // Update position - popover.update({position: 'right'}); - register_attachment(popover.content()(content)); - check_style(content, 'left', '100%'); - check_style(content, 'top', '50%'); - cleanup_actions.pop()?.(); - - // Update alignment - popover.update({align: 'start'}); - register_attachment(popover.content()(content)); - check_style(content, 'top', '0'); - cleanup_actions.pop()?.(); - - // Update offset - popover.update({offset: '15px'}); - register_attachment(popover.content()(content)); - check_style(content, 'left', 'calc(100% + 15px)'); - cleanup_actions.pop()?.(); - - // Multiple updates at once - popover.update({position: 'top', align: 'end', offset: '5px'}); - register_attachment(popover.content()(content)); - check_style(content, 'bottom', 'calc(100% + 5px)'); - check_style(content, 'right', '0'); - check_style(content, 'top', 'auto'); - check_style(content, 'left', 'auto'); - }); - - test('z-index is always applied', () => { - const {content} = elements; - - // Test each position type to ensure z-index is always applied - const positions: Array = ['left', 'right', 'top', 'bottom', 'center', 'overlay']; - - for (const position of positions) { - register_attachment(popover.content({position})(content)); - expect(content.style.zIndex).toBe('10'); - cleanup_actions.pop()?.(); - } - }); - - test('transform-origin is set correctly for each position', () => { - const {content} = elements; - - const position_origins = [ - {position: 'left', expected: 'right'}, - {position: 'right', expected: 'left'}, - {position: 'top', expected: 'bottom'}, - {position: 'bottom', expected: 'top'}, - {position: 'center', expected: 'center'}, - {position: 'overlay', expected: 'center'}, - ]; - - for (const {position, expected} of position_origins) { - register_attachment(popover.content({position: position as Position})(content)); - expect(content.style.getPropertyValue('transform-origin')).toBe(expected); - cleanup_actions.pop()?.(); - } - }); - }); - }); - - describe('interaction', () => { - test('clicking outside hides popover when disable_outside_click is false', () => { - const {trigger, content, body} = elements; - const onhide = vi.fn(); - popover = new Popover({onhide}); - - // Set up attachments - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show the popover - popover.show(); - expect(popover.visible).toBe(true); - - // Simulate click outside - const click_event = create_mock_event('click', body); - document.dispatchEvent(click_event); - - // Popover should be hidden - expect(popover.visible).toBe(false); - expect(onhide).toHaveBeenCalled(); - }); - - test('clicking outside does not hide when disable_outside_click is true', () => { - const {trigger, content, body} = elements; - const onhide = vi.fn(); - popover = new Popover({ - disable_outside_click: true, - onhide, - }); - - // Set up attachments - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show the popover - popover.show(); - expect(popover.visible).toBe(true); - - // Simulate click outside - const click_event = create_mock_event('click', body); - document.dispatchEvent(click_event); - - // Popover should still be visible - expect(popover.visible).toBe(true); - expect(onhide).not.toHaveBeenCalled(); - }); - - test('clicking on trigger or content does not hide popover', () => { - const {trigger, content} = elements; - const onhide = vi.fn(); - popover = new Popover({onhide}); - - // Set up attachments - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show the popover - popover.show(); - expect(popover.visible).toBe(true); - - // Simulate click on content (this is intercepted earlier in the event chain) - const content_click = create_mock_event('click', content); - document.dispatchEvent(content_click); - - // Should still be visible - expect(popover.visible).toBe(true); - expect(onhide).not.toHaveBeenCalled(); - - // Simulate click on trigger - const trigger_click = create_mock_event('click', trigger); - document.dispatchEvent(trigger_click); - - // Should still be visible (actual trigger handling is tested separately) - expect(popover.visible).toBe(true); - expect(onhide).not.toHaveBeenCalled(); - }); - }); - - describe('edge cases', () => { - test('nested elements within trigger or content', () => { - const {trigger, content} = elements; - - // Create nested elements - const inner_trigger = document.createElement('span'); - trigger.appendChild(inner_trigger); - - const inner_content = document.createElement('span'); - content.appendChild(inner_content); - - // Set up attachments - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show popover - popover.show(); - - // Now we'll test if a click on the inner trigger toggles the popover - // First note current visibility - expect(popover.visible).toBe(true); - - // Click the trigger element directly - trigger.click(); - - // The popover should toggle to hidden - expect(popover.visible).toBe(false); - - // Show it again for the next test - trigger.click(); - expect(popover.visible).toBe(true); - - // Now test if outside clicks work - click on document body (not content or trigger) - const body_click = create_mock_event('click', document.body); - document.dispatchEvent(body_click); - - // This should close the popover - expect(popover.visible).toBe(false); - }); - - test('changing disable_outside_click dynamically', () => { - const {trigger, content, body} = elements; - - // Set up attachments - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show popover - - popover.show(); - - // Initially outside clicks should hide - const outside_click1 = create_mock_event('click', body); - document.dispatchEvent(outside_click1); - expect(popover.visible).toBe(false); - - // Update to disable outside clicks - popover.update({disable_outside_click: true}); - - // Show again - popover.show(); - - // Now outside clicks should not hide - const outside_click2 = create_mock_event('click', body); - document.dispatchEvent(outside_click2); - expect(popover.visible).toBe(true); - - // Change back to allowing outside clicks - popover.update({disable_outside_click: false}); - - // Outside clicks should hide again - const outside_click3 = create_mock_event('click', body); - document.dispatchEvent(outside_click3); - expect(popover.visible).toBe(false); - }); - - test('changing class dynamically updates DOM', () => { - const {content} = elements; - popover = new Popover({ - popover_class: 'initial-class', - }); - - // Set up content attachment - register_attachment(popover.content()(content)); - - // Initial class should be applied - expect(content.classList.contains('initial-class')).toBe(true); - - // Update class - popover.update({popover_class: 'updated-class'}); - - // Old class should be removed, new class added - expect(content.classList.contains('initial-class')).toBe(false); - expect(content.classList.contains('updated-class')).toBe(true); - }); - - test('cleanup removes event listeners', () => { - const {trigger, content} = elements; - - // Set up attachments - const trigger_cleanup = popover.trigger()(trigger); - const content_cleanup = popover.content()(content); - - // Show popover to set up document click listener - popover.show(); - - // Clean up attachments - trigger_cleanup?.(); - content_cleanup?.(); - - // After cleanup, clicking trigger should do nothing - trigger.click(); - expect(popover.visible).toBe(true); // Still true because cleanup removed click handler - }); - - test('multiple popovers work independently', () => { - // Create two sets of elements - const elements1 = create_elements(); - const elements2 = create_elements(); - document.body.appendChild(elements2.container); - - const popover1 = new Popover(); - const popover2 = new Popover(); - - // Set up attachments for both popovers - register_attachment(popover1.trigger()(elements1.trigger)); - register_attachment(popover1.content()(elements1.content)); - - register_attachment(popover2.trigger()(elements2.trigger)); - register_attachment(popover2.content()(elements2.content)); - - // Show first popover - elements1.trigger.click(); - expect(popover1.visible).toBe(true); - expect(popover2.visible).toBe(false); - - // Show second popover - elements2.trigger.click(); - expect(popover1.visible).toBe(true); - expect(popover2.visible).toBe(true); - - // Hide first popover - elements1.trigger.click(); - expect(popover1.visible).toBe(false); - expect(popover2.visible).toBe(true); - - // Clean up - document.body.removeChild(elements2.container); - }); - }); - - describe('real-world scenarios and robustness', () => { - test('popover survives DOM manipulation', () => { - const {trigger, content} = elements; - const onhide = vi.fn(); - popover = new Popover({onhide}); - - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Show popover - trigger.click(); - expect(popover.visible).toBe(true); - - // Add/remove siblings (common in dynamic UIs) - const sibling = document.createElement('div'); - elements.container.appendChild(sibling); - elements.container.removeChild(sibling); - - // Popover should still be functional - expect(popover.visible).toBe(true); - trigger.click(); - expect(popover.visible).toBe(false); - expect(onhide).toHaveBeenCalled(); - }); - - test('handles rapid show/hide cycles', () => { - const {trigger} = elements; - const onshow = vi.fn(); - const onhide = vi.fn(); - popover = new Popover({onshow, onhide}); - - register_attachment(popover.trigger()(trigger)); - - // Rapid clicking should be handled gracefully - for (let i = 0; i < 10; i++) { - trigger.click(); - } - - // Should end up hidden (started hidden, 10 clicks = even number) - expect(popover.visible).toBe(false); - expect(onshow).toHaveBeenCalledTimes(5); - expect(onhide).toHaveBeenCalledTimes(5); - }); - - test('preserves state during attachment recreation', () => { - const {trigger} = elements; - popover = new Popover({position: 'top', align: 'start'}); - - // Set up initial attachment - let cleanup = popover.trigger()(trigger); - register_attachment(cleanup); - - // Show popover - trigger.click(); - expect(popover.visible).toBe(true); - - // Recreate attachment (simulates reactive updates) - cleanup?.(); - cleanup = popover.trigger()(trigger); - register_attachment(cleanup); - - // State should be preserved - expect(popover.visible).toBe(true); - expect(popover.position).toBe('top'); - expect(popover.align).toBe('start'); - }); - - test('handles element removal gracefully', () => { - const {trigger} = elements; - - // Should not throw when elements are missing - expect(() => { - popover.show(); - popover.hide(); - popover.toggle(); - }).not.toThrow(); - - // Should handle element removal during operation - register_attachment(popover.trigger()(trigger)); - popover.show(); - - // Should not crash when trigger is removed - trigger.remove(); - expect(() => popover.hide()).not.toThrow(); - }); - - test('ARIA attributes maintained correctly', () => { - const {trigger, content} = elements; - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Initial ARIA state - expect(trigger.getAttribute('aria-expanded')).toBe('false'); - expect(content.getAttribute('role')).toBe('dialog'); - - // Show and verify ARIA - popover.show(); - expect(trigger.getAttribute('aria-expanded')).toBe('true'); - expect(trigger.getAttribute('aria-controls')).toBeTruthy(); - expect(content.id).toBeTruthy(); - expect(trigger.getAttribute('aria-controls')).toBe(content.id); - - // Hide and verify ARIA persists - popover.hide(); - expect(trigger.getAttribute('aria-expanded')).toBe('false'); - expect(trigger.getAttribute('aria-controls')).toBe(content.id); - }); - - test('callbacks fire in correct order', () => { - const events: Array = []; - const onshow = () => events.push('show'); - const onhide = () => events.push('hide'); - popover = new Popover({onshow, onhide}); - - // Test multiple show/hide cycles - popover.show(); - popover.hide(); - popover.show(); - popover.hide(); - - expect(events).toEqual(['show', 'hide', 'show', 'hide']); - }); - - test('nested element click detection', () => { - const {trigger, content} = elements; - register_attachment(popover.trigger()(trigger)); - register_attachment(popover.content()(content)); - - // Create deeply nested structure - const level1 = document.createElement('div'); - const level2 = document.createElement('span'); - const level3 = document.createElement('em'); - level1.appendChild(level2); - level2.appendChild(level3); - content.appendChild(level1); - - popover.show(); - expect(popover.visible).toBe(true); - - // Click on deeply nested element should not close popover - const nested_click = create_mock_event('click', level3); - document.dispatchEvent(nested_click); - expect(popover.visible).toBe(true); - - // Click outside should close popover - const outside_click = create_mock_event('click', document.body); - document.dispatchEvent(outside_click); - expect(popover.visible).toBe(false); - }); - - test('multiple popovers independence', () => { - const popovers: Array = []; - const triggers: Array = []; - - // Create multiple popovers - for (let i = 0; i < 3; i++) { - const popover_instance = new Popover(); - const trigger = document.createElement('button'); - trigger.textContent = `Trigger ${i}`; - document.body.appendChild(trigger); - - register_attachment(popover_instance.trigger()(trigger)); - popovers.push(popover_instance); - triggers.push(trigger); - } - - // Show all popovers - for (const popover_instance of popovers) { - popover_instance.show(); - expect(popover_instance.visible).toBe(true); - } - - // Hide them one by one and verify others remain visible - for (let i = 0; i < popovers.length; i++) { - popovers[i]!.hide(); - expect(popovers[i]!.visible).toBe(false); - - // Check remaining popovers are still visible - for (let j = i + 1; j < popovers.length; j++) { - expect(popovers[j]!.visible).toBe(true); - } - } - - // Clean up - for (const trigger of triggers) { - trigger.remove(); - } - }); - }); -}); diff --git a/src/test/position_helpers.test.ts b/src/test/position_helpers.test.ts deleted file mode 100644 index 69a90caba..000000000 --- a/src/test/position_helpers.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -// @slop Claude Sonnet 3.7 - -import {test, expect} from 'vitest'; - -import {generate_position_styles, type Position, type Alignment} from '$lib/position_helpers.js'; - -// Helper to check common properties that should be on all position styles -const check_common_styles = (styles: Record) => { - expect(styles.position).toBe('absolute'); - expect(styles['z-index']).toBe('10'); -}; - -// Helper to check expected style value, considering browser normalization -const check_style_value = (styles: Record, prop: string, expected: string) => { - if ( - (expected === 'auto' && styles[prop] === '') || - (expected === '0' && styles[prop] === '0px') - ) { - return true; - } - expect(styles[prop]).toBe(expected); - return true; // Added return statement to fix type error -}; - -test('generate_position_styles - left position with different alignments', () => { - // Left + Start - let styles = generate_position_styles('left', 'start'); - check_common_styles(styles); - check_style_value(styles, 'right', '100%'); - check_style_value(styles, 'left', 'auto'); - check_style_value(styles, 'top', '0'); - check_style_value(styles, 'bottom', 'auto'); - expect(styles['transform-origin']).toBe('right'); - expect(styles.transform || '').not.toContain('translate'); - - // Left + Center - styles = generate_position_styles('left', 'center'); - check_common_styles(styles); - check_style_value(styles, 'right', '100%'); - check_style_value(styles, 'left', 'auto'); - check_style_value(styles, 'top', '50%'); - expect(styles.transform).toBe('translateY(-50%)'); - - // Left + End - styles = generate_position_styles('left', 'end'); - check_common_styles(styles); - check_style_value(styles, 'right', '100%'); - check_style_value(styles, 'left', 'auto'); - check_style_value(styles, 'bottom', '0'); - check_style_value(styles, 'top', 'auto'); -}); - -test('generate_position_styles - right position with different alignments', () => { - // Right + Start - let styles = generate_position_styles('right', 'start'); - check_common_styles(styles); - check_style_value(styles, 'left', '100%'); - check_style_value(styles, 'right', 'auto'); - check_style_value(styles, 'top', '0'); - check_style_value(styles, 'bottom', 'auto'); - expect(styles['transform-origin']).toBe('left'); - - // Right + Center - styles = generate_position_styles('right', 'center'); - check_common_styles(styles); - check_style_value(styles, 'left', '100%'); - check_style_value(styles, 'right', 'auto'); - check_style_value(styles, 'top', '50%'); - expect(styles.transform).toBe('translateY(-50%)'); - - // Right + End - styles = generate_position_styles('right', 'end'); - check_common_styles(styles); - check_style_value(styles, 'left', '100%'); - check_style_value(styles, 'right', 'auto'); - check_style_value(styles, 'bottom', '0'); - check_style_value(styles, 'top', 'auto'); -}); - -test('generate_position_styles - top position with different alignments', () => { - // Top + Start - let styles = generate_position_styles('top', 'start'); - check_common_styles(styles); - check_style_value(styles, 'bottom', '100%'); - check_style_value(styles, 'top', 'auto'); - check_style_value(styles, 'left', '0'); - check_style_value(styles, 'right', 'auto'); - expect(styles['transform-origin']).toBe('bottom'); - - // Top + Center - styles = generate_position_styles('top', 'center'); - check_common_styles(styles); - check_style_value(styles, 'bottom', '100%'); - check_style_value(styles, 'top', 'auto'); - check_style_value(styles, 'left', '50%'); - expect(styles.transform).toBe('translateX(-50%)'); - - // Top + End - styles = generate_position_styles('top', 'end'); - check_common_styles(styles); - check_style_value(styles, 'bottom', '100%'); - check_style_value(styles, 'top', 'auto'); - check_style_value(styles, 'left', 'auto'); - check_style_value(styles, 'right', '0'); -}); - -test('generate_position_styles - bottom position with different alignments', () => { - // Bottom + Start - let styles = generate_position_styles('bottom', 'start'); - check_common_styles(styles); - check_style_value(styles, 'top', '100%'); - check_style_value(styles, 'bottom', 'auto'); - check_style_value(styles, 'left', '0'); - check_style_value(styles, 'right', 'auto'); - expect(styles['transform-origin']).toBe('top'); - - // Bottom + Center - styles = generate_position_styles('bottom', 'center'); - check_common_styles(styles); - check_style_value(styles, 'top', '100%'); - check_style_value(styles, 'bottom', 'auto'); - check_style_value(styles, 'left', '50%'); - expect(styles.transform).toBe('translateX(-50%)'); - - // Bottom + End - styles = generate_position_styles('bottom', 'end'); - check_common_styles(styles); - check_style_value(styles, 'top', '100%'); - check_style_value(styles, 'bottom', 'auto'); - check_style_value(styles, 'left', 'auto'); - check_style_value(styles, 'right', '0'); -}); - -test('generate_position_styles - with offsets', () => { - // Test left with offset - let styles = generate_position_styles('left', 'start', '10px'); - expect(styles.right).toBe('calc(100% + 10px)'); - - // Test right with offset - styles = generate_position_styles('right', 'start', '10px'); - expect(styles.left).toBe('calc(100% + 10px)'); - - // Test top with offset - styles = generate_position_styles('top', 'start', '10px'); - expect(styles.bottom).toBe('calc(100% + 10px)'); - - // Test bottom with offset - styles = generate_position_styles('bottom', 'start', '10px'); - expect(styles.top).toBe('calc(100% + 10px)'); - - // Test with different offset values - styles = generate_position_styles('left', 'start', '5rem'); - expect(styles.right).toBe('calc(100% + 5rem)'); - - // Test with negative offset - styles = generate_position_styles('left', 'start', '-8px'); - expect(styles.right).toBe('calc(100% + -8px)'); -}); - -test('generate_position_styles - center position', () => { - const styles = generate_position_styles('center'); - check_common_styles(styles); - check_style_value(styles, 'top', '50%'); - check_style_value(styles, 'left', '50%'); - expect(styles.transform).toBe('translate(-50%, -50%)'); - expect(styles['transform-origin']).toBe('center'); - - // Center ignores alignment and offset - const styles_with_params = generate_position_styles('center', 'start', '10px'); - check_style_value(styles_with_params, 'top', '50%'); - check_style_value(styles_with_params, 'left', '50%'); - expect(styles_with_params.transform).toBe('translate(-50%, -50%)'); -}); - -test('generate_position_styles - overlay position', () => { - const styles = generate_position_styles('overlay'); - check_common_styles(styles); - check_style_value(styles, 'top', '0'); - check_style_value(styles, 'left', '0'); - expect(styles.width).toBe('100%'); - expect(styles.height).toBe('100%'); - expect(styles['transform-origin']).toBe('center'); - - // Overlay ignores alignment and offset - const styles_with_params = generate_position_styles('overlay', 'start', '10px'); - check_style_value(styles_with_params, 'top', '0'); - check_style_value(styles_with_params, 'left', '0'); - expect(styles_with_params.width).toBe('100%'); - expect(styles_with_params.height).toBe('100%'); -}); - -test('generate_position_styles - default parameters', () => { - // No parameters (uses defaults) - const styles = generate_position_styles(); - check_common_styles(styles); - check_style_value(styles, 'top', '50%'); - check_style_value(styles, 'left', '50%'); - expect(styles.transform).toBe('translate(-50%, -50%)'); - expect(styles['transform-origin']).toBe('center'); -}); - -test('generate_position_styles - throws on invalid position', () => { - // @ts-expect-error - Testing invalid position - expect(() => generate_position_styles('invalid')).toThrow(); -}); - -// Test all possible combinations systematically -test('generate_position_styles - all position/alignment combinations work', () => { - const positions: Array = ['left', 'right', 'top', 'bottom', 'center', 'overlay']; - const alignments: Array = ['start', 'center', 'end']; - const offsets = ['0', '10px']; - - for (const position of positions) { - for (const align of alignments) { - for (const offset of offsets) { - expect(() => { - const styles = generate_position_styles(position, align, offset); - // Basic validation that we got a style object back - expect(typeof styles).toBe('object'); - expect(styles.position).toBe('absolute'); - }).not.toThrow(); - } - } - } -}); diff --git a/src/test/prompt_helpers.test.ts b/src/test/prompt_helpers.test.ts index 9cec68d2c..fed23c8c6 100644 --- a/src/test/prompt_helpers.test.ts +++ b/src/test/prompt_helpers.test.ts @@ -1,6 +1,4 @@ -// @slop Claude Opus 4 - -import {test, expect} from 'vitest'; +import {test, assert} from 'vitest'; import {format_prompt_content} from '$lib/prompt_helpers.js'; @@ -40,7 +38,7 @@ const create_part = (partial: Partial = {}): SimplePart => { // Basic tests test('format_prompt_content - returns empty string for empty parts array', () => { const result = format_prompt_content([] as any); - expect(result).toBe(''); + assert.strictEqual(result, ''); }); test('format_prompt_content - filters out disabled parts', () => { @@ -50,14 +48,14 @@ test('format_prompt_content - filters out disabled parts', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('Content 2'); + assert.strictEqual(result, 'Content 2'); }); test('format_prompt_content - joins multiple enabled parts with double newlines', () => { const parts = [create_part({content: 'Content 1'}), create_part({content: 'Content 2'})]; const result = format_prompt_content(parts as any); - expect(result).toBe('Content 1\n\nContent 2'); + assert.strictEqual(result, 'Content 1\n\nContent 2'); }); // XML tag tests @@ -71,7 +69,7 @@ test('format_prompt_content - wraps content with XML tags when specified', () => ]; const result = format_prompt_content(parts as any); - expect(result).toBe('\nContent with tag\n'); + assert.strictEqual(result, '\nContent with tag\n'); }); test('format_prompt_content - uses xml_tag_name_default when no XML tag name is provided', () => { @@ -85,7 +83,7 @@ test('format_prompt_content - uses xml_tag_name_default when no XML tag name is ]; const result = format_prompt_content(parts as any); - expect(result).toBe('\nContent with default tag\n'); + assert.strictEqual(result, '\nContent with default tag\n'); }); // Test with different part types @@ -106,7 +104,10 @@ test('format_prompt_content - uses different part types as defaults', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('\nFile content\n\n\n\nSequence content\n'); + assert.strictEqual( + result, + '\nFile content\n\n\n\nSequence content\n', + ); }); test('format_prompt_content - uses different default XML tag names for different part types', () => { @@ -132,7 +133,8 @@ test('format_prompt_content - uses different default XML tag names for different ]; const result = format_prompt_content(parts as any); - expect(result).toBe( + assert.strictEqual( + result, '\nFile content\n\n\n\nText content\n\n\n\nSequence content\n', ); }); @@ -149,7 +151,7 @@ test('format_prompt_content - includes attributes with key and value', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('
\nContent with attributes\n
'); + assert.strictEqual(result, '
\nContent with attributes\n
'); }); test('format_prompt_content - handles empty values as boolean attributes', () => { @@ -163,7 +165,7 @@ test('format_prompt_content - handles empty values as boolean attributes', () => ]; const result = format_prompt_content(parts as any); - expect(result).toBe('\nContent with boolean attribute\n'); + assert.strictEqual(result, '\nContent with boolean attribute\n'); }); test('format_prompt_content - handles explicitly empty string values', () => { @@ -180,7 +182,8 @@ test('format_prompt_content - handles explicitly empty string values', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe( + assert.strictEqual( + result, '
\nContent with explicit empty value\n
', ); }); @@ -196,7 +199,7 @@ test('format_prompt_content - filters out attributes without keys', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('
\nContent with missing key\n
'); + assert.strictEqual(result, '
\nContent with missing key\n
'); }); test('format_prompt_content - handles multiple attributes with mix of empty and non-empty values', () => { @@ -216,7 +219,8 @@ test('format_prompt_content - handles multiple attributes with mix of empty and ]; const result = format_prompt_content(parts as any); - expect(result).toBe( + assert.strictEqual( + result, '', ); }); @@ -236,7 +240,7 @@ test('format_prompt_content - ignores attributes with empty keys after trimming' ]; const result = format_prompt_content(parts as any); - expect(result).toBe('
\nContent with whitespace key\n
'); + assert.strictEqual(result, '
\nContent with whitespace key\n
'); }); test('format_prompt_content - trims attribute keys before rendering', () => { @@ -253,7 +257,8 @@ test('format_prompt_content - trims attribute keys before rendering', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe( + assert.strictEqual( + result, '
\nContent with trimmed keys\n
', ); }); @@ -274,7 +279,7 @@ test('format_prompt_content - removes attributes with empty keys but preserves o ]; const result = format_prompt_content(parts as any); - expect(result).toBe('
\nMixed attributes\n
'); + assert.strictEqual(result, '
\nMixed attributes\n
'); }); test('format_prompt_content - filters out attributes with empty keys', () => { @@ -288,7 +293,7 @@ test('format_prompt_content - filters out attributes with empty keys', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('
\nContent with empty key\n
'); + assert.strictEqual(result, '
\nContent with empty key\n
'); }); // Edge cases @@ -296,7 +301,7 @@ test('format_prompt_content - trims whitespace from content', () => { const parts = [create_part({content: ' Content with whitespace '})]; const result = format_prompt_content(parts as any); - expect(result).toBe('Content with whitespace'); + assert.strictEqual(result, 'Content with whitespace'); }); test('format_prompt_content - skips parts with empty content', () => { @@ -307,7 +312,7 @@ test('format_prompt_content - skips parts with empty content', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('Real content'); + assert.strictEqual(result, 'Real content'); }); test('format_prompt_content - trims whitespace from XML tag name', () => { @@ -320,7 +325,7 @@ test('format_prompt_content - trims whitespace from XML tag name', () => { ]; const result = format_prompt_content(parts as any); - expect(result).toBe('\nTrimmed tag name\n'); + assert.strictEqual(result, '\nTrimmed tag name\n'); }); // Test that diskfile parts get the path attribute by default @@ -336,7 +341,7 @@ test('format_prompt_content - ensures diskfile parts have path attribute', () => }); const result = format_prompt_content([diskfile_part] as any); - expect(result).toBe('\nFile content with path\n'); + assert.strictEqual(result, '\nFile content with path\n'); }); // Test for when the path attribute is combined with other attributes @@ -355,7 +360,8 @@ test('format_prompt_content - combines path attribute with other attributes for }); const result = format_prompt_content([diskfile_part] as any); - expect(result).toBe( + assert.strictEqual( + result, '\nFile with multiple attributes\n', ); }); diff --git a/src/test/reorderable.svelte.test.ts b/src/test/reorderable.svelte.test.ts index ecb2ec015..781c0e863 100644 --- a/src/test/reorderable.svelte.test.ts +++ b/src/test/reorderable.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, vi, describe, beforeEach, afterEach} from 'vitest'; +import {test, vi, describe, beforeEach, afterEach, assert} from 'vitest'; import { Reorderable, @@ -104,21 +102,21 @@ describe('Reorderable', () => { test('creates with default values', () => { const reorderable = new Reorderable(); - expect(reorderable).toBeInstanceOf(Reorderable); - expect(reorderable.list_node).toBeNull(); - expect(reorderable.list_params).toBeNull(); - expect(reorderable.indices.size).toBe(0); - expect(reorderable.elements.size).toBe(0); - expect(reorderable.direction).toBe('vertical'); - expect(reorderable.id).toBeTruthy(); - expect(reorderable.id).not.toBe(new Reorderable().id); - expect(reorderable.list_class).toBe('reorderable_list'); - expect(reorderable.item_class).toBe('reorderable_item'); + assert.instanceOf(reorderable, Reorderable); + assert.isNull(reorderable.list_node); + assert.isNull(reorderable.list_params); + assert.strictEqual(reorderable.indices.size, 0); + assert.strictEqual(reorderable.elements.size, 0); + assert.strictEqual(reorderable.direction, 'vertical'); + assert.ok(reorderable.id); + assert.notStrictEqual(reorderable.id, new Reorderable().id); + assert.strictEqual(reorderable.list_class, 'reorderable_list'); + assert.strictEqual(reorderable.item_class, 'reorderable_item'); }); test('creates with custom direction', () => { const reorderable = new Reorderable({direction: 'horizontal'}); - expect(reorderable.direction).toBe('horizontal'); + assert.strictEqual(reorderable.direction, 'horizontal'); }); test('creates with custom styling', () => { @@ -128,11 +126,11 @@ describe('Reorderable', () => { dragging_class: 'custom_dragging', }); - expect(reorderable.list_class).toBe('custom_list'); - expect(reorderable.item_class).toBe('custom_item'); - expect(reorderable.dragging_class).toBe('custom_dragging'); + assert.strictEqual(reorderable.list_class, 'custom_list'); + assert.strictEqual(reorderable.item_class, 'custom_item'); + assert.strictEqual(reorderable.dragging_class, 'custom_dragging'); // Other styles should have default values - expect(reorderable.drag_over_class).toBe('drag_over'); + assert.strictEqual(reorderable.drag_over_class, 'drag_over'); }); }); @@ -156,43 +154,43 @@ describe('Reorderable', () => { test('initializes correctly', () => { cleanup_fn = attach_list(reorderable, list, {onreorder: mock_callback}); - expect(reorderable.list_node).toBe(list); - expect(reorderable.list_params).toEqual({onreorder: mock_callback}); - expect(list.classList.contains(reorderable.list_class!)).toBe(true); - expect(list.getAttribute('role')).toBe('list'); - expect(list.dataset.reorderable_list_id).toBe(reorderable.id); + assert.strictEqual(reorderable.list_node, list); + assert.deepEqual(reorderable.list_params, {onreorder: mock_callback}); + assert.ok(list.classList.contains(reorderable.list_class!)); + assert.strictEqual(list.getAttribute('role'), 'list'); + assert.strictEqual(list.dataset.reorderable_list_id, reorderable.id); }); test('re-attachment changes callbacks', () => { const mock_callback2 = vi.fn(); const cleanup1 = attach_list(reorderable, list, {onreorder: mock_callback}); - expect(reorderable.list_params).toEqual({onreorder: mock_callback}); + assert.deepEqual(reorderable.list_params, {onreorder: mock_callback}); // Re-attach with new callback cleanup1(); cleanup_fn = attach_list(reorderable, list, {onreorder: mock_callback2}); - expect(reorderable.list_params).toEqual({onreorder: mock_callback2}); + assert.deepEqual(reorderable.list_params, {onreorder: mock_callback2}); }); test('destroy cleans up', () => { cleanup_fn = attach_list(reorderable, list, {onreorder: mock_callback}); // Before destroy - expect(reorderable.list_node).toBe(list); - expect(list.classList.contains(reorderable.list_class!)).toBe(true); + assert.strictEqual(reorderable.list_node, list); + assert.ok(list.classList.contains(reorderable.list_class!)); // Destroy cleanup_fn(); cleanup_fn = undefined; // After destroy - expect(reorderable.list_node).toBeNull(); - expect(reorderable.list_params).toBeNull(); - expect(list.classList.contains(reorderable.list_class!)).toBe(false); - expect(list.hasAttribute('role')).toBe(false); - expect(list.dataset.reorderable_list_id).toBeUndefined(); + assert.isNull(reorderable.list_node); + assert.isNull(reorderable.list_params); + assert.ok(!list.classList.contains(reorderable.list_class!)); + assert.ok(!list.hasAttribute('role')); + assert.ok(list.dataset.reorderable_list_id === undefined); }); }); @@ -218,11 +216,11 @@ describe('Reorderable', () => { test('initializes correctly', () => { cleanup_fn = attach_item(reorderable, item, {index: 0}); - expect(item.classList.contains(reorderable.item_class!)).toBe(true); - expect(item.getAttribute('draggable')).toBe('true'); - expect(item.getAttribute('role')).toBe('listitem'); - expect(item.dataset.reorderable_item_id).toBeDefined(); - expect(item.dataset.reorderable_list_id).toBe(reorderable.id); + assert.ok(item.classList.contains(reorderable.item_class!)); + assert.strictEqual(item.getAttribute('draggable'), 'true'); + assert.strictEqual(item.getAttribute('role'), 'listitem'); + assert.isDefined(item.dataset.reorderable_item_id); + assert.strictEqual(item.dataset.reorderable_list_id, reorderable.id); // Either in pending items or regular maps const item_id = item.dataset.reorderable_item_id as ReorderableItemId; @@ -230,7 +228,7 @@ describe('Reorderable', () => { ? reorderable.indices.has(item_id) : reorderable.pending_items.some((p) => p.id === item_id); - expect(is_indexed).toBe(true); + assert.ok(is_indexed); }); test('re-attachment changes index', () => { @@ -241,10 +239,10 @@ describe('Reorderable', () => { // Check initial index if (reorderable.initialized) { - expect(reorderable.indices.get(item_id)).toBe(0); + assert.strictEqual(reorderable.indices.get(item_id), 0); } else { const pending_item = reorderable.pending_items.find((p) => p.id === item_id); - expect(pending_item?.index).toBe(0); + assert.strictEqual(pending_item?.index, 0); } // Re-attach with new index @@ -256,10 +254,10 @@ describe('Reorderable', () => { // Check if index was updated in the appropriate storage if (reorderable.initialized) { - expect(reorderable.indices.get(new_item_id)).toBe(5); + assert.strictEqual(reorderable.indices.get(new_item_id), 5); } else { const pending_item = reorderable.pending_items.find((p) => p.id === new_item_id); - expect(pending_item?.index).toBe(5); + assert.strictEqual(pending_item?.index, 5); } }); @@ -269,23 +267,23 @@ describe('Reorderable', () => { const item_id = item.dataset.reorderable_item_id as ReorderableItemId; // Before destroy - expect(item.classList.contains(reorderable.item_class!)).toBe(true); + assert.ok(item.classList.contains(reorderable.item_class!)); // Destroy cleanup_fn(); cleanup_fn = undefined; // After destroy - expect(item.classList.contains(reorderable.item_class!)).toBe(false); - expect(item.hasAttribute('draggable')).toBe(false); - expect(item.hasAttribute('role')).toBe(false); - expect(item.dataset.reorderable_item_id).toBeUndefined(); - expect(item.dataset.reorderable_list_id).toBeUndefined(); + assert.ok(!item.classList.contains(reorderable.item_class!)); + assert.ok(!item.hasAttribute('draggable')); + assert.ok(!item.hasAttribute('role')); + assert.ok(item.dataset.reorderable_item_id === undefined); + assert.ok(item.dataset.reorderable_list_id === undefined); // Item should be removed from storage const still_pending = reorderable.pending_items.some((p) => p.id === item_id); const still_indexed = reorderable.indices.has(item_id); - expect(still_pending || still_indexed).toBe(false); + assert.ok(!(still_pending || still_indexed)); }); }); @@ -319,29 +317,29 @@ describe('Reorderable', () => { test('update_indicator applies correct classes', () => { // Update indicators reorderable.update_indicator(item_id, 'top'); - expect(item.classList.contains(reorderable.drag_over_class!)).toBe(true); - expect(item.classList.contains(reorderable.drag_over_top_class!)).toBe(true); + assert.ok(item.classList.contains(reorderable.drag_over_class!)); + assert.ok(item.classList.contains(reorderable.drag_over_top_class!)); // Change indicator reorderable.update_indicator(item_id, 'bottom'); - expect(item.classList.contains(reorderable.drag_over_top_class!)).toBe(false); - expect(item.classList.contains(reorderable.drag_over_bottom_class!)).toBe(true); + assert.ok(!item.classList.contains(reorderable.drag_over_top_class!)); + assert.ok(item.classList.contains(reorderable.drag_over_bottom_class!)); // Invalid drop reorderable.update_indicator(item_id, 'left', false); - expect(item.classList.contains(reorderable.drag_over_left_class!)).toBe(false); - expect(item.classList.contains(reorderable.invalid_drop_class!)).toBe(true); + assert.ok(!item.classList.contains(reorderable.drag_over_left_class!)); + assert.ok(item.classList.contains(reorderable.invalid_drop_class!)); }); test('clear_indicators removes all indicator classes', () => { // Add indicator reorderable.update_indicator(item_id, 'right'); - expect(item.classList.contains(reorderable.drag_over_right_class!)).toBe(true); + assert.ok(item.classList.contains(reorderable.drag_over_right_class!)); // Clear indicators reorderable.clear_indicators(); - expect(item.classList.contains(reorderable.drag_over_class!)).toBe(false); - expect(item.classList.contains(reorderable.drag_over_right_class!)).toBe(false); + assert.ok(!item.classList.contains(reorderable.drag_over_class!)); + assert.ok(!item.classList.contains(reorderable.drag_over_right_class!)); }); }); @@ -395,10 +393,10 @@ describe('Reorderable', () => { first_item.dispatchEvent(drag_event); // Check if drag operation was set up - expect(reorderable.source_index).toBe(0); - expect(reorderable.source_item_id).toBe(item_id); - expect(first_item.classList.contains(reorderable.dragging_class!)).toBe(true); - expect(mock_data_transfer.setData).toHaveBeenCalled(); + assert.strictEqual(reorderable.source_index, 0); + assert.strictEqual(reorderable.source_item_id, item_id); + assert.ok(first_item.classList.contains(reorderable.dragging_class!)); + assert.ok(mock_data_transfer.setData.mock.calls.length > 0); }); test('dragend resets state', () => { @@ -416,9 +414,9 @@ describe('Reorderable', () => { list.dispatchEvent(dragend_event); // Check if state was reset - expect(reorderable.source_index).toBe(-1); - expect(reorderable.source_item_id).toBeNull(); - expect(first_item.classList.contains(reorderable.dragging_class!)).toBe(false); + assert.strictEqual(reorderable.source_index, -1); + assert.isNull(reorderable.source_item_id); + assert.ok(!first_item.classList.contains(reorderable.dragging_class!)); }); }); @@ -431,10 +429,8 @@ describe('Reorderable', () => { // Initialize first reorderable const cleanup1 = attach_list(reorderable1, list, {onreorder: vi.fn()}); - // Expect no error when trying to initialize second reorderable with same list - expect(() => { - attach_list(reorderable2, list, {onreorder: vi.fn()}); - }).not.toThrow(); + // Should not throw when trying to initialize second reorderable with same list + attach_list(reorderable2, list, {onreorder: vi.fn()}); // Clean up cleanup1(); @@ -456,7 +452,7 @@ describe('Reorderable', () => { const cleanup2 = attachment2(list); // Should work without errors - expect(reorderable.list_node).toBe(list); + assert.strictEqual(reorderable.list_node, list); // Clean up if (cleanup2) cleanup2(); @@ -497,8 +493,8 @@ describe('Reorderable', () => { inner_item.dispatchEvent(drag_event); // Should find the outer item as the dragged item - expect(reorderable.source_item_id).toBe(outer_id); - expect(reorderable.source_index).toBe(0); + assert.strictEqual(reorderable.source_item_id, outer_id); + assert.strictEqual(reorderable.source_index, 0); // Clean up outer_action.destroy?.(); @@ -537,13 +533,13 @@ describe('Reorderable', () => { target_item.dispatchEvent(drop_event1); // onreorder should not be called for invalid target - expect(onreorder).not.toHaveBeenCalled(); + assert.strictEqual(onreorder.mock.calls.length, 0); // Directly call the onreorder function as the implementation would reorderable.list_params?.onreorder(0, 2); // Now the callback should have been called - expect(onreorder).toHaveBeenCalledWith(0, 2); + assert.deepEqual(onreorder.mock.calls[0], [0, 2]); // Clean up for (const r of action_results) r?.destroy(); @@ -578,15 +574,15 @@ describe('Reorderable', () => { const other_id = other_item.dataset.reorderable_item_id as ReorderableItemId; reorderable.update_indicator(other_id, 'bottom'); - expect(other_item.classList.contains(reorderable.drag_over_class!)).toBe(true); + assert.ok(other_item.classList.contains(reorderable.drag_over_class!)); // Now try to apply indicators to the source item reorderable.update_indicator(source_id, 'top'); // Indicators should be cleared instead - expect(source_item.classList.contains(reorderable.drag_over_class!)).toBe(false); - expect(reorderable.active_indicator_item_id).toBeNull(); - expect(reorderable.current_indicator).toBe('none'); + assert.ok(!source_item.classList.contains(reorderable.drag_over_class!)); + assert.isNull(reorderable.active_indicator_item_id); + assert.strictEqual(reorderable.current_indicator, 'none'); // Clean up for (const r of action_results) r?.destroy(); @@ -637,15 +633,15 @@ describe('Reorderable', () => { first_item1.dispatchEvent(drag_event1); // Should only affect first reorderable - expect(reorderable1.source_index).toBe(0); - expect(reorderable2.source_index).toBe(-1); + assert.strictEqual(reorderable1.source_index, 0); + assert.strictEqual(reorderable2.source_index, -1); // Directly call the callback instead of relying on event propagation onreorder1(0, 1); // Only first callback should be called - expect(onreorder1).toHaveBeenCalled(); - expect(onreorder2).not.toHaveBeenCalled(); + assert.ok(onreorder1.mock.calls.length > 0); + assert.strictEqual(onreorder2.mock.calls.length, 0); // Clean up for (const r of action_results1) r?.destroy(); @@ -676,24 +672,24 @@ describe('Reorderable', () => { }); // Check list class - expect(list.classList.contains('my_list')).toBe(true); + assert.ok(list.classList.contains('my_list')); // Check item class const first_item = items[0]; const second_item = items[1]; if (!first_item || !second_item) throw new Error('Expected first and second items'); - expect(first_item.classList.contains('my_item')).toBe(true); + assert.ok(first_item.classList.contains('my_item')); // Apply dragging class first_item.classList.add(reorderable.dragging_class!); - expect(first_item.classList.contains('my_dragging')).toBe(true); + assert.ok(first_item.classList.contains('my_dragging')); // Apply indicator second_item.classList.add(reorderable.drag_over_class!); second_item.classList.add(reorderable.drag_over_top_class!); - expect(second_item.classList.contains('my_drag_over')).toBe(true); - expect(second_item.classList.contains('my_drag_over_top')).toBe(true); + assert.ok(second_item.classList.contains('my_drag_over')); + assert.ok(second_item.classList.contains('my_drag_over_top')); // Clean up for (const r of action_results) r?.destroy(); @@ -713,12 +709,12 @@ describe('Reorderable', () => { }); // Check list role - expect(list.getAttribute('role')).toBe('list'); + assert.strictEqual(list.getAttribute('role'), 'list'); // Check item role const first_item = items[0]; if (!first_item) throw new Error('Expected first item'); - expect(first_item.getAttribute('role')).toBe('listitem'); + assert.strictEqual(first_item.getAttribute('role'), 'listitem'); // Clean up for (const r of action_results) r?.destroy(); diff --git a/src/test/request_tracker.svelte.test.ts b/src/test/request_tracker.svelte.test.ts index e4c89bc66..e5c75ddd9 100644 --- a/src/test/request_tracker.svelte.test.ts +++ b/src/test/request_tracker.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Opus 4 - // @vitest-environment jsdom -import {test, expect, describe, vi, beforeEach, afterEach} from 'vitest'; +import {test, describe, vi, beforeEach, afterEach, assert} from 'vitest'; import {RequestTracker} from '$lib/request_tracker.svelte.js'; import {JSONRPC_INTERNAL_ERROR, JSONRPC_VERSION, JsonrpcErrorCode} from '$lib/jsonrpc.js'; @@ -37,27 +35,27 @@ describe('RequestTracker', () => { test('creates with default timeout', () => { const tracker = new RequestTracker(); - expect(tracker).toBeInstanceOf(RequestTracker); - expect(tracker.request_timeout_ms).toBe(120_000); - expect(tracker.pending_requests.size).toBe(0); + assert.instanceOf(tracker, RequestTracker); + assert.strictEqual(tracker.request_timeout_ms, 120_000); + assert.strictEqual(tracker.pending_requests.size, 0); }); test('creates with custom timeout', () => { const custom_timeout = 5000; const tracker = new RequestTracker(custom_timeout); - expect(tracker.request_timeout_ms).toBe(custom_timeout); - expect(tracker.pending_requests).toBeInstanceOf(Map); + assert.strictEqual(tracker.request_timeout_ms, custom_timeout); + assert.instanceOf(tracker.pending_requests, Map); }); test('handles zero or negative timeout values', () => { // Zero timeout should be allowed but would cause immediate timeouts const zero_tracker = new RequestTracker(0); - expect(zero_tracker.request_timeout_ms).toBe(0); + assert.strictEqual(zero_tracker.request_timeout_ms, 0); // Negative timeout should be allowed (though it's an edge case) const negative_tracker = new RequestTracker(-1000); - expect(negative_tracker.request_timeout_ms).toBe(-1000); + assert.strictEqual(negative_tracker.request_timeout_ms, -1000); }); }); @@ -68,21 +66,21 @@ describe('RequestTracker', () => { const deferred = tracker.track_request(id); // Should return a deferred promise with the correct interface - expect(deferred).toBeDefined(); - expect(deferred.promise).toBeInstanceOf(Promise); - expect(deferred.resolve).toBeInstanceOf(Function); - expect(deferred.reject).toBeInstanceOf(Function); + assert.isDefined(deferred); + assert.instanceOf(deferred.promise, Promise); + assert.instanceOf(deferred.resolve, Function); + assert.instanceOf(deferred.reject, Function); // Request should be stored with the correct properties - expect(tracker.pending_requests.has(id)).toBe(true); + assert.ok(tracker.pending_requests.has(id)); const request = tracker.pending_requests.get(id); - expect(request).toBeDefined(); - expect(request?.deferred).toBe(deferred); - expect(request?.status).toBe('pending'); - expect(request?.timeout).toBeDefined(); - expect(request?.created).toBeDefined(); - expect(typeof request?.created).toBe('string'); + assert.isDefined(request); + assert.strictEqual(request.deferred, deferred); + assert.strictEqual(request.status, 'pending'); + assert.isDefined(request.timeout); + assert.isDefined(request.created); + assert.strictEqual(typeof request.created, 'string'); // Clean up tracker.cancel_request(id); @@ -96,10 +94,10 @@ describe('RequestTracker', () => { const deferred1 = tracker.track_request(id1); const deferred2 = tracker.track_request(id2); - expect(deferred1).not.toBe(deferred2); - expect(tracker.pending_requests.size).toBe(2); - expect(tracker.pending_requests.get(id1)?.deferred).toBe(deferred1); - expect(tracker.pending_requests.get(id2)?.deferred).toBe(deferred2); + assert.notStrictEqual(deferred1, deferred2); + assert.strictEqual(tracker.pending_requests.size, 2); + assert.strictEqual(tracker.pending_requests.get(id1)?.deferred, deferred1); + assert.strictEqual(tracker.pending_requests.get(id2)?.deferred, deferred2); // Add promise handlers to catch rejections const promise1 = deferred1.promise.catch(() => { @@ -128,7 +126,7 @@ describe('RequestTracker', () => { return err; // Return to ensure promise settles }); - expect(tracker.pending_requests.has(id)).toBe(true); + assert.ok(tracker.pending_requests.has(id)); // Fast-forward time to trigger timeout vi.advanceTimersByTime(1001); @@ -136,11 +134,11 @@ describe('RequestTracker', () => { await Promise.resolve(); // Allow promise microtasks to process // Request should be removed and promise rejected with timeout error - expect(tracker.pending_requests.has(id)).toBe(false); - expect(rejection_error).toBeDefined(); - expect(rejection_error).toBeInstanceOf(ThrownJsonrpcError); - expect(rejection_error.code).toBe(JSONRPC_INTERNAL_ERROR); - expect(rejection_error.message).toBe(`request timed out: ${id}`); + assert.ok(!tracker.pending_requests.has(id)); + assert.isDefined(rejection_error); + assert.instanceOf(rejection_error, ThrownJsonrpcError); + assert.strictEqual(rejection_error.code, JSONRPC_INTERNAL_ERROR); + assert.strictEqual(rejection_error.message, `request timed out: ${id}`); }); test('cleans up previous request with same id', () => { @@ -152,18 +150,18 @@ describe('RequestTracker', () => { // Track first request const deferred1 = tracker.track_request(id); const timeout1 = tracker.pending_requests.get(id)?.timeout; - expect(timeout1).toBeDefined(); + assert.isDefined(timeout1); // Track second request with same id const deferred2 = tracker.track_request(id); // Verify timeout was cleared for first request - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout1); - expect(deferred1).not.toBe(deferred2); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout1)); + assert.notStrictEqual(deferred1, deferred2); // Only one request should exist - expect(tracker.pending_requests.size).toBe(1); - expect(tracker.pending_requests.get(id)?.deferred).toBe(deferred2); + assert.strictEqual(tracker.pending_requests.size, 1); + assert.strictEqual(tracker.pending_requests.get(id)?.deferred, deferred2); // Clean up tracker.cancel_request(id); @@ -207,10 +205,10 @@ describe('RequestTracker', () => { const result = await promise1; // The promise should have timed out rather than be settled directly - expect(result).toBe('timeout'); + assert.strictEqual(result, 'timeout'); // The first promise should not be directly resolved or rejected by the tracker - expect(promise1_settled).toBe(false); + assert.ok(!promise1_settled); // Cancel all requests to clean up tracker.cancel_all_requests(); @@ -231,14 +229,14 @@ describe('RequestTracker', () => { tracker.resolve_request(id, response); // Verify timeout was cleared - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout)); // Verify request status was updated before resolution - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); // Verify promise resolves with correct value const result = await deferred.promise; - expect(result).toBe(response); + assert.strictEqual(result, response); }); test('logs warning for unknown request id', () => { @@ -249,8 +247,10 @@ describe('RequestTracker', () => { tracker.resolve_request(unknown_id, response); - expect(warn_spy).toHaveBeenCalledTimes(1); - expect(warn_spy).toHaveBeenCalledWith(`received response for unknown request: ${unknown_id}`); + assert.strictEqual(warn_spy.mock.calls.length, 1); + assert.deepEqual(warn_spy.mock.calls[0], [ + `received response for unknown request: ${unknown_id}`, + ]); }); test('handles various data types', async () => { @@ -269,7 +269,7 @@ describe('RequestTracker', () => { const response = create_jsonrpc_response(id, {method}); tracker.resolve_request(id, response); const result = await deferred.promise; - expect(result).toBe(response); + assert.strictEqual(result, response); }); await Promise.all(promises); @@ -296,7 +296,7 @@ describe('RequestTracker', () => { tracker.resolve_request(id, create_jsonrpc_response(id, {test: 'result'})); await promise; - expect(status_when_resolved).toBe('success'); + assert.strictEqual(status_when_resolved, 'success'); }); }); @@ -318,15 +318,14 @@ describe('RequestTracker', () => { tracker.reject_request(id, error); // Verify timeout was cleared - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout)); + assert.ok(!tracker.pending_requests.has(id)); // Verify promise rejects with the correct error - await expect(deferred.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); - const rejection_error = await deferred.promise.catch((err) => err); - expect(rejection_error.code).toBe(error.error.code); - expect(rejection_error.message).toBe(error.error.message); + assert.instanceOf(rejection_error, ThrownJsonrpcError); + assert.strictEqual(rejection_error.code, error.error.code); + assert.strictEqual(rejection_error.message, error.error.message); }); test('logs warning for unknown request id', () => { @@ -339,8 +338,10 @@ describe('RequestTracker', () => { error: {code: JsonrpcErrorCode.parse(-32000), message: 'test'}, }); - expect(warn_spy).toHaveBeenCalledTimes(1); - expect(warn_spy).toHaveBeenCalledWith(`received error for unknown request: ${unknown_id}`); + assert.strictEqual(warn_spy.mock.calls.length, 1); + assert.deepEqual(warn_spy.mock.calls[0], [ + `received error for unknown request: ${unknown_id}`, + ]); }); test('handles various error types', async () => { @@ -379,13 +380,12 @@ describe('RequestTracker', () => { for (const {id, error} of test_cases) { const deferred = tracker.track_request(id); tracker.reject_request(id, error); - await expect(deferred.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); // eslint-disable-line no-await-in-loop - - const rejection_error = await deferred.promise.catch((err) => err); // eslint-disable-line no-await-in-loop - expect(rejection_error.code).toBe(error.error.code); - expect(rejection_error.message).toBe(error.error.message); + const rejection_error = await deferred.promise.catch((err) => err); + assert.instanceOf(rejection_error, ThrownJsonrpcError); + assert.strictEqual(rejection_error.code, error.error.code); + assert.strictEqual(rejection_error.message, error.error.message); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); } }); @@ -416,7 +416,7 @@ describe('RequestTracker', () => { }); await promise; - expect(status_when_rejected).toBe('failure'); + assert.strictEqual(status_when_rejected, 'failure'); }); }); @@ -439,12 +439,13 @@ describe('RequestTracker', () => { tracker.handle_message(message); // Verify resolve_request was called with correct arguments - expect(resolve_spy).toHaveBeenCalledWith(id, message); + assert.ok(resolve_spy.mock.calls.length > 0); + assert.deepEqual(resolve_spy.mock.calls[0], [id, message] as any); // Verify promise resolves with correct value const response = await deferred.promise; - expect(response).toBe(message); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.strictEqual(response, message as any); + assert.ok(!tracker.pending_requests.has(id)); }); test('rejects request with error when message contains error', async () => { @@ -466,15 +467,15 @@ describe('RequestTracker', () => { tracker.handle_message(message); // Verify reject_request was called with correct arguments - expect(reject_spy).toHaveBeenCalledWith(id, message); + assert.ok(reject_spy.mock.calls.length > 0); + assert.deepEqual(reject_spy.mock.calls[0], [id, message] as any); // Verify promise rejects with correct error - await expect(deferred.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); - const rejection_error = await deferred.promise.catch((err) => err); - expect(rejection_error.code).toBe(message.error.code); - expect(rejection_error.message).toBe(message.error.message); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.instanceOf(rejection_error, ThrownJsonrpcError); + assert.strictEqual(rejection_error.code, message.error.code); + assert.strictEqual(rejection_error.message, message.error.message); + assert.ok(!tracker.pending_requests.has(id)); }); test('ignores notification messages (no id)', () => { @@ -494,11 +495,11 @@ describe('RequestTracker', () => { }); // Verify no resolve/reject was called - expect(resolve_spy).not.toHaveBeenCalled(); - expect(reject_spy).not.toHaveBeenCalled(); + assert.strictEqual(resolve_spy.mock.calls.length, 0); + assert.strictEqual(reject_spy.mock.calls.length, 0); // Original request should still be pending - expect(tracker.pending_requests.has(id)).toBe(true); + assert.ok(tracker.pending_requests.has(id)); // Clean up tracker.cancel_request(id); @@ -521,11 +522,11 @@ describe('RequestTracker', () => { }); // Verify no resolve/reject was called - expect(resolve_spy).not.toHaveBeenCalled(); - expect(reject_spy).not.toHaveBeenCalled(); + assert.strictEqual(resolve_spy.mock.calls.length, 0); + assert.strictEqual(reject_spy.mock.calls.length, 0); // Original request should still be pending - expect(tracker.pending_requests.has(id)).toBe(true); + assert.ok(tracker.pending_requests.has(id)); // Clean up tracker.cancel_request(id); @@ -546,11 +547,11 @@ describe('RequestTracker', () => { tracker.handle_message({}); // Verify no resolve/reject was called - expect(resolve_spy).not.toHaveBeenCalled(); - expect(reject_spy).not.toHaveBeenCalled(); + assert.strictEqual(resolve_spy.mock.calls.length, 0); + assert.strictEqual(reject_spy.mock.calls.length, 0); // Original request should still be pending - expect(tracker.pending_requests.has(id)).toBe(true); + assert.ok(tracker.pending_requests.has(id)); // Clean up tracker.cancel_request(id); @@ -574,8 +575,9 @@ describe('RequestTracker', () => { tracker.handle_message(message); // Verify resolve_request was called with correct arguments - expect(resolve_spy).toHaveBeenCalledWith(id, message); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(resolve_spy.mock.calls.length > 0); + assert.deepEqual(resolve_spy.mock.calls[0], [id, message] as any); + assert.ok(!tracker.pending_requests.has(id)); }); test('prioritizes error over result if both exist in the message', async () => { @@ -598,14 +600,14 @@ describe('RequestTracker', () => { tracker.handle_message(message); // Should call reject_request, not resolve_request - expect(reject_spy).toHaveBeenCalledWith(id, message); + assert.ok(reject_spy.mock.calls.length > 0); + assert.deepEqual(reject_spy.mock.calls[0], [id, message] as any); // Promise should be rejected with the error - await expect(deferred.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); - const rejection_error = await deferred.promise.catch((err) => err); - expect(rejection_error.code).toBe(message.error.code); - expect(rejection_error.message).toBe(message.error.message); + assert.instanceOf(rejection_error, ThrownJsonrpcError); + assert.strictEqual(rejection_error.code, message.error.code); + assert.strictEqual(rejection_error.message, message.error.message); }); }); @@ -621,10 +623,10 @@ describe('RequestTracker', () => { tracker.cancel_request(id); // Verify timeout was cleared - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout)); // Request should be removed - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); }); test('does nothing for unknown request id', () => { @@ -635,7 +637,7 @@ describe('RequestTracker', () => { tracker.cancel_request(unknown_id); // Should not attempt to clear any timeout - expect(clear_timeout_spy).not.toHaveBeenCalled(); + assert.strictEqual(clear_timeout_spy.mock.calls.length, 0); }); test('handles cancel without affecting other requests', () => { @@ -649,8 +651,8 @@ describe('RequestTracker', () => { tracker.cancel_request(id1); // Only the specified request should be removed - expect(tracker.pending_requests.has(id1)).toBe(false); - expect(tracker.pending_requests.has(id2)).toBe(true); + assert.ok(!tracker.pending_requests.has(id1)); + assert.ok(tracker.pending_requests.has(id2)); // Clean up tracker.cancel_request(id2); @@ -691,14 +693,14 @@ describe('RequestTracker', () => { const result = await promise; // Result should be timeout, not a resolution or rejection - expect(result).toBe('timeout'); + assert.strictEqual(result, 'timeout'); // Request should be removed - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); // Promise should be neither resolved nor rejected directly - expect(was_resolved).toBe(false); - expect(was_rejected).toBe(false); + assert.ok(!was_resolved); + assert.ok(!was_rejected); }); }); @@ -718,18 +720,22 @@ describe('RequestTracker', () => { const timeout2 = tracker.pending_requests.get(id2)?.timeout; // Set up promise rejection tracking - const promise1 = expect(deferred1.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); - const promise2 = expect(deferred2.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); + const promise1 = deferred1.promise.catch((err) => { + assert.instanceOf(err, ThrownJsonrpcError); + }); + const promise2 = deferred2.promise.catch((err) => { + assert.instanceOf(err, ThrownJsonrpcError); + }); // Cancel all requests tracker.cancel_all_requests(custom_reason); // Verify timeouts were cleared - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout1); - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout2); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout1)); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout2)); // All requests should be removed - expect(tracker.pending_requests.size).toBe(0); + assert.strictEqual(tracker.pending_requests.size, 0); // Wait for promise rejections to complete await Promise.allSettled([promise1, promise2]); @@ -740,10 +746,12 @@ describe('RequestTracker', () => { const id = 'req_1'; const deferred = tracker.track_request(id); - const promise = expect(deferred.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); + const promise = deferred.promise.catch((err) => { + assert.instanceOf(err, ThrownJsonrpcError); + }); tracker.cancel_all_requests(); - expect(tracker.pending_requests.size).toBe(0); + assert.strictEqual(tracker.pending_requests.size, 0); await promise; }); @@ -755,7 +763,7 @@ describe('RequestTracker', () => { tracker.cancel_all_requests(); // Should not attempt to clear any timeouts - expect(clear_timeout_spy).not.toHaveBeenCalled(); + assert.strictEqual(clear_timeout_spy.mock.calls.length, 0); }); test('sets failure status before rejecting', async () => { @@ -781,7 +789,7 @@ describe('RequestTracker', () => { tracker.cancel_all_requests(); await promise; - expect(status_when_rejected).toBe('failure'); + assert.strictEqual(status_when_rejected, 'failure'); }); test('rejects with ThrownJsonrpcError instance when cancelling all requests', async () => { @@ -791,7 +799,9 @@ describe('RequestTracker', () => { const deferred = tracker.track_request(id); // Set up testing for ThrownJsonrpcError instance - const promise = expect(deferred.promise).rejects.toBeInstanceOf(ThrownJsonrpcError); + const promise = deferred.promise.catch((err) => { + assert.instanceOf(err, ThrownJsonrpcError); + }); tracker.cancel_all_requests(); @@ -813,12 +823,12 @@ describe('RequestTracker', () => { const deferred2 = tracker.track_request(id); // Should be different deferred objects - expect(deferred1).not.toBe(deferred2); - expect(tracker.pending_requests.size).toBe(1); - expect(tracker.pending_requests.get(id)?.deferred).toBe(deferred2); + assert.notStrictEqual(deferred1, deferred2); + assert.strictEqual(tracker.pending_requests.size, 1); + assert.strictEqual(tracker.pending_requests.get(id)?.deferred, deferred2); // Should have cleared the timeout from the first request - expect(clear_timeout_spy).toHaveBeenCalledWith(timeout1); + assert.ok(clear_timeout_spy.mock.calls.some((call) => call[0] === timeout1)); // Clean up tracker.cancel_request(id); @@ -835,14 +845,14 @@ describe('RequestTracker', () => { for (const {id, method} of test_cases) { const deferred = tracker.track_request(id); - expect(tracker.pending_requests.has(id)).toBe(true); + assert.ok(tracker.pending_requests.has(id)); const response = create_jsonrpc_response(id, {method}); tracker.resolve_request(id, response); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); - const result = await deferred.promise; // eslint-disable-line no-await-in-loop - expect(result).toBe(response); + const result = await deferred.promise; + assert.strictEqual(result, response); } }); @@ -860,13 +870,13 @@ describe('RequestTracker', () => { }); const result = await deferred.promise; - expect(result).toEqual({ + assert.deepEqual(result as any, { jsonrpc: JSONRPC_VERSION, id, method: 'test', result: null, }); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); }); test('request timeout uses correct error object', async () => { @@ -882,9 +892,9 @@ describe('RequestTracker', () => { vi.advanceTimersByTime(101); const error = await error_promise; - expect(error).toBeInstanceOf(ThrownJsonrpcError); - expect(error.code).toBe(JSONRPC_INTERNAL_ERROR); - expect(error.message).toBe(`request timed out: ${id}`); + assert.instanceOf(error, ThrownJsonrpcError); + assert.strictEqual(error.code, JSONRPC_INTERNAL_ERROR); + assert.strictEqual(error.message, `request timed out: ${id}`); }); test('handles undefined timeout when clearing timeouts', () => { @@ -903,7 +913,7 @@ describe('RequestTracker', () => { tracker.cancel_request(id); // Request should be removed - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); // Cleanup clearTimeout(original_timeout); @@ -951,7 +961,7 @@ describe('RequestTracker', () => { // Promise should resolve with first value const result = await deferred.promise; - expect(result).toEqual({ + assert.deepEqual(result, { jsonrpc: '2.0', id, result: { @@ -960,7 +970,7 @@ describe('RequestTracker', () => { }); // Warnings should be logged for the duplicate calls - expect(warn_spy).toHaveBeenCalledTimes(2); + assert.strictEqual(warn_spy.mock.calls.length, 2); }); }); @@ -971,8 +981,8 @@ describe('RequestTracker', () => { // Track the request const deferred = tracker.track_request(id); - expect(tracker.pending_requests.has(id)).toBe(true); - expect(tracker.pending_requests.get(id)?.status).toBe('pending'); + assert.ok(tracker.pending_requests.has(id)); + assert.strictEqual(tracker.pending_requests.get(id)?.status, 'pending'); // Resolve the request const response = create_jsonrpc_response(id, {status: 'success'}); @@ -982,11 +992,11 @@ describe('RequestTracker', () => { const result = await deferred.promise; // Request should be resolved and removed - expect(result).toEqual({ + assert.deepEqual(result, { ...response, result: {status: 'success'}, }); - expect(tracker.pending_requests.has(id)).toBe(false); + assert.ok(!tracker.pending_requests.has(id)); }); test('handles simultaneous requests with different IDs', async () => { @@ -1000,7 +1010,7 @@ describe('RequestTracker', () => { })); // All requests should be pending - expect(tracker.pending_requests.size).toBe(ids.length); + assert.strictEqual(tracker.pending_requests.size, ids.length); // Resolve them in reverse order for (let i = ids.length - 1; i >= 0; i--) { @@ -1014,14 +1024,14 @@ describe('RequestTracker', () => { // Verify each result matches its request results.forEach((result, index) => { - expect(result.id).toBe(ids[index]); + assert.strictEqual(result.id, ids[index]); if (is_jsonrpc_response(result)) { - expect(result.result.test).toBe('result'); + assert.strictEqual(result.result.test, 'result'); } }); // All requests should be removed - expect(tracker.pending_requests.size).toBe(0); + assert.strictEqual(tracker.pending_requests.size, 0); }); test('handles a mix of resolved, rejected, and timed out requests', async () => { @@ -1051,19 +1061,19 @@ describe('RequestTracker', () => { // Set up promises to check results const resolve_promise = resolve_deferred.promise.then((result) => { - expect(result).toHaveProperty('method', 'test_method'); + assert.strictEqual((result as any).method, 'test_method'); return true; }); const reject_promise = reject_deferred.promise.catch((error) => { - expect(error).toBeInstanceOf(ThrownJsonrpcError); - expect(error.message).toBe('rejected'); + assert.instanceOf(error, ThrownJsonrpcError); + assert.strictEqual(error.message, 'rejected'); return true; }); const timeout_promise = timeout_deferred.promise.catch((error) => { - expect(error).toBeInstanceOf(ThrownJsonrpcError); - expect(error.message).toBe(`request timed out: ${timeout_id}`); + assert.instanceOf(error, ThrownJsonrpcError); + assert.strictEqual(error.message, `request timed out: ${timeout_id}`); return true; }); @@ -1071,7 +1081,7 @@ describe('RequestTracker', () => { await Promise.allSettled([resolve_promise, reject_promise, timeout_promise]); // All requests should be removed - expect(tracker.pending_requests.size).toBe(0); + assert.strictEqual(tracker.pending_requests.size, 0); }); }); }); diff --git a/src/test/server/env_file_helpers.basic.test.ts b/src/test/server/env_file_helpers.basic.test.ts index 9fce7a597..f2a50119b 100644 --- a/src/test/server/env_file_helpers.basic.test.ts +++ b/src/test/server/env_file_helpers.basic.test.ts @@ -1,11 +1,7 @@ -// @slop Claude Sonnet 4.5 - -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {update_env_variable} from '$lib/server/env_file_helpers.js'; -/* eslint-disable @typescript-eslint/require-await */ - /** * Creates an in-memory file system for testing. * No module-level mocks - uses dependency injection instead. @@ -30,165 +26,110 @@ const create_mock_fs = (initial_files: Record = {}) => { files[path] = content; }, get_file: (path: string): string | undefined => files[path], - get_all_files: (): Record => ({...files}), }; }; -describe('update_env_variable - basic functionality', () => { - test('updates existing variable with quotes', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"\n', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"\n'); - }); - - test('updates existing variable without quotes', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY=old_value\n', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY=new_value\n'); - }); - - test('adds new variable to empty file', async () => { - const fs = create_mock_fs({ - '/test/.env': '', - }); +// null initial means no file exists (triggers ENOENT → create) +const basic_cases: Array< + [label: string, initial: string | null, key: string, value: string, expected: string] +> = [ + [ + 'updates existing variable with quotes', + 'API_KEY="old_value"\n', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"\n', + ], + [ + 'updates existing variable without quotes', + 'API_KEY=old_value\n', + 'API_KEY', + 'new_value', + 'API_KEY=new_value\n', + ], + ['adds new variable to empty file', '', 'NEW_KEY', 'new_value', 'NEW_KEY="new_value"'], + [ + 'adds new variable to existing file with content', + 'EXISTING_KEY="existing_value"', + 'NEW_KEY', + 'new_value', + 'EXISTING_KEY="existing_value"\nNEW_KEY="new_value"', + ], + ['creates file if it does not exist', null, 'NEW_KEY', 'new_value', 'NEW_KEY="new_value"'], + [ + 'preserves quote style for quoted variables', + 'API_KEY="old_value"', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"', + ], + [ + 'preserves quote style for unquoted variables', + 'API_KEY=old_value', + 'API_KEY', + 'new_value', + 'API_KEY=new_value', + ], +]; + +const formatting_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'preserves comments above variables', + '# This is a comment\nAPI_KEY="old_value"\n# Another comment', + 'API_KEY', + 'new_value', + '# This is a comment\nAPI_KEY="new_value"\n# Another comment', + ], + [ + 'preserves empty lines', + 'API_KEY="old_value"\n\nOTHER_KEY="other_value"', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"\n\nOTHER_KEY="other_value"', + ], + [ + 'handles file with trailing newline', + 'API_KEY="old_value"\n', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"\n', + ], + [ + 'handles file without trailing newline', + 'API_KEY="old_value"', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"', + ], +]; - await update_env_variable('NEW_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('NEW_KEY="new_value"'); - }); - - test('adds new variable to existing file with content', async () => { - const fs = create_mock_fs({ - '/test/.env': 'EXISTING_KEY="existing_value"', - }); - - await update_env_variable('NEW_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('EXISTING_KEY="existing_value"\nNEW_KEY="new_value"'); - }); - - test('creates file if it does not exist', async () => { - const fs = create_mock_fs({}); - - await update_env_variable('NEW_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('NEW_KEY="new_value"'); - }); - - test('preserves quote style for quoted variables', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"'); - }); - - test('preserves quote style for unquoted variables', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY=old_value', - }); +describe('update_env_variable - basic functionality', () => { + test.each(basic_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs(initial !== null ? {'/test/.env': initial} : {}); - await update_env_variable('API_KEY', 'new_value', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY=new_value'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); }); describe('update_env_variable - formatting preservation', () => { - test('preserves comments above variables', async () => { - const fs = create_mock_fs({ - '/test/.env': '# This is a comment\nAPI_KEY="old_value"\n# Another comment', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe( - '# This is a comment\nAPI_KEY="new_value"\n# Another comment', - ); - }); - - test('preserves empty lines', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"\n\nOTHER_KEY="other_value"', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"\n\nOTHER_KEY="other_value"'); - }); - - test('handles file with trailing newline', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"\n', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"\n'); - }); - - test('handles file without trailing newline', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); + test.each(formatting_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - await update_env_variable('API_KEY', 'new_value', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); }); @@ -199,13 +140,16 @@ describe('update_env_variable - error handling', () => { throw new Error(error_message); }; - await expect( - update_env_variable('API_KEY', 'new_value', { + try { + await update_env_variable('API_KEY', 'new_value', { env_file_path: '/test/.env', read_file: custom_read, - write_file: async () => {}, // eslint-disable-line @typescript-eslint/no-empty-function - }), - ).rejects.toThrow(error_message); + write_file: async () => {}, + }); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, error_message); + } }); test('propagates write file error', async () => { @@ -214,12 +158,15 @@ describe('update_env_variable - error handling', () => { throw new Error(error_message); }; - await expect( - update_env_variable('API_KEY', 'new_value', { + try { + await update_env_variable('API_KEY', 'new_value', { env_file_path: '/test/.env', read_file: async () => '', write_file: custom_write, - }), - ).rejects.toThrow(error_message); + }); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, error_message); + } }); }); diff --git a/src/test/server/env_file_helpers.comments.test.ts b/src/test/server/env_file_helpers.comments.test.ts index c4aa61dbd..fe6ef5083 100644 --- a/src/test/server/env_file_helpers.comments.test.ts +++ b/src/test/server/env_file_helpers.comments.test.ts @@ -1,11 +1,7 @@ -// @slop Claude Sonnet 4.5 - -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {update_env_variable} from '$lib/server/env_file_helpers.js'; -/* eslint-disable @typescript-eslint/require-await */ - const create_mock_fs = (initial_files: Record = {}) => { const files = {...initial_files}; return { @@ -28,204 +24,119 @@ const create_mock_fs = (initial_files: Record = {}) => { }; }; -describe('update_env_variable - inline comment preservation', () => { - test('preserves inline comment after quoted value', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value" # this is important', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value" # this is important'); - }); - - test('preserves inline comment after unquoted value', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY=old_value # comment here', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY=new_value # comment here'); - }); - - test('preserves inline comment with no space before hash', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"# no space comment', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"# no space comment'); - }); - - test('preserves inline comment with multiple spaces before hash', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value" # spaced comment', - }); +const comment_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'preserves inline comment after quoted value', + 'API_KEY="old_value" # this is important', + 'API_KEY', + 'new_value', + 'API_KEY="new_value" # this is important', + ], + [ + 'preserves inline comment after unquoted value', + 'API_KEY=old_value # comment here', + 'API_KEY', + 'new_value', + 'API_KEY=new_value # comment here', + ], + [ + 'preserves inline comment with no space before hash', + 'API_KEY="old_value"# no space comment', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"# no space comment', + ], + [ + 'preserves inline comment with multiple spaces before hash', + 'API_KEY="old_value" # spaced comment', + 'API_KEY', + 'new_value', + 'API_KEY="new_value" # spaced comment', + ], + [ + 'does not treat hash inside quoted value as comment', + 'API_KEY="value#with#hashes" # real comment', + 'API_KEY', + 'new_value', + 'API_KEY="new_value" # real comment', + ], + [ + 'treats hash in unquoted value as start of comment', + 'API_KEY=value#notacomment', + 'API_KEY', + 'new_value', + 'API_KEY=new_value#notacomment', + ], + [ + 'handles empty inline comment', + 'API_KEY="old_value" #', + 'API_KEY', + 'new_value', + 'API_KEY="new_value" #', + ], + [ + 'preserves inline comment with special characters', + 'API_KEY="old" # TODO: update this! @important', + 'API_KEY', + 'new', + 'API_KEY="new" # TODO: update this! @important', + ], + [ + 'handles single quotes with inline comment', + "API_KEY='old_value' # comment", + 'API_KEY', + 'new_value', + 'API_KEY="new_value" # comment', + ], + [ + 'does not add inline comment when original has none', + 'API_KEY="old_value"', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"', + ], + [ + 'preserves multiple hashes in comment', + 'API_KEY="old" # comment ## with ### hashes', + 'API_KEY', + 'new', + 'API_KEY="new" # comment ## with ### hashes', + ], + [ + 'preserves comment after escaped backslash at end of value', + 'API_KEY="test\\\\" # important comment', + 'API_KEY', + 'new', + 'API_KEY="new" # important comment', + ], + [ + 'preserves comment after single escaped backslash', + 'PATH="C:\\\\temp\\\\" # Windows path', + 'PATH', + 'D:\\\\new', + 'PATH="D:\\\\new" # Windows path', + ], + [ + 'handles escaped quote followed by more content (not a closing quote)', + 'MSG="Say \\"hello\\" please" # greeting', + 'MSG', + 'new message', + 'MSG="new message" # greeting', + ], +]; - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value" # spaced comment'); - }); - - test('does not treat hash inside quoted value as comment', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="value#with#hashes" # real comment', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value" # real comment'); - }); - - test('treats hash in unquoted value as start of comment', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY=value#notacomment', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // The #notacomment part should be preserved as comment - expect(fs.get_file('/test/.env')).toBe('API_KEY=new_value#notacomment'); - }); - - test('handles empty inline comment', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value" #', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value" #'); - }); - - test('preserves inline comment with special characters', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old" # TODO: update this! @important', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new" # TODO: update this! @important'); - }); - - test('handles single quotes with inline comment', async () => { - const fs = create_mock_fs({ - '/test/.env': "API_KEY='old_value' # comment", - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Should preserve quotes and comment - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value" # comment'); - }); - - test('does not add inline comment when original has none', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"'); - }); - - test('preserves multiple hashes in comment', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old" # comment ## with ### hashes', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new" # comment ## with ### hashes'); - }); - - test('preserves comment after escaped backslash at end of value', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="test\\\\" # important comment', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // The \\\\ is two backslashes (one escaped), then closing quote, then comment - expect(fs.get_file('/test/.env')).toBe('API_KEY="new" # important comment'); - }); - - test('preserves comment after single escaped backslash', async () => { - const fs = create_mock_fs({ - '/test/.env': 'PATH="C:\\\\temp\\\\" # Windows path', - }); - - await update_env_variable('PATH', 'D:\\\\new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('PATH="D:\\\\new" # Windows path'); - }); - - test('handles escaped quote followed by more content (not a closing quote)', async () => { - const fs = create_mock_fs({ - '/test/.env': 'MSG="Say \\"hello\\" please" # greeting', - }); +describe('update_env_variable - inline comment preservation', () => { + test.each(comment_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - await update_env_variable('MSG', 'new message', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - // The \" are escaped quotes, final " is closing quote - expect(fs.get_file('/test/.env')).toBe('MSG="new message" # greeting'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); }); diff --git a/src/test/server/env_file_helpers.duplicates.test.ts b/src/test/server/env_file_helpers.duplicates.test.ts index 2ac55d968..00d7d691f 100644 --- a/src/test/server/env_file_helpers.duplicates.test.ts +++ b/src/test/server/env_file_helpers.duplicates.test.ts @@ -1,11 +1,7 @@ -// @slop Claude Sonnet 4.5 - -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {update_env_variable} from '$lib/server/env_file_helpers.js'; -/* eslint-disable @typescript-eslint/require-await */ - const create_mock_fs = (initial_files: Record = {}) => { const files = {...initial_files}; return { @@ -28,172 +24,98 @@ const create_mock_fs = (initial_files: Record = {}) => { }; }; -describe('update_env_variable - duplicate keys (LAST wins behavior)', () => { - test('updates LAST occurrence when key appears twice', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="first_value"\nAPI_KEY="second_value"', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // First occurrence stays unchanged, second (last) is updated - expect(fs.get_file('/test/.env')).toBe('API_KEY="first_value"\nAPI_KEY="new_value"'); - }); - - test('updates LAST occurrence when key appears three times', async () => { - const fs = create_mock_fs({ - '/test/.env': 'KEY="first"\nKEY="second"\nKEY="third"', - }); - - await update_env_variable('KEY', 'updated', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - const result = fs.get_file('/test/.env'); - expect(result).toBe('KEY="first"\nKEY="second"\nKEY="updated"'); - - // Verify first two occurrences are unchanged - const lines = result?.split('\n') || []; - expect(lines[0]).toBe('KEY="first"'); - expect(lines[1]).toBe('KEY="second"'); - expect(lines[2]).toBe('KEY="updated"'); - }); - - test('matches dotenv behavior: last wins', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY=first_value\nAPI_KEY=second_value\nAPI_KEY=third_value', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // dotenv would use "third_value", so we update the third occurrence - const result = fs.get_file('/test/.env'); - expect(result).toBe('API_KEY=first_value\nAPI_KEY=second_value\nAPI_KEY=new_value'); - }); - - test('updates LAST occurrence with inline comments preserved', async () => { - const fs = create_mock_fs({ - '/test/.env': 'KEY="first" # dev\nKEY="second" # prod', - }); - - await update_env_variable('KEY', 'updated', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('KEY="first" # dev\nKEY="updated" # prod'); - }); - - test('updates LAST occurrence when duplicates have different quote styles', async () => { - const fs = create_mock_fs({ - '/test/.env': 'KEY=unquoted_first\nKEY="quoted_second"', - }); - - await update_env_variable('KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // First stays unquoted, second (last) is updated and stays quoted - expect(fs.get_file('/test/.env')).toBe('KEY=unquoted_first\nKEY="new"'); - }); +const duplicate_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'updates LAST occurrence when key appears twice', + 'API_KEY="first_value"\nAPI_KEY="second_value"', + 'API_KEY', + 'new_value', + 'API_KEY="first_value"\nAPI_KEY="new_value"', + ], + [ + 'updates LAST occurrence when key appears three times', + 'KEY="first"\nKEY="second"\nKEY="third"', + 'KEY', + 'updated', + 'KEY="first"\nKEY="second"\nKEY="updated"', + ], + [ + 'matches dotenv behavior: last wins', + 'API_KEY=first_value\nAPI_KEY=second_value\nAPI_KEY=third_value', + 'API_KEY', + 'new_value', + 'API_KEY=first_value\nAPI_KEY=second_value\nAPI_KEY=new_value', + ], + [ + 'updates LAST occurrence with inline comments preserved', + 'KEY="first" # dev\nKEY="second" # prod', + 'KEY', + 'updated', + 'KEY="first" # dev\nKEY="updated" # prod', + ], + [ + 'updates LAST occurrence when duplicates have different quote styles', + 'KEY=unquoted_first\nKEY="quoted_second"', + 'KEY', + 'new', + 'KEY=unquoted_first\nKEY="new"', + ], + [ + 'updates LAST occurrence when separated by other keys', + 'API_KEY="first"\nOTHER_KEY="value"\nAPI_KEY="second"', + 'API_KEY', + 'new', + 'API_KEY="first"\nOTHER_KEY="value"\nAPI_KEY="new"', + ], + [ + 'updates LAST occurrence when separated by comments', + 'API_KEY="first"\n# Comment\nAPI_KEY="second"', + 'API_KEY', + 'new', + 'API_KEY="first"\n# Comment\nAPI_KEY="new"', + ], + [ + 'updates LAST occurrence when separated by empty lines', + 'API_KEY="first"\n\nAPI_KEY="second"', + 'API_KEY', + 'new', + 'API_KEY="first"\n\nAPI_KEY="new"', + ], + [ + 'handles keys that are substrings of each other', + 'KEY="value1"\nSECRET_KEY="value2"', + 'KEY', + 'new_value', + 'KEY="new_value"\nSECRET_KEY="value2"', + ], + [ + 'handles keys that are prefixes of each other', + 'API_KEY="value1"\nAPI_KEY_SECRET="value2"', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"\nAPI_KEY_SECRET="value2"', + ], + [ + 'does not match keys in comments', + '# API_KEY="commented"\nAPI_KEY="actual_value"', + 'API_KEY', + 'new_value', + '# API_KEY="commented"\nAPI_KEY="new_value"', + ], +]; - test('updates LAST occurrence when separated by other keys', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="first"\nOTHER_KEY="value"\nAPI_KEY="second"', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="first"\nOTHER_KEY="value"\nAPI_KEY="new"'); - }); - - test('updates LAST occurrence when separated by comments', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="first"\n# Comment\nAPI_KEY="second"', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="first"\n# Comment\nAPI_KEY="new"'); - }); - - test('updates LAST occurrence when separated by empty lines', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="first"\n\nAPI_KEY="second"', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="first"\n\nAPI_KEY="new"'); - }); - - test('handles keys that are substrings of each other', async () => { - const fs = create_mock_fs({ - '/test/.env': 'KEY="value1"\nSECRET_KEY="value2"', - }); - - await update_env_variable('KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Should only update KEY, not SECRET_KEY - expect(fs.get_file('/test/.env')).toBe('KEY="new_value"\nSECRET_KEY="value2"'); - }); - - test('handles keys that are prefixes of each other', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="value1"\nAPI_KEY_SECRET="value2"', - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Should only update API_KEY, not API_KEY_SECRET - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"\nAPI_KEY_SECRET="value2"'); - }); - - test('does not match keys in comments', async () => { - const fs = create_mock_fs({ - '/test/.env': '# API_KEY="commented"\nAPI_KEY="actual_value"', - }); +describe('update_env_variable - duplicate keys (LAST wins behavior)', () => { + test.each(duplicate_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - await update_env_variable('API_KEY', 'new_value', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - // Comment line should be unchanged - expect(fs.get_file('/test/.env')).toBe('# API_KEY="commented"\nAPI_KEY="new_value"'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); }); diff --git a/src/test/server/env_file_helpers.edge_cases.test.ts b/src/test/server/env_file_helpers.edge_cases.test.ts index bac58a395..e4a99d13e 100644 --- a/src/test/server/env_file_helpers.edge_cases.test.ts +++ b/src/test/server/env_file_helpers.edge_cases.test.ts @@ -1,11 +1,7 @@ -// @slop Claude Sonnet 4.5 - -import {test, expect, describe} from 'vitest'; +import {test, describe, assert} from 'vitest'; import {update_env_variable} from '$lib/server/env_file_helpers.js'; -/* eslint-disable @typescript-eslint/require-await */ - const create_mock_fs = (initial_files: Record = {}) => { const files = {...initial_files}; return { @@ -28,114 +24,77 @@ const create_mock_fs = (initial_files: Record = {}) => { }; }; -describe('update_env_variable - quote detection edge cases', () => { - test('does not add quotes when original value contains quotes but assignment does not', async () => { - const fs = create_mock_fs({ - '/test/.env': "NAME=O'Brien", - }); - - await update_env_variable('NAME', 'Smith', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Should preserve unquoted style - expect(fs.get_file('/test/.env')).toBe('NAME=Smith'); - }); - - test('handles value with internal quotes when quoted', async () => { - const fs = create_mock_fs({ - '/test/.env': 'NAME="O\'Brien"', - }); - - await update_env_variable('NAME', 'Smith', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Should preserve quoted style - expect(fs.get_file('/test/.env')).toBe('NAME="Smith"'); - }); - - test('handles single quote style', async () => { - const fs = create_mock_fs({ - '/test/.env': "API_KEY='old_value'", - }); - - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Converts to double quotes (implementation detail) - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"'); - }); - - test('handles escaped quotes in value', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="value with \\" escaped quotes"', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new"'); - }); - - test('handles escaped quote at end of value', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="test\\\\"', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new"'); - }); +const quote_detection_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'does not add quotes when original value contains quotes but assignment does not', + "NAME=O'Brien", + 'NAME', + 'Smith', + 'NAME=Smith', + ], + [ + 'handles value with internal quotes when quoted', + 'NAME="O\'Brien"', + 'NAME', + 'Smith', + 'NAME="Smith"', + ], + [ + 'handles single quote style', + "API_KEY='old_value'", + 'API_KEY', + 'new_value', + 'API_KEY="new_value"', + ], + [ + 'handles escaped quotes in value', + 'API_KEY="value with \\" escaped quotes"', + 'API_KEY', + 'new', + 'API_KEY="new"', + ], + [ + 'handles escaped quote at end of value', + 'API_KEY="test\\\\"', + 'API_KEY', + 'new', + 'API_KEY="new"', + ], + [ + 'handles multiple escaped quotes in sequence', + 'API_KEY="test\\\\\\"value"', + 'API_KEY', + 'new', + 'API_KEY="new"', + ], + [ + 'handles escaped quote with inline comment', + 'API_KEY="test\\" quote" # comment', + 'API_KEY', + 'new', + 'API_KEY="new" # comment', + ], +]; - test('handles multiple escaped quotes in sequence', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="test\\\\\\"value"', - }); - - await update_env_variable('API_KEY', 'new', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY="new"'); - }); - - test('handles escaped quote with inline comment', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="test\\" quote" # comment', - }); +describe('update_env_variable - quote detection edge cases', () => { + test.each(quote_detection_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - await update_env_variable('API_KEY', 'new', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY="new" # comment'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); }); describe('update_env_variable - special values', () => { test('handles empty value', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); + const fs = create_mock_fs({'/test/.env': 'API_KEY="old_value"'}); await update_env_variable('API_KEY', '', { env_file_path: '/test/.env', @@ -143,13 +102,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY=""'); + assert.strictEqual(fs.get_file('/test/.env'), 'API_KEY=""'); }); test('handles value with equals sign', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); + const fs = create_mock_fs({'/test/.env': 'API_KEY="old_value"'}); await update_env_variable('API_KEY', 'value=with=equals', { env_file_path: '/test/.env', @@ -157,13 +114,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY="value=with=equals"'); + assert.strictEqual(fs.get_file('/test/.env'), 'API_KEY="value=with=equals"'); }); test('handles value with newlines', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); + const fs = create_mock_fs({'/test/.env': 'API_KEY="old_value"'}); await update_env_variable('API_KEY', 'value\nwith\nnewlines', { env_file_path: '/test/.env', @@ -171,13 +126,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY="value\nwith\nnewlines"'); + assert.strictEqual(fs.get_file('/test/.env'), 'API_KEY="value\nwith\nnewlines"'); }); test('handles value with backslashes (Windows paths)', async () => { - const fs = create_mock_fs({ - '/test/.env': 'PATH_KEY="old_path"', - }); + const fs = create_mock_fs({'/test/.env': 'PATH_KEY="old_path"'}); await update_env_variable('PATH_KEY', 'C:\\Users\\Admin\\Documents', { env_file_path: '/test/.env', @@ -185,13 +138,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('PATH_KEY="C:\\Users\\Admin\\Documents"'); + assert.strictEqual(fs.get_file('/test/.env'), 'PATH_KEY="C:\\Users\\Admin\\Documents"'); }); test('handles value with unicode characters', async () => { - const fs = create_mock_fs({ - '/test/.env': 'UNICODE_KEY="old"', - }); + const fs = create_mock_fs({'/test/.env': 'UNICODE_KEY="old"'}); const unicode_value = '你好世界 🌍 Привет мир'; await update_env_variable('UNICODE_KEY', unicode_value, { @@ -200,13 +151,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe(`UNICODE_KEY="${unicode_value}"`); + assert.strictEqual(fs.get_file('/test/.env'), `UNICODE_KEY="${unicode_value}"`); }); test('handles very long values', async () => { - const fs = create_mock_fs({ - '/test/.env': 'LONG_KEY="short"', - }); + const fs = create_mock_fs({'/test/.env': 'LONG_KEY="short"'}); const long_value = 'x'.repeat(10000); await update_env_variable('LONG_KEY', long_value, { @@ -215,13 +164,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe(`LONG_KEY="${long_value}"`); + assert.strictEqual(fs.get_file('/test/.env'), `LONG_KEY="${long_value}"`); }); test('handles value with JSON content', async () => { - const fs = create_mock_fs({ - '/test/.env': 'JSON_KEY="old"', - }); + const fs = create_mock_fs({'/test/.env': 'JSON_KEY="old"'}); const json_value = '{"name":"test","nested":{"key":"value"},"array":[1,2,3]}'; await update_env_variable('JSON_KEY', json_value, { @@ -230,13 +177,11 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe(`JSON_KEY="${json_value}"`); + assert.strictEqual(fs.get_file('/test/.env'), `JSON_KEY="${json_value}"`); }); test('handles value with special characters', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY="old_value"', - }); + const fs = create_mock_fs({'/test/.env': 'API_KEY="old_value"'}); await update_env_variable('API_KEY', 'value!@#$%^&*()_+-=[]{}|;:,.<>?', { env_file_path: '/test/.env', @@ -244,53 +189,47 @@ describe('update_env_variable - special values', () => { write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('API_KEY="value!@#$%^&*()_+-=[]{}|;:,.<>?"'); + assert.strictEqual(fs.get_file('/test/.env'), 'API_KEY="value!@#$%^&*()_+-=[]{}|;:,.<>?"'); }); }); -describe('update_env_variable - whitespace handling', () => { - test('handles key with spaces around equals sign', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY = "old_value"', - }); +const whitespace_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'handles key with spaces around equals sign', + 'API_KEY = "old_value"', + 'API_KEY', + 'new_value', + 'API_KEY="new_value"', + ], + [ + 'handles key with leading whitespace in file', + ' LEADING_SPACE="old_value"', + 'LEADING_SPACE', + 'new_value', + 'LEADING_SPACE="new_value"', + ], + [ + 'handles key with trailing whitespace before equals', + 'TRAILING_SPACE ="old_value"', + 'TRAILING_SPACE', + 'new_value', + 'TRAILING_SPACE="new_value"', + ], +]; - await update_env_variable('API_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Normalizes to no spaces - expect(fs.get_file('/test/.env')).toBe('API_KEY="new_value"'); - }); - - test('handles key with leading whitespace in file', async () => { - const fs = create_mock_fs({ - '/test/.env': ' LEADING_SPACE="old_value"', - }); - - await update_env_variable('LEADING_SPACE', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - // Normalizes whitespace - expect(fs.get_file('/test/.env')).toBe('LEADING_SPACE="new_value"'); - }); - - test('handles key with trailing whitespace before equals', async () => { - const fs = create_mock_fs({ - '/test/.env': 'TRAILING_SPACE ="old_value"', - }); +describe('update_env_variable - whitespace handling', () => { + test.each(whitespace_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - await update_env_variable('TRAILING_SPACE', 'new_value', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('TRAILING_SPACE="new_value"'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); test('preserves exact original formatting for non-matching lines', async () => { @@ -305,90 +244,88 @@ describe('update_env_variable - whitespace handling', () => { }); const result = fs.get_file('/test/.env'); - expect(result).toBe( + assert.strictEqual( + result, ' INDENT_KEY = "spaced" \nTARGET_KEY="new"\n\t\tTAB_KEY\t=\t"tabbed"\t', ); // Verify exact preservation of unchanged lines const lines = result?.split('\n') || []; - expect(lines[0]).toBe(' INDENT_KEY = "spaced" '); - expect(lines[2]).toBe('\t\tTAB_KEY\t=\t"tabbed"\t'); + assert.strictEqual(lines[0], ' INDENT_KEY = "spaced" '); + assert.strictEqual(lines[2], '\t\tTAB_KEY\t=\t"tabbed"\t'); }); }); -describe('update_env_variable - special keys', () => { - test('handles key with underscores and numbers', async () => { - const fs = create_mock_fs({ - '/test/.env': 'API_KEY_123="old_value"', - }); - - await update_env_variable('API_KEY_123', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('API_KEY_123="new_value"'); - }); - - test('handles key with dots (regex special char)', async () => { - const fs = create_mock_fs({ - '/test/.env': 'NORMAL_KEY="value1"\nSPECIAL.KEY="value2"', - }); +const special_key_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'handles key with underscores and numbers', + 'API_KEY_123="old_value"', + 'API_KEY_123', + 'new_value', + 'API_KEY_123="new_value"', + ], + [ + 'handles key with dots (regex special char)', + 'NORMAL_KEY="value1"\nSPECIAL.KEY="value2"', + 'SPECIAL.KEY', + 'new_value', + 'NORMAL_KEY="value1"\nSPECIAL.KEY="new_value"', + ], + [ + 'handles empty key name', + 'VALID_KEY="value"', + '', + 'empty_key_value', + 'VALID_KEY="value"\n="empty_key_value"', + ], +]; - await update_env_variable('SPECIAL.KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('NORMAL_KEY="value1"\nSPECIAL.KEY="new_value"'); - }); - - test('handles empty key name', async () => { - const fs = create_mock_fs({ - '/test/.env': 'VALID_KEY="value"', - }); +describe('update_env_variable - special keys', () => { + test.each(special_key_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - // Empty key edge case - await update_env_variable('', 'empty_key_value', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - expect(fs.get_file('/test/.env')).toBe('VALID_KEY="value"\n="empty_key_value"'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); }); -describe('update_env_variable - file variations', () => { - test('handles file with only comments', async () => { - const fs = create_mock_fs({ - '/test/.env': '# Comment 1\n# Comment 2', - }); +const file_variation_cases: Array< + [label: string, initial: string, key: string, value: string, expected: string] +> = [ + [ + 'handles file with only comments', + '# Comment 1\n# Comment 2', + 'NEW_KEY', + 'new_value', + '# Comment 1\n# Comment 2\nNEW_KEY="new_value"', + ], + [ + 'handles file with only empty lines', + '\n\n\n', + 'NEW_KEY', + 'new_value', + '\n\n\n\nNEW_KEY="new_value"', + ], +]; - await update_env_variable('NEW_KEY', 'new_value', { - env_file_path: '/test/.env', - read_file: fs.read_file, - write_file: fs.write_file, - }); - - expect(fs.get_file('/test/.env')).toBe('# Comment 1\n# Comment 2\nNEW_KEY="new_value"'); - }); - - test('handles file with only empty lines', async () => { - const fs = create_mock_fs({ - '/test/.env': '\n\n\n', - }); +describe('update_env_variable - file variations', () => { + test.each(file_variation_cases)('%s', async (_label, initial, key, value, expected) => { + const fs = create_mock_fs({'/test/.env': initial}); - await update_env_variable('NEW_KEY', 'new_value', { + await update_env_variable(key, value, { env_file_path: '/test/.env', read_file: fs.read_file, write_file: fs.write_file, }); - // File ends with newline, so blank line separator is added - expect(fs.get_file('/test/.env')).toBe('\n\n\n\nNEW_KEY="new_value"'); + assert.strictEqual(fs.get_file('/test/.env'), expected); }); test('verifies path is resolved to absolute', async () => { @@ -403,8 +340,8 @@ describe('update_env_variable - file variations', () => { }); // Path should be absolute - expect(resolved_path).toBeDefined(); - expect(resolved_path?.startsWith('/')).toBe(true); - expect(resolved_path?.endsWith('relative/.env')).toBe(true); + assert.ok(resolved_path); + assert.ok(resolved_path.startsWith('/')); + assert.ok(resolved_path.endsWith('relative/.env')); }); }); diff --git a/src/test/server/scoped_fs_advanced.test.ts b/src/test/server/scoped_fs_advanced.test.ts index 67e710b8d..59bda6788 100644 --- a/src/test/server/scoped_fs_advanced.test.ts +++ b/src/test/server/scoped_fs_advanced.test.ts @@ -1,13 +1,9 @@ -// @slop Claude Sonnet 3.7 - -import {test, expect, vi, beforeEach, afterEach, describe} from 'vitest'; +import {test, vi, beforeEach, afterEach, describe, assert} from 'vitest'; import * as fs from 'node:fs/promises'; import * as fs_sync from 'node:fs'; import {ScopedFs, SymlinkNotAllowedError} from '$lib/server/scoped_fs.js'; -/* eslint-disable @typescript-eslint/require-await, @typescript-eslint/no-empty-function, no-await-in-loop */ - // Mock fs/promises and fs modules vi.mock('node:fs/promises', () => ({ readFile: vi.fn(), @@ -133,11 +129,11 @@ describe('ScopedFs - constructor edge cases', () => { const scoped_fs = new ScopedFs(paths_with_empty); // Should only have 2 allowed paths (empty strings filtered) - expect(scoped_fs.allowed_paths.length).toBe(2); + assert.strictEqual(scoped_fs.allowed_paths.length, 2); // Valid paths should be allowed - expect(scoped_fs.is_path_allowed('/valid/path/file.txt')).toBe(true); - expect(scoped_fs.is_path_allowed('/another/path/file.txt')).toBe(true); + assert.ok(scoped_fs.is_path_allowed('/valid/path/file.txt')); + assert.ok(scoped_fs.is_path_allowed('/another/path/file.txt')); }); test('should filter out null and undefined values from allowed paths', () => { @@ -145,7 +141,7 @@ describe('ScopedFs - constructor edge cases', () => { const scoped_fs = new ScopedFs(paths_with_nullish); // Should only have 2 allowed paths (nullish values filtered) - expect(scoped_fs.allowed_paths.length).toBe(2); + assert.strictEqual(scoped_fs.allowed_paths.length, 2); }); test('should handle array with only empty/falsy values', () => { @@ -153,10 +149,10 @@ describe('ScopedFs - constructor edge cases', () => { const scoped_fs = new ScopedFs(empty_array); // Should have no allowed paths - expect(scoped_fs.allowed_paths.length).toBe(0); + assert.strictEqual(scoped_fs.allowed_paths.length, 0); // No paths should be allowed - expect(scoped_fs.is_path_allowed('/any/path')).toBe(false); + assert.ok(!scoped_fs.is_path_allowed('/any/path')); }); }); @@ -176,7 +172,7 @@ describe('ScopedFs - advanced path validation', () => { // All should be allowed for (const path of special_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); } }); @@ -192,7 +188,7 @@ describe('ScopedFs - advanced path validation', () => { ]; for (const path of paths_with_multiple_slashes) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); } }); @@ -217,14 +213,14 @@ describe('ScopedFs - advanced path validation', () => { // Test valid paths for (const path of valid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); - expect(await scoped_fs.is_path_safe(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); + assert.ok(await scoped_fs.is_path_safe(path)); } // Test invalid paths for (const path of invalid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); - expect(await scoped_fs.is_path_safe(path)).toBe(false); + assert.ok(!scoped_fs.is_path_allowed(path)); + assert.ok(!(await scoped_fs.is_path_safe(path))); } }); }); @@ -269,7 +265,7 @@ describe('ScopedFs - advanced directory operations', () => { } await scoped_fs.readdir(DIR_PATHS.ALLOWED, option as any); - expect(fs.readdir).toHaveBeenCalledWith(DIR_PATHS.ALLOWED, option); + assert.deepEqual(vi.mocked(fs.readdir).mock.calls[0], [DIR_PATHS.ALLOWED, option] as any); } }); @@ -278,17 +274,22 @@ describe('ScopedFs - advanced directory operations', () => { // Test creating a deeply nested directory await scoped_fs.mkdir(DIR_PATHS.NESTED, {recursive: true}); - expect(fs.mkdir).toHaveBeenCalledWith(DIR_PATHS.NESTED, {recursive: true}); + assert.deepEqual(vi.mocked(fs.mkdir).mock.calls[0], [DIR_PATHS.NESTED, {recursive: true}]); // Without recursive flag, it should still try to create the directory await scoped_fs.mkdir(DIR_PATHS.NEW_DIR); - expect(fs.mkdir).toHaveBeenCalledWith(DIR_PATHS.NEW_DIR, undefined); + assert.deepEqual(vi.mocked(fs.mkdir).mock.calls[1], [DIR_PATHS.NEW_DIR, undefined]); // Should properly bubble up errors from fs.mkdir const error = new Error('EEXIST: directory already exists'); vi.mocked(fs.mkdir).mockRejectedValueOnce(error); - await expect(scoped_fs.mkdir(DIR_PATHS.ALLOWED)).rejects.toThrow(error); + try { + await scoped_fs.mkdir(DIR_PATHS.ALLOWED); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.strictEqual(e, error); + } }); test('rm - should handle various removal options', async () => { @@ -307,7 +308,7 @@ describe('ScopedFs - advanced directory operations', () => { vi.mocked(fs.rm).mockResolvedValueOnce(); await scoped_fs.rm(DIR_PATHS.ALLOWED, options); - expect(fs.rm).toHaveBeenCalledWith(DIR_PATHS.ALLOWED, options); + assert.deepEqual(vi.mocked(fs.rm).mock.calls[0], [DIR_PATHS.ALLOWED, options]); } }); }); @@ -357,8 +358,13 @@ describe('ScopedFs - advanced security features', () => { }); // Each case should be rejected with a SymlinkNotAllowedError - await expect(scoped_fs.read_file(path)).rejects.toThrow(SymlinkNotAllowedError); - expect(fs.readFile).not.toHaveBeenCalled(); + try { + await scoped_fs.read_file(path); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); } }); @@ -374,8 +380,13 @@ describe('ScopedFs - advanced security features', () => { ]; for (const path of tricky_traversal_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); - await expect(scoped_fs.read_file(path)).rejects.toThrow('Path is not allowed'); + assert.ok(!scoped_fs.is_path_allowed(path)); + try { + await scoped_fs.read_file(path); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } } }); @@ -393,8 +404,8 @@ describe('ScopedFs - advanced security features', () => { // Should detect symlink and return false without calling access const exists = await scoped_fs.exists('/allowed/path/evil-symlink'); - expect(exists).toBe(false); - expect(fs.access).not.toHaveBeenCalled(); + assert.ok(!exists); + assert.strictEqual(vi.mocked(fs.access).mock.calls.length, 0); }); }); @@ -406,10 +417,13 @@ describe('ScopedFs - error handling and edge cases', () => { vi.mocked(fs.lstat).mockRejectedValue(new Error('Unknown filesystem error')); // The error during symlink check should be caught and rethrown - await expect(scoped_fs.read_file(FILE_PATHS.ALLOWED)).rejects.toThrow( - 'Unknown filesystem error', - ); - expect(fs.readFile).not.toHaveBeenCalled(); + try { + await scoped_fs.read_file(FILE_PATHS.ALLOWED); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Unknown filesystem error'); + } + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); test('should ignore ENOENT errors when checking target path', async () => { @@ -425,7 +439,11 @@ describe('ScopedFs - error handling and edge cases', () => { // Should proceed despite ENOENT - file may not exist yet await scoped_fs.write_file(FILE_PATHS.NONEXISTENT, 'new content'); - expect(fs.writeFile).toHaveBeenCalledWith(FILE_PATHS.NONEXISTENT, 'new content', 'utf8'); + assert.deepEqual(vi.mocked(fs.writeFile).mock.calls[0], [ + FILE_PATHS.NONEXISTENT, + 'new content', + 'utf8', + ]); }); test('should ignore ENOENT errors when checking parent directories', async () => { @@ -454,7 +472,7 @@ describe('ScopedFs - error handling and edge cases', () => { // Should proceed despite parent directories not existing await scoped_fs.write_file(deep_path, 'content'); - expect(fs.writeFile).toHaveBeenCalledWith(deep_path, 'content', 'utf8'); + assert.deepEqual(vi.mocked(fs.writeFile).mock.calls[0], [deep_path, 'content', 'utf8']); }); test('should NOT ignore non-ENOENT errors when checking paths', async () => { @@ -466,8 +484,13 @@ describe('ScopedFs - error handling and edge cases', () => { vi.mocked(fs.lstat).mockRejectedValueOnce(eacces_error); // Should throw the EACCES error, not ignore it - await expect(scoped_fs.read_file(FILE_PATHS.ALLOWED)).rejects.toThrow('EACCES'); - expect(fs.readFile).not.toHaveBeenCalled(); + try { + await scoped_fs.read_file(FILE_PATHS.ALLOWED); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'EACCES'); + } + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); test('should NOT ignore non-ENOENT errors when checking parent directories', async () => { @@ -487,8 +510,13 @@ describe('ScopedFs - error handling and edge cases', () => { vi.mocked(fs.lstat).mockRejectedValueOnce(eacces_error); // Should throw the EACCES error from parent check - await expect(scoped_fs.read_file(nested_path)).rejects.toThrow('EACCES'); - expect(fs.readFile).not.toHaveBeenCalled(); + try { + await scoped_fs.read_file(nested_path); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'EACCES'); + } + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); test('should handle a variety of filesystem errors from underlying operations', async () => { @@ -521,8 +549,13 @@ describe('ScopedFs - error handling and edge cases', () => { vi.mocked(fs.readFile).mockRejectedValueOnce(error); // Error should be passed through - await expect(scoped_fs.read_file(FILE_PATHS.ALLOWED)).rejects.toThrow(message); - expect(fs.readFile).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, 'utf8'); + try { + await scoped_fs.read_file(FILE_PATHS.ALLOWED); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, message); + } + assert.deepEqual(vi.mocked(fs.readFile).mock.calls[0], [FILE_PATHS.ALLOWED, 'utf8']); } }); @@ -536,13 +569,17 @@ describe('ScopedFs - error handling and edge cases', () => { }); // The path itself is allowed since it's under an allowed directory - expect(scoped_fs.is_path_allowed(deep_nonexistent_path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(deep_nonexistent_path)); // Write operation should be allowed after path validation vi.mocked(fs.writeFile).mockResolvedValueOnce(); await scoped_fs.write_file(deep_nonexistent_path, 'content'); - expect(fs.writeFile).toHaveBeenCalledWith(deep_nonexistent_path, 'content', 'utf8'); + assert.deepEqual(vi.mocked(fs.writeFile).mock.calls[0], [ + deep_nonexistent_path, + 'content', + 'utf8', + ]); }); test('should handle extreme edge cases gracefully', async () => { @@ -556,14 +593,14 @@ describe('ScopedFs - error handling and edge cases', () => { ]; for (const path of edge_case_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); // Mock a successful read to test the full flow vi.mocked(fs.readFile).mockReset(); vi.mocked(fs.readFile).mockResolvedValueOnce('content' as any); const content = await scoped_fs.read_file(path); - expect(content).toBe('content'); + assert.strictEqual(content, 'content'); } }); }); @@ -591,12 +628,16 @@ describe('ScopedFs - advanced use cases', () => { await scoped_fs.rm(source_file); // Verify all operations happened with correct parameters - expect(content).toBe('file content'); - expect(fs.mkdir).toHaveBeenCalledWith(workflow_dir, undefined); - expect(fs.writeFile).toHaveBeenCalledWith(source_file, 'original content', 'utf8'); - expect(fs.readFile).toHaveBeenCalledWith(source_file, 'utf8'); - expect(fs.copyFile).toHaveBeenCalledWith(source_file, dest_file, undefined); - expect(fs.rm).toHaveBeenCalledWith(source_file, undefined); + assert.strictEqual(content, 'file content'); + assert.deepEqual(vi.mocked(fs.mkdir).mock.calls[0], [workflow_dir, undefined]); + assert.deepEqual(vi.mocked(fs.writeFile).mock.calls[0], [ + source_file, + 'original content', + 'utf8', + ]); + assert.deepEqual(vi.mocked(fs.readFile).mock.calls[0], [source_file, 'utf8']); + assert.deepEqual(vi.mocked(fs.copyFile).mock.calls[0], [source_file, dest_file, undefined]); + assert.deepEqual(vi.mocked(fs.rm).mock.calls[0], [source_file, undefined]); }); test('should handle concurrent operations correctly', async () => { @@ -621,12 +662,15 @@ describe('ScopedFs - advanced use cases', () => { ]); // Verify results - expect(result1).toBe('content1'); - expect(result2).toBe('content2'); - expect(fs.readFile).toHaveBeenCalledTimes(2); - expect(fs.writeFile).toHaveBeenCalledTimes(2); - expect(fs.writeFile).toHaveBeenCalledWith('/allowed/path/output1.txt', 'data1', 'utf8'); - expect(fs.writeFile).toHaveBeenCalledWith('/allowed/path/output2.txt', 'data2', 'utf8'); + assert.strictEqual(result1, 'content1'); + assert.strictEqual(result2, 'content2'); + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 2); + assert.strictEqual(vi.mocked(fs.writeFile).mock.calls.length, 2); + // Check that both write calls happened (order may vary with concurrent ops) + const write_calls = vi.mocked(fs.writeFile).mock.calls; + const write_paths = write_calls.map((c) => c[0]); + assert.ok(write_paths.includes('/allowed/path/output1.txt')); + assert.ok(write_paths.includes('/allowed/path/output2.txt')); }); test('should handle sequential operations that build on each other', async () => { @@ -666,10 +710,10 @@ describe('ScopedFs - advanced use cases', () => { await scoped_fs.rm(base_dir, {recursive: true}); // Verify everything worked as expected - expect(contents).toEqual(['content1', 'content2']); - expect(fs.mkdir).toHaveBeenCalledWith(base_dir, undefined); - expect(fs.readdir).toHaveBeenCalledWith(base_dir, undefined); - expect(fs.rm).toHaveBeenCalledWith(base_dir, {recursive: true}); + assert.deepEqual(contents, ['content1', 'content2']); + assert.deepEqual(vi.mocked(fs.mkdir).mock.calls[0], [base_dir, undefined] as any); + assert.deepEqual(vi.mocked(fs.readdir).mock.calls[0], [base_dir, undefined] as any); + assert.deepEqual(vi.mocked(fs.rm).mock.calls[0], [base_dir, {recursive: true}] as any); }); }); @@ -681,11 +725,11 @@ describe('ScopedFs - directory path trailing slash handling', () => { // All paths should have trailing slashes internally for (const path of scoped_fs.allowed_paths) { - expect(path.endsWith('/')).toBe(true); + assert.ok(path.endsWith('/')); } // Original array should be unmodified - expect(paths_with_mix).toEqual(['/path1', '/path2/', '/path3/subdir', '/path4/subdir/']); + assert.deepEqual(paths_with_mix, ['/path1', '/path2/', '/path3/subdir', '/path4/subdir/']); }); test('should correctly validate paths regardless of trailing slashes', () => { @@ -703,7 +747,7 @@ describe('ScopedFs - directory path trailing slash handling', () => { ]; for (const path of valid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); } // These paths should all be rejected @@ -715,7 +759,7 @@ describe('ScopedFs - directory path trailing slash handling', () => { ]; for (const path of invalid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); + assert.ok(!scoped_fs.is_path_allowed(path)); } }); @@ -742,12 +786,12 @@ describe('ScopedFs - directory path trailing slash handling', () => { // Read operations should work with all variations await scoped_fs.read_file(path); - expect(fs.readFile).toHaveBeenCalledWith(path, 'utf8'); + assert.deepEqual(vi.mocked(fs.readFile).mock.calls[0], [path, 'utf8']); // Write operations should also work vi.mocked(fs.writeFile).mockClear(); await scoped_fs.write_file(path, 'content'); - expect(fs.writeFile).toHaveBeenCalledWith(path, 'content', 'utf8'); + assert.deepEqual(vi.mocked(fs.writeFile).mock.calls[0], [path, 'content', 'utf8']); } }); @@ -776,13 +820,18 @@ describe('ScopedFs - directory path trailing slash handling', () => { // Test valid paths for (const path of valid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); } // Test invalid paths for (const path of invalid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); - await expect(scoped_fs.read_file(path)).rejects.toThrow('Path is not allowed'); + assert.ok(!scoped_fs.is_path_allowed(path)); + try { + await scoped_fs.read_file(path); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } } }); @@ -794,14 +843,14 @@ describe('ScopedFs - directory path trailing slash handling', () => { const test_paths = ['/', '/etc', '/usr/bin', '/home/user/file.txt']; for (const path of test_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); // Mock successful read vi.mocked(fs.readFile).mockReset(); vi.mocked(fs.readFile).mockResolvedValueOnce('content' as any); const content = await scoped_fs.read_file(path); - expect(content).toBe('content'); + assert.strictEqual(content, 'content'); } }); }); diff --git a/src/test/server/scoped_fs_basic.test.ts b/src/test/server/scoped_fs_basic.test.ts index c3127abbd..b77f46a24 100644 --- a/src/test/server/scoped_fs_basic.test.ts +++ b/src/test/server/scoped_fs_basic.test.ts @@ -1,13 +1,9 @@ -// @slop Claude Sonnet 3.7 - -import {test, expect, vi, beforeEach, afterEach, describe} from 'vitest'; +import {test, vi, beforeEach, afterEach, describe, assert} from 'vitest'; import * as fs from 'node:fs/promises'; import * as fs_sync from 'node:fs'; import {ScopedFs, SymlinkNotAllowedError} from '$lib/server/scoped_fs.js'; -/* eslint-disable no-await-in-loop, @typescript-eslint/no-empty-function */ - // Mock fs/promises and fs modules vi.mock('node:fs/promises', () => ({ readFile: vi.fn(), @@ -68,7 +64,7 @@ afterEach(() => { describe('ScopedFs - construction and initialization', () => { test('constructor - should accept an array of allowed paths', () => { const scoped_fs = create_test_instance(); - expect(scoped_fs).toBeInstanceOf(ScopedFs); + assert.instanceOf(scoped_fs, ScopedFs); }); test('constructor - should make a defensive copy of allowed paths', () => { @@ -79,96 +75,73 @@ describe('ScopedFs - construction and initialization', () => { original_paths.push('/new/path'); // The instance should still only allow the original paths - expect(scoped_fs.is_path_allowed('/new/path')).toBe(false); + assert.ok(!scoped_fs.is_path_allowed('/new/path')); }); test('constructor - should throw for invalid paths', () => { // Non-absolute path - expect(() => new ScopedFs(['relative/path'])).toThrow(); + assert.throws(() => new ScopedFs(['relative/path'])); // Empty path array should work but won't allow any paths const empty_scoped_fs = new ScopedFs([]); - expect(empty_scoped_fs.is_path_allowed('/any/path')).toBe(false); + assert.ok(!empty_scoped_fs.is_path_allowed('/any/path')); }); }); -describe('ScopedFs - path validation', () => { - test('is_path_allowed - should return true for paths within allowed directories', () => { - const scoped_fs = create_test_instance(); - - const valid_paths = [ - ...TEST_ALLOWED_PATHS, - FILE_PATHS.ALLOWED, - FILE_PATHS.NESTED, - '/allowed/path/subdir/', - ]; - - for (const path of valid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); - } - }); - - test('is_path_allowed - should return false for paths outside allowed directories', () => { - const scoped_fs = create_test_instance(); - - const invalid_paths = [ - FILE_PATHS.OUTSIDE, - DIR_PATHS.OUTSIDE, - '/allowed', // parent of allowed path - '/allowed-other', // similar prefix - ]; - - for (const path of invalid_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); - } - }); - - test('is_path_allowed - should reject relative paths', () => { - const scoped_fs = create_test_instance(); - - const relative_paths = ['relative/path', './relative/path', '../relative/path']; +const path_allowed_cases: Array<[label: string, path: string, expected: boolean]> = [ + // Within allowed directories + ['allowed root /allowed/path', '/allowed/path', true], + ['allowed root /allowed/other/path/', '/allowed/other/path/', true], + ['allowed root /another/allowed/directory', '/another/allowed/directory', true], + ['nested file in allowed path', FILE_PATHS.ALLOWED, true], + ['deep nested file in allowed path', FILE_PATHS.NESTED, true], + ['subdirectory in allowed path', '/allowed/path/subdir/', true], + // Outside allowed directories + ['file outside allowed paths', FILE_PATHS.OUTSIDE, false], + ['directory outside allowed paths', DIR_PATHS.OUTSIDE, false], + ['parent of allowed path', '/allowed', false], + ['similar prefix but not allowed', '/allowed-other', false], + // Relative paths + ['relative path (no prefix)', 'relative/path', false], + ['dot-relative path', './relative/path', false], + ['parent-relative path', '../relative/path', false], + // Path traversal + ['traversal via ../', FILE_PATHS.TRAVERSAL, false], + ['traversal to non-allowed sibling', '/allowed/path/../not-allowed', false], +]; - for (const path of relative_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); - } - }); - - test('is_path_allowed - should detect path traversal attempts', () => { +describe('ScopedFs - path validation', () => { + test.each(path_allowed_cases)('is_path_allowed - %s', (_label, path, expected) => { const scoped_fs = create_test_instance(); - - const traversal_paths = [FILE_PATHS.TRAVERSAL, '/allowed/path/../not-allowed']; - - for (const path of traversal_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(false); - } + assert.strictEqual(scoped_fs.is_path_allowed(path), expected); }); test('is_path_allowed - should handle special cases correctly', () => { const scoped_fs = create_test_instance(); // Empty path - expect(scoped_fs.is_path_allowed('')).toBe(false); + assert.ok(!scoped_fs.is_path_allowed('')); // Root directory (only allowed if explicitly included) - expect(scoped_fs.is_path_allowed('/')).toBe(false); + assert.ok(!scoped_fs.is_path_allowed('/')); // With root directory explicitly allowed const root_scoped_fs = new ScopedFs(['/']); - expect(root_scoped_fs.is_path_allowed('/')).toBe(true); - expect(root_scoped_fs.is_path_allowed('/any/path')).toBe(true); + assert.ok(root_scoped_fs.is_path_allowed('/')); + assert.ok(root_scoped_fs.is_path_allowed('/any/path')); }); test('is_path_safe - should verify path security including symlink checks', async () => { const scoped_fs = create_test_instance(); // Regular allowed path without symlinks - expect(await scoped_fs.is_path_safe(FILE_PATHS.ALLOWED)).toBe(true); + assert.ok(await scoped_fs.is_path_safe(FILE_PATHS.ALLOWED)); // Path outside allowed directories - expect(await scoped_fs.is_path_safe(FILE_PATHS.OUTSIDE)).toBe(false); + assert.ok(!(await scoped_fs.is_path_safe(FILE_PATHS.OUTSIDE))); // Path with traversal - expect(await scoped_fs.is_path_safe(FILE_PATHS.TRAVERSAL)).toBe(false); + assert.ok(!(await scoped_fs.is_path_safe(FILE_PATHS.TRAVERSAL))); // Mock a symlink to test rejection vi.mocked(fs.lstat).mockImplementationOnce(() => @@ -180,7 +153,7 @@ describe('ScopedFs - path validation', () => { ); // Symlinked file should fail the safety check - expect(await scoped_fs.is_path_safe('/allowed/path/symlink')).toBe(false); + assert.ok(!(await scoped_fs.is_path_safe('/allowed/path/symlink'))); }); }); @@ -192,8 +165,9 @@ describe('ScopedFs - file operations', () => { vi.mocked(fs.readFile).mockResolvedValueOnce(test_content as any); const content = await scoped_fs.read_file(FILE_PATHS.ALLOWED); - expect(content).toBe(test_content); - expect(fs.readFile).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, 'utf8'); + assert.strictEqual(content, test_content); + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 1); + assert.deepEqual(vi.mocked(fs.readFile).mock.calls[0], [FILE_PATHS.ALLOWED, 'utf8']); }); test('read_file - should return Buffer when options specify buffer encoding', async () => { @@ -203,8 +177,8 @@ describe('ScopedFs - file operations', () => { vi.mocked(fs.readFile).mockResolvedValueOnce(test_buffer); const content = await scoped_fs.read_file(FILE_PATHS.ALLOWED, null); - expect(content).toBe(test_buffer); - expect(fs.readFile).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, null); + assert.strictEqual(content, test_buffer); + assert.deepEqual(vi.mocked(fs.readFile).mock.calls[0], [FILE_PATHS.ALLOWED, null]); }); test('read_file - should pass through various encoding options', async () => { @@ -222,15 +196,20 @@ describe('ScopedFs - file operations', () => { vi.mocked(fs.readFile).mockResolvedValueOnce('content' as any); await scoped_fs.read_file(FILE_PATHS.ALLOWED, options as any); - expect(fs.readFile).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, expected); + assert.deepEqual(vi.mocked(fs.readFile).mock.calls[0], [FILE_PATHS.ALLOWED, expected] as any); } }); test('read_file - should throw for paths outside allowed directories', async () => { const scoped_fs = create_test_instance(); - await expect(scoped_fs.read_file(FILE_PATHS.OUTSIDE)).rejects.toThrow('Path is not allowed'); - expect(fs.readFile).not.toHaveBeenCalled(); + try { + await scoped_fs.read_file(FILE_PATHS.OUTSIDE); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); test('write_file - should write to files in allowed paths', async () => { @@ -240,16 +219,23 @@ describe('ScopedFs - file operations', () => { vi.mocked(fs.writeFile).mockResolvedValueOnce(); await scoped_fs.write_file(FILE_PATHS.ALLOWED, test_content); - expect(fs.writeFile).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, test_content, 'utf8'); + assert.deepEqual(vi.mocked(fs.writeFile).mock.calls[0], [ + FILE_PATHS.ALLOWED, + test_content, + 'utf8', + ]); }); test('write_file - should throw for paths outside allowed directories', async () => { const scoped_fs = create_test_instance(); - await expect(scoped_fs.write_file(FILE_PATHS.OUTSIDE, 'content')).rejects.toThrow( - 'Path is not allowed', - ); - expect(fs.writeFile).not.toHaveBeenCalled(); + try { + await scoped_fs.write_file(FILE_PATHS.OUTSIDE, 'content'); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } + assert.strictEqual(vi.mocked(fs.writeFile).mock.calls.length, 0); }); }); @@ -260,14 +246,19 @@ describe('ScopedFs - directory operations', () => { vi.mocked(fs.mkdir).mockResolvedValueOnce(undefined); await scoped_fs.mkdir(DIR_PATHS.NEW_DIR, {recursive: true}); - expect(fs.mkdir).toHaveBeenCalledWith(DIR_PATHS.NEW_DIR, {recursive: true}); + assert.deepEqual(vi.mocked(fs.mkdir).mock.calls[0], [DIR_PATHS.NEW_DIR, {recursive: true}]); }); test('mkdir - should throw for paths outside allowed directories', async () => { const scoped_fs = create_test_instance(); - await expect(scoped_fs.mkdir(DIR_PATHS.OUTSIDE)).rejects.toThrow('Path is not allowed'); - expect(fs.mkdir).not.toHaveBeenCalled(); + try { + await scoped_fs.mkdir(DIR_PATHS.OUTSIDE); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } + assert.strictEqual(vi.mocked(fs.mkdir).mock.calls.length, 0); }); test('readdir - should list directory contents in allowed paths', async () => { @@ -277,15 +268,20 @@ describe('ScopedFs - directory operations', () => { vi.mocked(fs.readdir).mockResolvedValueOnce(dir_contents as any); const contents = await scoped_fs.readdir(DIR_PATHS.ALLOWED, null); - expect(contents).toEqual(dir_contents); - expect(fs.readdir).toHaveBeenCalledWith(DIR_PATHS.ALLOWED, null); + assert.deepEqual(contents, dir_contents); + assert.deepEqual(vi.mocked(fs.readdir).mock.calls[0], [DIR_PATHS.ALLOWED, null] as any); }); test('readdir - should throw for paths outside allowed directories', async () => { const scoped_fs = create_test_instance(); - await expect(scoped_fs.readdir(DIR_PATHS.OUTSIDE)).rejects.toThrow('Path is not allowed'); - expect(fs.readdir).not.toHaveBeenCalled(); + try { + await scoped_fs.readdir(DIR_PATHS.OUTSIDE); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } + assert.strictEqual(vi.mocked(fs.readdir).mock.calls.length, 0); }); test('rm - should remove files or directories in allowed paths', async () => { @@ -294,14 +290,19 @@ describe('ScopedFs - directory operations', () => { vi.mocked(fs.rm).mockResolvedValueOnce(); await scoped_fs.rm(DIR_PATHS.ALLOWED, {recursive: true}); - expect(fs.rm).toHaveBeenCalledWith(DIR_PATHS.ALLOWED, {recursive: true}); + assert.deepEqual(vi.mocked(fs.rm).mock.calls[0], [DIR_PATHS.ALLOWED, {recursive: true}]); }); test('rm - should throw for paths outside allowed directories', async () => { const scoped_fs = create_test_instance(); - await expect(scoped_fs.rm(DIR_PATHS.OUTSIDE)).rejects.toThrow('Path is not allowed'); - expect(fs.rm).not.toHaveBeenCalled(); + try { + await scoped_fs.rm(DIR_PATHS.OUTSIDE); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } + assert.strictEqual(vi.mocked(fs.rm).mock.calls.length, 0); }); }); @@ -316,15 +317,20 @@ describe('ScopedFs - stat operations', () => { vi.mocked(fs.stat).mockResolvedValueOnce(mock_stats); const stats = await scoped_fs.stat(FILE_PATHS.ALLOWED); - expect(stats).toBe(mock_stats); - expect(fs.stat).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, undefined); + assert.strictEqual(stats, mock_stats); + assert.deepEqual(vi.mocked(fs.stat).mock.calls[0], [FILE_PATHS.ALLOWED, undefined]); }); test('stat - should throw for paths outside allowed directories', async () => { const scoped_fs = create_test_instance(); - await expect(scoped_fs.stat(FILE_PATHS.OUTSIDE)).rejects.toThrow('Path is not allowed'); - expect(fs.stat).not.toHaveBeenCalled(); + try { + await scoped_fs.stat(FILE_PATHS.OUTSIDE); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } + assert.strictEqual(vi.mocked(fs.stat).mock.calls.length, 0); }); test('stat - should handle bigint option correctly', async () => { @@ -341,7 +347,7 @@ describe('ScopedFs - stat operations', () => { vi.mocked(fs.stat).mockResolvedValueOnce({} as any); await scoped_fs.stat(FILE_PATHS.ALLOWED, options as any); - expect(fs.stat).toHaveBeenCalledWith(FILE_PATHS.ALLOWED, expected_options); + assert.deepEqual(vi.mocked(fs.stat).mock.calls[0], [FILE_PATHS.ALLOWED, expected_options]); } }); @@ -358,8 +364,8 @@ describe('ScopedFs - stat operations', () => { vi.mocked(fs.stat).mockResolvedValueOnce(bigint_stats); const result = await scoped_fs.stat(FILE_PATHS.ALLOWED, {bigint: true}); - expect(result).toBe(bigint_stats); - expect(typeof (result as unknown as fs_sync.BigIntStats).size).toBe('bigint'); + assert.strictEqual(result, bigint_stats as any); + assert.strictEqual(typeof (result as unknown as fs_sync.BigIntStats).size, 'bigint'); }); }); @@ -378,7 +384,7 @@ describe('ScopedFs - existence checking', () => { for (const {mock_fn, expected} of existence_tests) { mock_fn(); const exists = await scoped_fs.exists(FILE_PATHS.ALLOWED); - expect(exists).toBe(expected); + assert.strictEqual(exists, expected); } }); @@ -386,8 +392,8 @@ describe('ScopedFs - existence checking', () => { const scoped_fs = create_test_instance(); const exists = await scoped_fs.exists(FILE_PATHS.OUTSIDE); - expect(exists).toBe(false); - expect(fs.access).not.toHaveBeenCalled(); + assert.ok(!exists); + assert.strictEqual(vi.mocked(fs.access).mock.calls.length, 0); }); }); @@ -400,7 +406,7 @@ describe('ScopedFs - copy operations', () => { vi.mocked(fs.copyFile).mockResolvedValueOnce(); await scoped_fs.copy_file(source, destination); - expect(fs.copyFile).toHaveBeenCalledWith(source, destination, undefined); + assert.deepEqual(vi.mocked(fs.copyFile).mock.calls[0], [source, destination, undefined]); }); test('copy_file - should pass through mode parameter', async () => { @@ -413,14 +419,14 @@ describe('ScopedFs - copy operations', () => { vi.mocked(fs.copyFile).mockResolvedValueOnce(); await scoped_fs.copy_file(source, destination, COPYFILE_EXCL); - expect(fs.copyFile).toHaveBeenCalledWith(source, destination, COPYFILE_EXCL); + assert.deepEqual(vi.mocked(fs.copyFile).mock.calls[0], [source, destination, COPYFILE_EXCL]); // Test with COPYFILE_FICLONE mode const COPYFILE_FICLONE = 2; vi.mocked(fs.copyFile).mockResolvedValueOnce(); await scoped_fs.copy_file(source, destination, COPYFILE_FICLONE); - expect(fs.copyFile).toHaveBeenCalledWith(source, destination, COPYFILE_FICLONE); + assert.deepEqual(vi.mocked(fs.copyFile).mock.calls[1], [source, destination, COPYFILE_FICLONE]); }); test('copy_file - should throw if either source or destination is outside allowed paths', async () => { @@ -431,10 +437,15 @@ describe('ScopedFs - copy operations', () => { ]; for (const {source, destination} of invalid_copy_operations) { - await expect(scoped_fs.copy_file(source, destination)).rejects.toThrow('Path is not allowed'); + try { + await scoped_fs.copy_file(source, destination); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Path is not allowed'); + } } - expect(fs.copyFile).not.toHaveBeenCalled(); + assert.strictEqual(vi.mocked(fs.copyFile).mock.calls.length, 0); }); }); @@ -451,11 +462,14 @@ describe('ScopedFs - symlink detection', () => { } as any), ); - await expect(scoped_fs.read_file('/allowed/path/symlink.txt')).rejects.toThrow( - SymlinkNotAllowedError, - ); + try { + await scoped_fs.read_file('/allowed/path/symlink.txt'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } - expect(fs.readFile).not.toHaveBeenCalled(); + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); test('should reject operations when parent directory is a symlink', async () => { @@ -479,10 +493,13 @@ describe('ScopedFs - symlink detection', () => { } as any), ); - await expect(scoped_fs.read_file('/allowed/path/symlink-dir/file.txt')).rejects.toThrow( - SymlinkNotAllowedError, - ); + try { + await scoped_fs.read_file('/allowed/path/symlink-dir/file.txt'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } - expect(fs.readFile).not.toHaveBeenCalled(); + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); }); diff --git a/src/test/server/scoped_fs_security.test.ts b/src/test/server/scoped_fs_security.test.ts index c8a46d15b..3cc5f0d48 100644 --- a/src/test/server/scoped_fs_security.test.ts +++ b/src/test/server/scoped_fs_security.test.ts @@ -1,13 +1,9 @@ -// @slop Claude Sonnet 3.7 - -import {test, expect, vi, beforeEach, afterEach, describe} from 'vitest'; +import {test, vi, beforeEach, afterEach, describe, assert} from 'vitest'; import * as fs from 'node:fs/promises'; import * as fs_sync from 'node:fs'; import {ScopedFs, PathNotAllowedError, SymlinkNotAllowedError} from '$lib/server/scoped_fs.js'; -/* eslint-disable no-await-in-loop, @typescript-eslint/no-empty-function, @typescript-eslint/require-await */ - // Mock fs/promises and fs modules vi.mock('node:fs/promises', () => ({ readFile: vi.fn(), @@ -105,7 +101,12 @@ describe('ScopedFs - symlink security', () => { } as any), ); - await expect(operation()).rejects.toThrow(SymlinkNotAllowedError); + try { + await operation(); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } } // Test exists() separately @@ -119,7 +120,7 @@ describe('ScopedFs - symlink security', () => { ); const exists = await scoped_fs.exists(FILE_PATHS.SYMLINK); - expect(exists).toBe(false); + assert.ok(!exists); }); test('should reject symlinks in parent directories', async () => { @@ -160,14 +161,20 @@ describe('ScopedFs - symlink security', () => { }); // Should throw for any operation on a file in a symlinked parent directory - await expect(scoped_fs.read_file(FILE_PATHS.PARENT_SYMLINK)).rejects.toThrow( - SymlinkNotAllowedError, - ); + try { + await scoped_fs.read_file(FILE_PATHS.PARENT_SYMLINK); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } // Should also throw for mkdir in a symlinked directory - await expect(scoped_fs.mkdir('/allowed/path/symlink-dir/subdir')).rejects.toThrow( - SymlinkNotAllowedError, - ); + try { + await scoped_fs.mkdir('/allowed/path/symlink-dir/subdir'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } }); test('should reject symlinks in grandparent directories', async () => { @@ -204,9 +211,12 @@ describe('ScopedFs - symlink security', () => { }); // Should detect the symlink even when it's not the immediate parent - await expect( - scoped_fs.read_file(`${DIR_PATHS.GRANDPARENT_SYMLINK_DIR}/file.txt`), - ).rejects.toThrow(SymlinkNotAllowedError); + try { + await scoped_fs.read_file(`${DIR_PATHS.GRANDPARENT_SYMLINK_DIR}/file.txt`); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } }); test('should detect symlinks consistently across all operations', async () => { @@ -244,7 +254,12 @@ describe('ScopedFs - symlink security', () => { // All operations should detect the symlink for (const operation of operations) { - await expect(operation()).rejects.toThrow(SymlinkNotAllowedError); + try { + await operation(); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, SymlinkNotAllowedError); + } } }); @@ -262,10 +277,10 @@ describe('ScopedFs - symlink security', () => { // Should return false rather than throwing for exists() const result = await scoped_fs.exists(FILE_PATHS.SYMLINK); - expect(result).toBe(false); + assert.ok(!result); // access should not be called since the symlink is detected first - expect(fs.access).not.toHaveBeenCalled(); + assert.strictEqual(vi.mocked(fs.access).mock.calls.length, 0); }); test('is_path_safe should return false for symlinks', async () => { @@ -298,7 +313,7 @@ describe('ScopedFs - symlink security', () => { // Should safely return false without throwing const is_safe = await scoped_fs.is_path_safe(path); - expect(is_safe).toBe(false); + assert.ok(!is_safe); } }); }); @@ -318,13 +333,18 @@ describe('ScopedFs - path traversal security', () => { // Check both synchronous and asynchronous validation for (const path of traversal_paths) { // Synchronous check should fail - expect(scoped_fs.is_path_allowed(path)).toBe(false); + assert.ok(!scoped_fs.is_path_allowed(path)); // Async checks should also fail - expect(await scoped_fs.is_path_safe(path)).toBe(false); + assert.ok(!(await scoped_fs.is_path_safe(path))); // Operations should throw - await expect(scoped_fs.read_file(path)).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.read_file(path); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } } }); @@ -340,8 +360,8 @@ describe('ScopedFs - path traversal security', () => { ]; for (const path of legitimate_paths) { - expect(scoped_fs.is_path_allowed(path)).toBe(true); - expect(await scoped_fs.is_path_safe(path)).toBe(true); + assert.ok(scoped_fs.is_path_allowed(path)); + assert.ok(await scoped_fs.is_path_safe(path)); // Mock successful read vi.mocked(fs.readFile).mockReset(); @@ -349,7 +369,7 @@ describe('ScopedFs - path traversal security', () => { // Should allow operations on these paths const content = await scoped_fs.read_file(path); - expect(content).toBe('content'); + assert.strictEqual(content, 'content'); } }); }); @@ -376,16 +396,21 @@ describe('ScopedFs - access control security', () => { ]; for (const {path, allowed} of boundary_test_cases) { - expect(scoped_fs.is_path_allowed(path)).toBe(allowed); + assert.strictEqual(scoped_fs.is_path_allowed(path), allowed); // For valid paths, mock a successful read if (allowed) { vi.mocked(fs.readFile).mockReset(); vi.mocked(fs.readFile).mockResolvedValueOnce('content' as any); const content = await scoped_fs.read_file(path); - expect(content).toBe('content'); + assert.strictEqual(content, 'content'); } else { - await expect(scoped_fs.read_file(path)).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.read_file(path); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } } } }); @@ -405,7 +430,7 @@ describe('ScopedFs - access control security', () => { ]; for (const path of root_test_paths) { - expect(root_scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(root_scoped_fs.is_path_allowed(path)); // Mock successful read vi.mocked(fs.readFile).mockReset(); @@ -413,12 +438,17 @@ describe('ScopedFs - access control security', () => { // Should allow operations const content = await root_scoped_fs.read_file(path); - expect(content).toBe('content'); + assert.strictEqual(content, 'content'); } // Non-absolute paths should still be rejected - expect(root_scoped_fs.is_path_allowed('relative/path')).toBe(false); - await expect(root_scoped_fs.read_file('relative/path')).rejects.toThrow(PathNotAllowedError); + assert.ok(!root_scoped_fs.is_path_allowed('relative/path')); + try { + await root_scoped_fs.read_file('relative/path'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } }); test('should properly isolate between allowed paths', async () => { @@ -445,13 +475,18 @@ describe('ScopedFs - access control security', () => { // Check allowed paths for (const path of allowed_paths) { - expect(complex_scoped_fs.is_path_allowed(path)).toBe(true); + assert.ok(complex_scoped_fs.is_path_allowed(path)); } // Check disallowed paths for (const path of disallowed_paths) { - expect(complex_scoped_fs.is_path_allowed(path)).toBe(false); - await expect(complex_scoped_fs.read_file(path)).rejects.toThrow(PathNotAllowedError); + assert.ok(!complex_scoped_fs.is_path_allowed(path)); + try { + await complex_scoped_fs.read_file(path); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } } }); @@ -459,14 +494,23 @@ describe('ScopedFs - access control security', () => { const scoped_fs = create_test_instance(); // Empty path should be rejected by all operations - await expect(scoped_fs.read_file('')).rejects.toThrow(PathNotAllowedError); - await expect(scoped_fs.write_file('', 'content')).rejects.toThrow(PathNotAllowedError); - await expect(scoped_fs.stat('')).rejects.toThrow(PathNotAllowedError); - await expect(scoped_fs.mkdir('')).rejects.toThrow(PathNotAllowedError); - await expect(scoped_fs.readdir('')).rejects.toThrow(PathNotAllowedError); + for (const operation of [ + () => scoped_fs.read_file(''), + () => scoped_fs.write_file('', 'content'), + () => scoped_fs.stat(''), + () => scoped_fs.mkdir(''), + () => scoped_fs.readdir(''), + ]) { + try { + await operation(); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } + } // exists() should return false for empty path - expect(await scoped_fs.exists('')).toBe(false); + assert.ok(!(await scoped_fs.exists(''))); }); test('copy_file should validate both source and destination paths', async () => { @@ -477,29 +521,44 @@ describe('ScopedFs - access control security', () => { await scoped_fs.copy_file('/allowed/path/source.txt', '/allowed/other/path/dest.txt'); // Invalid source - await expect( - scoped_fs.copy_file('/not/allowed/source.txt', '/allowed/path/dest.txt'), - ).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.copy_file('/not/allowed/source.txt', '/allowed/path/dest.txt'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } // Invalid destination - await expect( - scoped_fs.copy_file('/allowed/path/source.txt', '/not/allowed/dest.txt'), - ).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.copy_file('/allowed/path/source.txt', '/not/allowed/dest.txt'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } // Both invalid - await expect( - scoped_fs.copy_file('/not/allowed/source.txt', '/not/allowed/dest.txt'), - ).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.copy_file('/not/allowed/source.txt', '/not/allowed/dest.txt'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } // Path traversal in source - await expect( - scoped_fs.copy_file('/allowed/path/../../../etc/passwd', '/allowed/path/dest.txt'), - ).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.copy_file('/allowed/path/../../../etc/passwd', '/allowed/path/dest.txt'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } // Path traversal in destination - await expect( - scoped_fs.copy_file('/allowed/path/source.txt', '/allowed/path/../../../etc/passwd'), - ).rejects.toThrow(PathNotAllowedError); + try { + await scoped_fs.copy_file('/allowed/path/source.txt', '/allowed/path/../../../etc/passwd'); + assert.fail('Expected error to be thrown'); + } catch (e) { + assert.instanceOf(e, PathNotAllowedError); + } }); }); @@ -516,8 +575,8 @@ describe('ScopedFs - security error handling', () => { for (const path of test_paths) { const error = new PathNotAllowedError(path); - expect(error.message).toBe(`Path is not allowed: ${path}`); - expect(error.name).toBe('PathNotAllowedError'); + assert.strictEqual(error.message, `Path is not allowed: ${path}`); + assert.strictEqual(error.name, 'PathNotAllowedError'); } }); @@ -526,8 +585,8 @@ describe('ScopedFs - security error handling', () => { for (const path of test_paths) { const error = new SymlinkNotAllowedError(path); - expect(error.message).toBe(`Path is a symlink which is not allowed: ${path}`); - expect(error.name).toBe('SymlinkNotAllowedError'); + assert.strictEqual(error.message, `Path is a symlink which is not allowed: ${path}`); + assert.strictEqual(error.name, 'SymlinkNotAllowedError'); } }); @@ -538,7 +597,12 @@ describe('ScopedFs - security error handling', () => { vi.mocked(fs.lstat).mockRejectedValueOnce(new Error('Permission denied')); // Should throw the filesystem error, not a security error - await expect(scoped_fs.read_file(FILE_PATHS.ALLOWED)).rejects.toThrow('Permission denied'); - expect(fs.readFile).not.toHaveBeenCalled(); + try { + await scoped_fs.read_file(FILE_PATHS.ALLOWED); + assert.fail('Expected error to be thrown'); + } catch (e: any) { + assert.include(e.message, 'Permission denied'); + } + assert.strictEqual(vi.mocked(fs.readFile).mock.calls.length, 0); }); }); diff --git a/src/test/server/security.test.ts b/src/test/server/security.test.ts index ee72b9457..cda009e96 100644 --- a/src/test/server/security.test.ts +++ b/src/test/server/security.test.ts @@ -1,18 +1,16 @@ -// @slop Claude Opus 4 - -import {describe, test, expect, vi} from 'vitest'; +import {describe, test, vi, assert} from 'vitest'; import type {Handler} from 'hono'; import { parse_allowed_origins, should_allow_origin, verify_request_source, -} from '$lib/server/security.js'; +} from '@fuzdev/fuz_app/http/origin.js'; // Test helpers const create_mock_context = (headers: Record = {}) => { const next = vi.fn(); - const text = vi.fn((content: string, status: number) => ({content, status})); + const json = vi.fn((content: unknown, status: number) => ({content, status})); // Convert all header keys to lowercase for case-insensitive lookup const normalized_headers: Record = {}; @@ -24,10 +22,10 @@ const create_mock_context = (headers: Record = {}) => { req: { header: (name: string) => normalized_headers[name.toLowerCase()], }, - text, + json, }; - return {c, next, text}; + return {c, next, json}; }; const test_pattern = ( @@ -38,89 +36,87 @@ const test_pattern = ( const regexps = parse_allowed_origins(pattern); for (const origin of valid_origins) { - expect(should_allow_origin(origin, regexps), `${origin} should match ${pattern}`).toBe(true); + assert.ok(should_allow_origin(origin, regexps), `${origin} should match ${pattern}`); } for (const origin of invalid_origins) { - expect(should_allow_origin(origin, regexps), `${origin} should not match ${pattern}`).toBe( - false, - ); + assert.ok(!should_allow_origin(origin, regexps), `${origin} should not match ${pattern}`); } }; const test_middleware_allows = async (handler: Handler, headers: Record) => { const {c, next} = create_mock_context(headers); await handler(c as any, next); - expect(next).toHaveBeenCalled(); + assert.ok(next.mock.calls.length > 0); }; const test_middleware_blocks = async ( handler: Handler, headers: Record, - expected_message: string, + expected_error: string, expected_status = 403, ) => { - const {c, next, text} = create_mock_context(headers); + const {c, next, json} = create_mock_context(headers); const result = await handler(c as any, next); - expect(next).not.toHaveBeenCalled(); - expect(text).toHaveBeenCalledWith(expected_message, expected_status); - expect(result).toEqual({content: expected_message, status: expected_status}); + assert.strictEqual(next.mock.calls.length, 0); + assert.deepEqual(json.mock.calls[0], [{error: expected_error}, expected_status]); + assert.deepEqual(result, {content: {error: expected_error}, status: expected_status}); }; describe('parse_allowed_origins', () => { test('returns empty array for undefined', () => { - expect(parse_allowed_origins(undefined)).toEqual([]); + assert.deepEqual(parse_allowed_origins(undefined), []); }); test('returns empty array for empty string', () => { - expect(parse_allowed_origins('')).toEqual([]); + assert.deepEqual(parse_allowed_origins(''), []); }); test('parses single origin', () => { const patterns = parse_allowed_origins('http://localhost:3000'); - expect(patterns).toHaveLength(1); - expect(patterns[0]).toBeInstanceOf(RegExp); + assert.strictEqual(patterns.length, 1); + assert.instanceOf(patterns[0], RegExp); }); test('parses multiple comma-separated origins', () => { const patterns = parse_allowed_origins('http://localhost:3000,https://example.com'); - expect(patterns).toHaveLength(2); + assert.strictEqual(patterns.length, 2); }); test('trims whitespace from origins', () => { const patterns = parse_allowed_origins(' http://localhost:3000 , https://example.com '); - expect(patterns).toHaveLength(2); + assert.strictEqual(patterns.length, 2); }); test('filters out empty entries', () => { const patterns = parse_allowed_origins('http://localhost:3000,,https://example.com,'); - expect(patterns).toHaveLength(2); + assert.strictEqual(patterns.length, 2); }); test('handles complex patterns', () => { const patterns = parse_allowed_origins( 'https://*.example.com,http://localhost:*,https://*.test.com:*', ); - expect(patterns).toHaveLength(3); + assert.strictEqual(patterns.length, 3); }); }); describe('should_allow_origin', () => { test('returns false for empty patterns', () => { - expect(should_allow_origin('http://example.com', [])).toBe(false); + assert.ok(!should_allow_origin('http://example.com', [])); }); test('matches exact origins', () => { const patterns = parse_allowed_origins('http://example.com'); - expect(should_allow_origin('http://example.com', patterns)).toBe(true); - expect(should_allow_origin('https://example.com', patterns)).toBe(false); + assert.ok(should_allow_origin('http://example.com', patterns)); + assert.ok(!should_allow_origin('https://example.com', patterns)); }); test('matches any of multiple patterns', () => { const patterns = parse_allowed_origins('http://localhost:3000,https://example.com'); - expect(should_allow_origin('http://localhost:3000', patterns)).toBe(true); - expect(should_allow_origin('https://example.com', patterns)).toBe(true); - expect(should_allow_origin('http://other.com', patterns)).toBe(false); + assert.ok(should_allow_origin('http://localhost:3000', patterns)); + assert.ok(should_allow_origin('https://example.com', patterns)); + assert.ok(!should_allow_origin('http://other.com', patterns)); }); }); @@ -151,14 +147,17 @@ describe('pattern_to_regexp', () => { }); test('throws on paths in patterns', () => { - expect(() => parse_allowed_origins('http://example.com/api')).toThrow( - 'Paths not allowed in origin patterns', + assert.throws( + () => parse_allowed_origins('http://example.com/api'), + /Paths not allowed in origin patterns/, ); - expect(() => parse_allowed_origins('https://example.com/api/v1')).toThrow( - 'Paths not allowed in origin patterns', + assert.throws( + () => parse_allowed_origins('https://example.com/api/v1'), + /Paths not allowed in origin patterns/, ); - expect(() => parse_allowed_origins('http://localhost:3000/')).toThrow( - 'Paths not allowed in origin patterns', + assert.throws( + () => parse_allowed_origins('http://localhost:3000/'), + /Paths not allowed in origin patterns/, ); }); @@ -291,10 +290,10 @@ describe('pattern_to_regexp', () => { test('ensures wildcards cannot match dots', () => { const patterns = parse_allowed_origins('https://*.example.com'); // The wildcard should match 'safe' but not 'safe.evil' - expect(should_allow_origin('https://safe.example.com', patterns)).toBe(true); - expect(should_allow_origin('https://safe.evil.example.com', patterns)).toBe(false); + assert.ok(should_allow_origin('https://safe.example.com', patterns)); + assert.ok(!should_allow_origin('https://safe.evil.example.com', patterns)); // This is critical - the wildcard should not be able to match across dots - expect(should_allow_origin('https://safe.com.evil.com.example.com', patterns)).toBe(false); + assert.ok(!should_allow_origin('https://safe.com.evil.com.example.com', patterns)); }); }); @@ -369,47 +368,53 @@ describe('pattern_to_regexp', () => { describe('error handling', () => { test('throws on invalid pattern format', () => { - expect(() => parse_allowed_origins('not-a-url')).toThrow('Invalid origin pattern'); - expect(() => parse_allowed_origins('ftp://example.com')).toThrow('Invalid origin pattern'); - expect(() => parse_allowed_origins('//example.com')).toThrow('Invalid origin pattern'); - expect(() => parse_allowed_origins('*.example.com')).toThrow('Invalid origin pattern'); - expect(() => parse_allowed_origins('example.com')).toThrow('Invalid origin pattern'); - expect(() => parse_allowed_origins('localhost:3000')).toThrow('Invalid origin pattern'); + assert.throws(() => parse_allowed_origins('not-a-url'), /Invalid origin pattern/); + assert.throws(() => parse_allowed_origins('ftp://example.com'), /Invalid origin pattern/); + assert.throws(() => parse_allowed_origins('//example.com'), /Invalid origin pattern/); + assert.throws(() => parse_allowed_origins('*.example.com'), /Invalid origin pattern/); + assert.throws(() => parse_allowed_origins('example.com'), /Invalid origin pattern/); + assert.throws(() => parse_allowed_origins('localhost:3000'), /Invalid origin pattern/); }); test('throws on wildcards in wrong positions', () => { - expect(() => parse_allowed_origins('http://ex*ample.com')).toThrow( - 'Wildcards must be complete labels', + assert.throws( + () => parse_allowed_origins('http://ex*ample.com'), + /Wildcards must be complete labels/, ); - expect(() => parse_allowed_origins('http://example*.com')).toThrow( - 'Wildcards must be complete labels', + assert.throws( + () => parse_allowed_origins('http://example*.com'), + /Wildcards must be complete labels/, ); - expect(() => parse_allowed_origins('http://*example.com')).toThrow( - 'Wildcards must be complete labels', + assert.throws( + () => parse_allowed_origins('http://*example.com'), + /Wildcards must be complete labels/, ); - expect(() => parse_allowed_origins('http://example.*com')).toThrow( - 'Wildcards must be complete labels', + assert.throws( + () => parse_allowed_origins('http://example.*com'), + /Wildcards must be complete labels/, ); }); test('throws on invalid port wildcards', () => { - expect(() => parse_allowed_origins('http://example.com:*000')).toThrow( - 'Invalid origin pattern', - ); - expect(() => parse_allowed_origins('http://example.com:3*')).toThrow( - 'Invalid origin pattern', + assert.throws( + () => parse_allowed_origins('http://example.com:*000'), + /Invalid origin pattern/, ); + assert.throws(() => parse_allowed_origins('http://example.com:3*'), /Invalid origin pattern/); }); test('throws on wildcards in IPv6 addresses', () => { - expect(() => parse_allowed_origins('http://[*::1]:3000')).toThrow( - 'Wildcards not allowed in IPv6 addresses', + assert.throws( + () => parse_allowed_origins('http://[*::1]:3000'), + /Wildcards not allowed in IPv6 addresses/, ); - expect(() => parse_allowed_origins('https://[2001:db8:*::1]')).toThrow( - 'Wildcards not allowed in IPv6 addresses', + assert.throws( + () => parse_allowed_origins('https://[2001:db8:*::1]'), + /Wildcards not allowed in IPv6 addresses/, ); - expect(() => parse_allowed_origins('http://[::ffff:*.0.0.1]:8080')).toThrow( - 'Wildcards not allowed in IPv6 addresses', + assert.throws( + () => parse_allowed_origins('http://[::ffff:*.0.0.1]:8080'), + /Wildcards not allowed in IPv6 addresses/, ); }); }); @@ -419,43 +424,43 @@ describe('pattern_to_regexp', () => { const patterns = parse_allowed_origins('https://example.com'); // All these should match - expect(should_allow_origin('https://example.com', patterns)).toBe(true); - expect(should_allow_origin('https://Example.com', patterns)).toBe(true); - expect(should_allow_origin('https://EXAMPLE.COM', patterns)).toBe(true); - expect(should_allow_origin('https://ExAmPlE.cOm', patterns)).toBe(true); + assert.ok(should_allow_origin('https://example.com', patterns)); + assert.ok(should_allow_origin('https://Example.com', patterns)); + assert.ok(should_allow_origin('https://EXAMPLE.COM', patterns)); + assert.ok(should_allow_origin('https://ExAmPlE.cOm', patterns)); }); test('protocol is also case-insensitive due to regex i flag', () => { const patterns = parse_allowed_origins('https://example.com'); // These should match (case-insensitive regex) - expect(should_allow_origin('https://example.com', patterns)).toBe(true); - expect(should_allow_origin('https://Example.com', patterns)).toBe(true); - expect(should_allow_origin('https://EXAMPLE.COM', patterns)).toBe(true); + assert.ok(should_allow_origin('https://example.com', patterns)); + assert.ok(should_allow_origin('https://Example.com', patterns)); + assert.ok(should_allow_origin('https://EXAMPLE.COM', patterns)); // Different protocol should NOT match - expect(should_allow_origin('http://example.com', patterns)).toBe(false); + assert.ok(!should_allow_origin('http://example.com', patterns)); // Note: The regex uses 'i' flag making the entire pattern case-insensitive // In practice, browsers always send lowercase protocols, but our regex would match this - expect(should_allow_origin('HTTPS://example.com', patterns)).toBe(true); + assert.ok(should_allow_origin('HTTPS://example.com', patterns)); }); test('case-insensitive matching with wildcards', () => { const patterns = parse_allowed_origins('https://*.example.com'); - expect(should_allow_origin('https://API.example.com', patterns)).toBe(true); - expect(should_allow_origin('https://api.EXAMPLE.com', patterns)).toBe(true); - expect(should_allow_origin('https://Api.Example.Com', patterns)).toBe(true); + assert.ok(should_allow_origin('https://API.example.com', patterns)); + assert.ok(should_allow_origin('https://api.EXAMPLE.com', patterns)); + assert.ok(should_allow_origin('https://Api.Example.Com', patterns)); }); test('case-insensitive with IPv6', () => { // IPv6 addresses can have hexadecimal characters that are case-insensitive const patterns = parse_allowed_origins('https://[2001:DB8::1]'); - expect(should_allow_origin('https://[2001:db8::1]', patterns)).toBe(true); - expect(should_allow_origin('https://[2001:DB8::1]', patterns)).toBe(true); - expect(should_allow_origin('https://[2001:dB8::1]', patterns)).toBe(true); + assert.ok(should_allow_origin('https://[2001:db8::1]', patterns)); + assert.ok(should_allow_origin('https://[2001:DB8::1]', patterns)); + assert.ok(should_allow_origin('https://[2001:dB8::1]', patterns)); }); }); @@ -495,7 +500,7 @@ describe('pattern_to_regexp', () => { test('handles very long origin strings', () => { const long_subdomain = 'a'.repeat(63) + '.example.com'; const patterns = parse_allowed_origins(`https://*.example.com`); - expect(should_allow_origin(`https://${long_subdomain}`, patterns)).toBe(true); + assert.ok(should_allow_origin(`https://${long_subdomain}`, patterns)); }); }); @@ -503,14 +508,14 @@ describe('pattern_to_regexp', () => { test('handles IPv6 addresses', () => { // Note: Zone identifiers (e.g., %lo0) are not supported by URL constructor const patterns = parse_allowed_origins('http://[::1]:3000,https://[2001:db8::1]'); - expect(patterns).toHaveLength(2); + assert.strictEqual(patterns.length, 2); // Test various IPv6 formats - expect(should_allow_origin('http://[::1]:3000', patterns)).toBe(true); - expect(should_allow_origin('https://[2001:db8::1]', patterns)).toBe(true); + assert.ok(should_allow_origin('http://[::1]:3000', patterns)); + assert.ok(should_allow_origin('https://[2001:db8::1]', patterns)); // Should not match without brackets - expect(should_allow_origin('http://::1:3000', patterns)).toBe(false); + assert.ok(!should_allow_origin('http://::1:3000', patterns)); }); test('handles various IPv6 formats', () => { @@ -554,20 +559,20 @@ describe('pattern_to_regexp', () => { const patterns = parse_allowed_origins('https://example.com'); // Trailing dots won't match because we do exact string matching - expect(should_allow_origin('https://example.com.', patterns)).toBe(false); - expect(should_allow_origin('https://example.com', patterns)).toBe(true); + assert.ok(!should_allow_origin('https://example.com.', patterns)); + assert.ok(should_allow_origin('https://example.com', patterns)); // If you want to match trailing dots, you need to include them in the pattern const patternsWithDot = parse_allowed_origins('https://example.com.'); - expect(should_allow_origin('https://example.com.', patternsWithDot)).toBe(true); - expect(should_allow_origin('https://example.com', patternsWithDot)).toBe(false); + assert.ok(should_allow_origin('https://example.com.', patternsWithDot)); + assert.ok(!should_allow_origin('https://example.com', patternsWithDot)); }); test('handles punycode domains', () => { // International domain names are converted to punycode const patterns = parse_allowed_origins('https://xn--e1afmkfd.xn--p1ai'); // пример.рф in punycode - expect(should_allow_origin('https://xn--e1afmkfd.xn--p1ai', patterns)).toBe(true); + assert.ok(should_allow_origin('https://xn--e1afmkfd.xn--p1ai', patterns)); // The original Unicode domain would need to be converted to punycode before comparison }); @@ -586,13 +591,13 @@ describe('pattern_to_regexp', () => { ]; for (const origin of localhost_origins) { - expect(should_allow_origin(origin, patterns)).toBe(true); + assert.ok(should_allow_origin(origin, patterns)); } }); test('handles empty hostname edge case', () => { // This should be caught as invalid - expect(() => parse_allowed_origins('http://:3000')).toThrow('Invalid origin pattern'); + assert.throws(() => parse_allowed_origins('http://:3000'), /Invalid origin pattern/); }); test('handles special regex characters in fixed parts', () => { @@ -640,7 +645,7 @@ describe('verify_request_source middleware', () => { { origin: 'http://evil.com', }, - 'forbidden origin', + 'forbidden_origin', ); }); @@ -662,14 +667,14 @@ describe('verify_request_source middleware', () => { { origin: 'http://[::1]:8080', }, - 'forbidden origin', + 'forbidden_origin', ); await test_middleware_blocks( middleware, { origin: 'https://[2001:db8::2]:443', }, - 'forbidden origin', + 'forbidden_origin', ); }); @@ -703,7 +708,7 @@ describe('verify_request_source middleware', () => { { referer: 'http://evil.com/page', }, - 'forbidden referer', + 'forbidden_referer', ); }); @@ -721,7 +726,7 @@ describe('verify_request_source middleware', () => { { referer: 'http://localhost.:3000/page', }, - 'forbidden referer', + 'forbidden_referer', ); // Origin header with trailing dot also won't match @@ -730,7 +735,7 @@ describe('verify_request_source middleware', () => { { origin: 'http://localhost.:3000', }, - 'forbidden origin', + 'forbidden_origin', ); // To match trailing dots, you need them in the pattern @@ -761,7 +766,7 @@ describe('verify_request_source middleware', () => { { referer: 'http://[::2]:3000/page', }, - 'forbidden referer', + 'forbidden_referer', ); }); @@ -771,7 +776,7 @@ describe('verify_request_source middleware', () => { { referer: 'not-a-valid-url', }, - 'forbidden referer', + 'forbidden_referer', ); }); @@ -784,7 +789,7 @@ describe('verify_request_source middleware', () => { { referer: 'data:text/html,

test

', }, - 'forbidden referer', + 'forbidden_referer', ); }); }); @@ -825,7 +830,7 @@ describe('verify_request_source middleware', () => { { origin: 'http://localhost:3000', }, - 'forbidden origin', + 'forbidden_origin', ); }); @@ -835,7 +840,7 @@ describe('verify_request_source middleware', () => { { referer: 'http://localhost:3000/page', }, - 'forbidden referer', + 'forbidden_referer', ); }); @@ -879,7 +884,7 @@ describe('integration scenarios', () => { ]; for (const origin of dev_origins) { - expect(should_allow_origin(origin, dev_patterns)).toBe(true); + assert.ok(should_allow_origin(origin, dev_patterns)); } }); @@ -904,11 +909,11 @@ describe('integration scenarios', () => { ]; for (const origin of allowed) { - expect(should_allow_origin(origin, prod_patterns)).toBe(true); + assert.ok(should_allow_origin(origin, prod_patterns)); } for (const origin of blocked) { - expect(should_allow_origin(origin, prod_patterns)).toBe(false); + assert.ok(!should_allow_origin(origin, prod_patterns)); } }); @@ -938,21 +943,21 @@ describe('integration scenarios', () => { ); // HTTP dev with any port - expect(should_allow_origin('http://api.dev.example.com', patterns)).toBe(true); - expect(should_allow_origin('http://api.dev.example.com:3000', patterns)).toBe(true); - expect(should_allow_origin('http://api.dev.example.com:8080', patterns)).toBe(true); + assert.ok(should_allow_origin('http://api.dev.example.com', patterns)); + assert.ok(should_allow_origin('http://api.dev.example.com:3000', patterns)); + assert.ok(should_allow_origin('http://api.dev.example.com:8080', patterns)); // HTTPS prod without port flexibility - expect(should_allow_origin('https://api.prod.example.com', patterns)).toBe(true); - expect(should_allow_origin('https://api.prod.example.com:443', patterns)).toBe(false); + assert.ok(should_allow_origin('https://api.prod.example.com', patterns)); + assert.ok(!should_allow_origin('https://api.prod.example.com:443', patterns)); // Exact match - expect(should_allow_origin('https://example.com', patterns)).toBe(true); + assert.ok(should_allow_origin('https://example.com', patterns)); // Should not match - expect(should_allow_origin('https://api.dev.example.com', patterns)).toBe(false); // Wrong protocol - expect(should_allow_origin('http://api.prod.example.com', patterns)).toBe(false); // Wrong protocol - expect(should_allow_origin('https://sub.example.com', patterns)).toBe(false); // No wildcard + assert.ok(!should_allow_origin('https://api.dev.example.com', patterns)); // Wrong protocol + assert.ok(!should_allow_origin('http://api.prod.example.com', patterns)); // Wrong protocol + assert.ok(!should_allow_origin('https://sub.example.com', patterns)); // No wildcard }); }); @@ -961,24 +966,24 @@ describe('normalize_origin', () => { const patterns = parse_allowed_origins('https://example.com:443'); // The pattern explicitly includes :443 - expect(should_allow_origin('https://example.com:443', patterns)).toBe(true); + assert.ok(should_allow_origin('https://example.com:443', patterns)); // Without the port, it won't match (we don't normalize) - expect(should_allow_origin('https://example.com', patterns)).toBe(false); + assert.ok(!should_allow_origin('https://example.com', patterns)); }); test('handles explicit default port 80 for HTTP', () => { const patterns = parse_allowed_origins('http://example.com:80'); // The pattern explicitly includes :80 - expect(should_allow_origin('http://example.com:80', patterns)).toBe(true); + assert.ok(should_allow_origin('http://example.com:80', patterns)); // Without the port, it won't match (we don't normalize) - expect(should_allow_origin('http://example.com', patterns)).toBe(false); + assert.ok(!should_allow_origin('http://example.com', patterns)); }); test('preserves non-standard ports', () => { const patterns = parse_allowed_origins('https://example.com:8443'); - expect(should_allow_origin('https://example.com:8443', patterns)).toBe(true); - expect(should_allow_origin('https://example.com', patterns)).toBe(false); + assert.ok(should_allow_origin('https://example.com:8443', patterns)); + assert.ok(!should_allow_origin('https://example.com', patterns)); }); }); diff --git a/src/test/socket.svelte.test.ts b/src/test/socket.svelte.test.ts index 3935c5300..439b22cc9 100644 --- a/src/test/socket.svelte.test.ts +++ b/src/test/socket.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {beforeEach, describe, test, expect, vi, afterEach} from 'vitest'; +import {beforeEach, describe, test, vi, afterEach, assert} from 'vitest'; import {Socket} from '$lib/socket.svelte.js'; import {DEFAULT_CLOSE_CODE} from '$lib/socket_helpers.js'; @@ -125,9 +123,10 @@ describe('Socket', () => { const socket = new Socket({app}); socket.connect(TEST_URLS.BASE); - expect(globalThis.WebSocket).toHaveBeenCalledWith(TEST_URLS.BASE); - expect(socket.url).toBe(TEST_URLS.BASE); - expect(socket.status).toBe('pending'); + assert.ok((globalThis.WebSocket as any).mock.calls.length > 0); + assert.deepEqual((globalThis.WebSocket as any).mock.calls[0], [TEST_URLS.BASE]); + assert.strictEqual(socket.url, TEST_URLS.BASE); + assert.strictEqual(socket.status, 'pending'); }); test('disconnect closes WebSocket with default close code', () => { @@ -140,9 +139,9 @@ describe('Socket', () => { // Disconnect socket.disconnect(); - expect(mock_socket.close_code).toBe(DEFAULT_CLOSE_CODE); - expect(socket.ws).toBeNull(); - expect(socket.open).toBe(false); + assert.strictEqual(mock_socket.close_code, DEFAULT_CLOSE_CODE); + assert.isNull(socket.ws); + assert.ok(!socket.open); }); test('connection success updates state correctly', () => { @@ -150,9 +149,9 @@ describe('Socket', () => { socket.connect(TEST_URLS.BASE); mock_socket.connect(); - expect(socket.open).toBe(true); - expect(socket.status).toBe('success'); - expect(socket.connected).toBe(true); + assert.ok(socket.open); + assert.strictEqual(socket.status, 'success'); + assert.ok(socket.connected); }); test('update_url reconnects with new URL if already connected', () => { @@ -160,14 +159,14 @@ describe('Socket', () => { socket.connect(TEST_URLS.BASE); mock_socket.connect(); - expect(socket.url).toBe(TEST_URLS.BASE); + assert.strictEqual(socket.url, TEST_URLS.BASE); // Update URL socket.update_url(TEST_URLS.ALTERNATE); - expect(socket.url).toBe(TEST_URLS.ALTERNATE); - expect(globalThis.WebSocket).toHaveBeenCalledTimes(2); - expect(globalThis.WebSocket).toHaveBeenLastCalledWith(TEST_URLS.ALTERNATE); + assert.strictEqual(socket.url, TEST_URLS.ALTERNATE); + assert.strictEqual((globalThis.WebSocket as any).mock.calls.length, 2); + assert.deepEqual((globalThis.WebSocket as any).mock.calls[1], [TEST_URLS.ALTERNATE]); }); }); @@ -177,8 +176,8 @@ describe('Socket', () => { // Not connected yet const sent = socket.send(TEST_MESSAGE.BASIC); - expect(sent).toBe(false); - expect(socket.queued_message_count).toBe(1); + assert.ok(!sent); + assert.strictEqual(socket.queued_message_count, 1); }); test('send transmits message when socket is connected', () => { @@ -188,11 +187,11 @@ describe('Socket', () => { const sent = socket.send(TEST_MESSAGE.BASIC); - expect(sent).toBe(true); - expect(mock_socket.sent_messages.length).toBe(1); + assert.ok(sent); + assert.strictEqual(mock_socket.sent_messages.length, 1); const first_message = mock_socket.sent_messages[0]; - expect(first_message).toBeDefined(); - expect(JSON.parse(first_message!)).toEqual(TEST_MESSAGE.BASIC); + assert.isDefined(first_message); + assert.deepEqual(JSON.parse(first_message), TEST_MESSAGE.BASIC); }); test('message queueing sends queued messages when reconnected', () => { @@ -202,15 +201,15 @@ describe('Socket', () => { socket.send({method: 'message_a'}); socket.send({method: 'message_b'}); - expect(socket.queued_message_count).toBe(2); + assert.strictEqual(socket.queued_message_count, 2); // Connect socket.connect(TEST_URLS.BASE); mock_socket.connect(); // Messages should be sent - expect(mock_socket.sent_messages.length).toBe(2); - expect(socket.queued_message_count).toBe(0); + assert.strictEqual(mock_socket.sent_messages.length, 2); + assert.strictEqual(socket.queued_message_count, 0); }); }); @@ -220,7 +219,7 @@ describe('Socket', () => { // Queue a message socket.send(TEST_MESSAGE.BASIC); - expect(socket.queued_message_count).toBe(1); + assert.strictEqual(socket.queued_message_count, 1); // Mock send failure const error_message = 'Send operation failed'; @@ -233,13 +232,13 @@ describe('Socket', () => { mock_socket.connect(); // Message should move to failed - expect(socket.queued_message_count).toBe(0); - expect(socket.failed_message_count).toBe(1); + assert.strictEqual(socket.queued_message_count, 0); + assert.strictEqual(socket.failed_message_count, 1); // Check error reason const failed_message = Array.from(socket.failed_messages.values())[0]; - expect(failed_message).toBeDefined(); - expect(failed_message!.reason).toBe(error_message); + assert.isDefined(failed_message); + assert.strictEqual(failed_message.reason, error_message); }); test('clear_failed_messages removes all failed messages', () => { @@ -259,12 +258,12 @@ describe('Socket', () => { socket.retry_queued_messages(); // Verify message moved to failed - expect(socket.queued_message_count).toBe(0); - expect(socket.failed_message_count).toBe(1); + assert.strictEqual(socket.queued_message_count, 0); + assert.strictEqual(socket.failed_message_count, 1); // Clear failed messages socket.clear_failed_messages(); - expect(socket.failed_message_count).toBe(0); + assert.strictEqual(socket.failed_message_count, 0); }); }); @@ -278,12 +277,12 @@ describe('Socket', () => { // Simulate unexpected close mock_socket.dispatchEvent('close'); - expect(socket.open).toBe(false); - expect(socket.status).toBe('failure'); + assert.ok(!socket.open); + assert.strictEqual(socket.status, 'failure'); // Should reconnect after delay vi.advanceTimersByTime(1000); - expect(globalThis.WebSocket).toHaveBeenCalledTimes(2); + assert.strictEqual((globalThis.WebSocket as any).mock.calls.length, 2); }); test('reconnect delay uses exponential backoff', () => { @@ -295,17 +294,17 @@ describe('Socket', () => { // Initial connect socket.connect(TEST_URLS.BASE); mock_socket.connect(); - expect(socket.status).toBe('success'); + assert.strictEqual(socket.status, 'success'); // First unexpected close mock_socket.dispatchEvent('close', {code: 1006}); - expect(socket.status).toBe('failure'); - expect(socket.reconnect_count).toBe(1); - expect(socket.current_reconnect_delay).toBe(1000); // 1000 * 1.5^0 + assert.strictEqual(socket.status, 'failure'); + assert.strictEqual(socket.reconnect_count, 1); + assert.strictEqual(socket.current_reconnect_delay, 1000); // 1000 * 1.5^0 // Trigger first reconnect vi.advanceTimersByTime(1000); - expect(globalThis.WebSocket).toHaveBeenCalledTimes(2); + assert.strictEqual((globalThis.WebSocket as any).mock.calls.length, 2); // Test subsequent reconnects with increasing delays // Clear timers between tests to avoid interference @@ -317,8 +316,8 @@ describe('Socket', () => { socket.status = 'failure'; socket.reconnect_count = 1; socket.maybe_reconnect(); - expect(socket.reconnect_count).toBe(2); - expect(socket.current_reconnect_delay).toBe(1500); // 1000 * 1.5^1 + assert.strictEqual(socket.reconnect_count, 2); + assert.strictEqual(socket.current_reconnect_delay, 1500); // 1000 * 1.5^1 // Clear timeout to avoid interference if (socket.reconnect_timeout !== null) { @@ -329,8 +328,8 @@ describe('Socket', () => { socket.status = 'failure'; socket.reconnect_count = 2; socket.maybe_reconnect(); - expect(socket.reconnect_count).toBe(3); - expect(socket.current_reconnect_delay).toBe(2250); // 1000 * 1.5^2 + assert.strictEqual(socket.reconnect_count, 3); + assert.strictEqual(socket.current_reconnect_delay, 2250); // 1000 * 1.5^2 // Test max delay cap if (socket.reconnect_timeout !== null) { @@ -339,8 +338,8 @@ describe('Socket', () => { socket.status = 'failure'; socket.reconnect_count = 14; socket.maybe_reconnect(); - expect(socket.reconnect_count).toBe(15); - expect(socket.current_reconnect_delay).toBe(30000); // Capped at max value + assert.strictEqual(socket.reconnect_count, 15); + assert.strictEqual(socket.current_reconnect_delay, 30000); // Capped at max value }); }); @@ -355,7 +354,7 @@ describe('Socket', () => { vi.advanceTimersByTime(1000); // Check ping was sent - expect(app.api.ping).toHaveBeenCalled(); + assert.ok((app.api.ping as any).mock.calls.length > 0); }); }); }); diff --git a/src/test/sortable.svelte.test.ts b/src/test/sortable.svelte.test.ts index a054ee7bb..71ca8b8de 100644 --- a/src/test/sortable.svelte.test.ts +++ b/src/test/sortable.svelte.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 3.7 - // @vitest-environment jsdom -import {test, expect, describe, beforeEach} from 'vitest'; +import {test, describe, beforeEach, assert} from 'vitest'; import {z} from 'zod'; import {Sortable, type Sorter, sort_by_text, sort_by_numeric} from '$lib/sortable.svelte.js'; @@ -82,12 +80,12 @@ describe('Sortable', () => { ); const first_sorter = sorters[0]; - expect(first_sorter).toBeDefined(); - expect(sortable.items).toBe(items); - expect(sortable.sorters).toBe(sorters); - expect(sortable.active_key).toBe(first_sorter!.key); - expect(sortable.active_sorter).toBe(first_sorter); - expect(sortable.active_sort_fn).toBe(first_sorter!.fn); + assert.isDefined(first_sorter); + assert.strictEqual(sortable.items, items); + assert.strictEqual(sortable.sorters, sorters); + assert.strictEqual(sortable.active_key, first_sorter.key); + assert.strictEqual(sortable.active_sorter, first_sorter); + assert.strictEqual(sortable.active_sort_fn, first_sorter.fn); }); test('uses default key when provided', () => { @@ -98,10 +96,10 @@ describe('Sortable', () => { ); const sorter_at_2 = sorters[2]; - expect(sorter_at_2).toBeDefined(); - expect(sortable.default_key).toBe('value'); - expect(sortable.active_key).toBe('value'); - expect(sortable.active_sorter).toBe(sorter_at_2); + assert.isDefined(sorter_at_2); + assert.strictEqual(sortable.default_key, 'value'); + assert.strictEqual(sortable.active_key, 'value'); + assert.strictEqual(sortable.active_sorter, sorter_at_2); }); test('falls back to first sorter when default key is invalid', () => { @@ -112,9 +110,9 @@ describe('Sortable', () => { ); const first_sorter = sorters[0]; - expect(first_sorter).toBeDefined(); - expect(sortable.default_key).toBe('invalid_key'); - expect(sortable.active_key).toBe(first_sorter!.key); + assert.isDefined(first_sorter); + assert.strictEqual(sortable.default_key, 'invalid_key'); + assert.strictEqual(sortable.active_key, first_sorter.key); }); test('handles empty sorters array', () => { @@ -123,9 +121,9 @@ describe('Sortable', () => { () => [], ); - expect(sortable.active_key).toBe(''); - expect(sortable.active_sorter).toBeUndefined(); - expect(sortable.active_sort_fn).toBeUndefined(); + assert.strictEqual(sortable.active_key, ''); + assert.ok(sortable.active_sorter === undefined); + assert.ok(sortable.active_sort_fn === undefined); }); }); @@ -138,8 +136,8 @@ describe('Sortable', () => { ); const first_sorter = sorters[0]; - expect(first_sorter).toBeDefined(); - expect(sortable.active_key).toBe(first_sorter!.key); + assert.isDefined(first_sorter); + assert.strictEqual(sortable.active_key, first_sorter.key); // Change sorters to new array without the current active key current_sorters = [sorters[2]!, sorters[3]!]; @@ -149,7 +147,7 @@ describe('Sortable', () => { sortable.update_active_key(); // Now the active key should match the first sorter in the new array - expect(sortable.active_key).toBe('value'); + assert.strictEqual(sortable.active_key, 'value'); }); test('preserves active key if still valid after sorters change', () => { @@ -161,27 +159,27 @@ describe('Sortable', () => { const sorter_at_1 = sorters[1]; const sorter_at_2 = sorters[2]; - expect(sorter_at_1).toBeDefined(); - expect(sorter_at_2).toBeDefined(); + assert.isDefined(sorter_at_1); + assert.isDefined(sorter_at_2); // Set active key to the second sorter - sortable.active_key = sorter_at_1!.key; + sortable.active_key = sorter_at_1.key; // Change sorters but keep the active key - current_sorters = [sorter_at_1!, sorter_at_2!]; + current_sorters = [sorter_at_1, sorter_at_2]; sortable.update_active_key(); - expect(sortable.active_key).toBe(sorter_at_1!.key); + assert.strictEqual(sortable.active_key, sorter_at_1.key); }); }); describe('sort_by_text', () => { test('sorts text values in ascending order', () => { const sorter_0 = sorters[0]; - expect(sorter_0).toBeDefined(); + assert.isDefined(sorter_0); const sortable = new Sortable( () => items, - () => [sorter_0!], + () => [sorter_0], ); const sorted = sortable.sorted_items; @@ -189,27 +187,27 @@ describe('Sortable', () => { const item1 = sorted[1]; const item2 = sorted[2]; const item3 = sorted[3]; - expect(item0).toBeDefined(); - expect(item1).toBeDefined(); - expect(item2).toBeDefined(); - expect(item3).toBeDefined(); + assert.isDefined(item0); + assert.isDefined(item1); + assert.isDefined(item2); + assert.isDefined(item3); - expect(item0!.name).toBe('Apple'); - expect(item1!.name).toBe('Apple'); - expect(item2!.name).toBe('Banana'); - expect(item3!.name).toBe('Cherry'); + assert.strictEqual(item0.name, 'Apple'); + assert.strictEqual(item1.name, 'Apple'); + assert.strictEqual(item2.name, 'Banana'); + assert.strictEqual(item3.name, 'Cherry'); // Verify that items with the same name are sorted by cid as fallback - expect(item0!.cid).toBe(40); // First "Apple" has higher cid - expect(item1!.cid).toBe(10); // Second "Apple" has lower cid + assert.strictEqual(item0.cid, 40); // First "Apple" has higher cid + assert.strictEqual(item1.cid, 10); // Second "Apple" has lower cid }); test('sorts text values in descending order', () => { const sorter_1 = sorters[1]; - expect(sorter_1).toBeDefined(); + assert.isDefined(sorter_1); const sortable = new Sortable( () => items, - () => [sorter_1!], + () => [sorter_1], ); const sorted = sortable.sorted_items; @@ -217,29 +215,29 @@ describe('Sortable', () => { const item1 = sorted[1]; const item2 = sorted[2]; const item3 = sorted[3]; - expect(item0).toBeDefined(); - expect(item1).toBeDefined(); - expect(item2).toBeDefined(); - expect(item3).toBeDefined(); + assert.isDefined(item0); + assert.isDefined(item1); + assert.isDefined(item2); + assert.isDefined(item3); - expect(item0!.name).toBe('Cherry'); - expect(item1!.name).toBe('Banana'); - expect(item2!.name).toBe('Apple'); - expect(item3!.name).toBe('Apple'); + assert.strictEqual(item0.name, 'Cherry'); + assert.strictEqual(item1.name, 'Banana'); + assert.strictEqual(item2.name, 'Apple'); + assert.strictEqual(item3.name, 'Apple'); // Verify that items with the same name are sorted by cid as fallback - expect(item2!.cid).toBe(40); // First "Apple" has higher cid - expect(item3!.cid).toBe(10); // Second "Apple" has lower cid + assert.strictEqual(item2.cid, 40); // First "Apple" has higher cid + assert.strictEqual(item3.cid, 10); // Second "Apple" has lower cid }); }); describe('sort_by_numeric', () => { test('sorts numeric values in ascending order', () => { const sorter_2 = sorters[2]; - expect(sorter_2).toBeDefined(); + assert.isDefined(sorter_2); const sortable = new Sortable( () => items, - () => [sorter_2!], + () => [sorter_2], ); const sorted = sortable.sorted_items; @@ -247,23 +245,23 @@ describe('Sortable', () => { const item1 = sorted[1]; const item2 = sorted[2]; const item3 = sorted[3]; - expect(item0).toBeDefined(); - expect(item1).toBeDefined(); - expect(item2).toBeDefined(); - expect(item3).toBeDefined(); - - expect(item0!.value).toBe(5); - expect(item1!.value).toBe(10); - expect(item2!.value).toBe(15); - expect(item3!.value).toBe(20); + assert.isDefined(item0); + assert.isDefined(item1); + assert.isDefined(item2); + assert.isDefined(item3); + + assert.strictEqual(item0.value, 5); + assert.strictEqual(item1.value, 10); + assert.strictEqual(item2.value, 15); + assert.strictEqual(item3.value, 20); }); test('sorts numeric values in descending order', () => { const sorter_3 = sorters[3]; - expect(sorter_3).toBeDefined(); + assert.isDefined(sorter_3); const sortable = new Sortable( () => items, - () => [sorter_3!], + () => [sorter_3], ); const sorted = sortable.sorted_items; @@ -271,15 +269,15 @@ describe('Sortable', () => { const item1 = sorted[1]; const item2 = sorted[2]; const item3 = sorted[3]; - expect(item0).toBeDefined(); - expect(item1).toBeDefined(); - expect(item2).toBeDefined(); - expect(item3).toBeDefined(); - - expect(item0!.value).toBe(20); - expect(item1!.value).toBe(15); - expect(item2!.value).toBe(10); - expect(item3!.value).toBe(5); + assert.isDefined(item0); + assert.isDefined(item1); + assert.isDefined(item2); + assert.isDefined(item3); + + assert.strictEqual(item0.value, 20); + assert.strictEqual(item1.value, 15); + assert.strictEqual(item2.value, 10); + assert.strictEqual(item3.value, 5); }); test('maintains stable sort order with equal values using cid', () => { @@ -300,14 +298,14 @@ describe('Sortable', () => { const item0 = sorted[0]; const item1 = sorted[1]; const item2 = sorted[2]; - expect(item0).toBeDefined(); - expect(item1).toBeDefined(); - expect(item2).toBeDefined(); + assert.isDefined(item0); + assert.isDefined(item1); + assert.isDefined(item2); // Items with equal values should be sorted by cid - expect(item0!.cid).toBe(300); - expect(item1!.cid).toBe(200); - expect(item2!.cid).toBe(100); + assert.strictEqual(item0.cid, 300); + assert.strictEqual(item1.cid, 200); + assert.strictEqual(item2.cid, 100); }); }); @@ -321,7 +319,7 @@ describe('Sortable', () => { ); // Start with 4 items - expect(sortable.sorted_items.length).toBe(4); + assert.strictEqual(sortable.sorted_items.length, 4); // Add a new item const new_item = new TestCell(app, create_uuid(), 'Dragonfruit', 25, 50); @@ -330,8 +328,8 @@ describe('Sortable', () => { current_items = [...current_items, new_item]; // Now we should see 5 items - expect(sortable.sorted_items.length).toBe(5); - expect(sortable.sorted_items.some((item) => item.cid === 50)).toBe(true); + assert.strictEqual(sortable.sorted_items.length, 5); + assert.ok(sortable.sorted_items.some((item) => item.cid === 50)); }); test('updates when active_key changes', () => { @@ -341,19 +339,19 @@ describe('Sortable', () => { ); const first_item = sortable.sorted_items[0]; - expect(first_item).toBeDefined(); + assert.isDefined(first_item); // Initially sorted by name (first sorter) - expect(first_item!.name).toBe('Apple'); + assert.strictEqual(first_item.name, 'Apple'); // Change to sort by value sortable.active_key = 'value'; const first_item_after = sortable.sorted_items[0]; - expect(first_item_after).toBeDefined(); + assert.isDefined(first_item_after); // Should now be sorted by value - expect(first_item_after!.value).toBe(5); + assert.strictEqual(first_item_after.value, 5); }); }); }); diff --git a/src/test/test_helpers.ts b/src/test/test_helpers.ts index db85fca0e..5c0fcde03 100644 --- a/src/test/test_helpers.ts +++ b/src/test/test_helpers.ts @@ -1,47 +1,6 @@ import type {Frontend} from '../lib/frontend.svelte.ts'; import type {DiskfilePath} from '../lib/diskfile_types.ts'; -// TODO these aren't used, should they be for improved type safety? - -/** - * Vitest's `expects` does not narrow types, this does for falsy values. - * - * @see https://github.com/vitest-dev/vitest/issues/2883 - */ -export const expect_ok: (value: T, message?: string) => asserts value = (value, message) => { - if (!value) { - throw new Error(message ?? 'Expected value to be truthy'); - } -}; - -/** - * Vitest's `expects` does not narrow types, this does for undefined values. - * - * @see https://github.com/vitest-dev/vitest/issues/2883 - */ -export const expect_defined: (value: T | undefined, message?: string) => asserts value is T = ( - value, - message, -) => { - if (value === undefined) { - throw new Error(message ?? 'Expected value to be defined'); - } -}; - -/** - * Vitest's `expects` does not narrow types, this does for nullish values. - * - * @see https://github.com/vitest-dev/vitest/issues/2883 - */ -export const expect_nonnullish: ( - value: T | undefined | null, - message?: string, -) => asserts value is T = (value, message) => { - if (value == null) { - throw new Error(message ?? 'Expected value to be non-nullish'); - } -}; - // TODO improve this pattern /** * Applies testing-specific modifications to a Zzz instance. diff --git a/src/test/xml.test.ts b/src/test/xml.test.ts index 2342b37c0..d75edfc34 100644 --- a/src/test/xml.test.ts +++ b/src/test/xml.test.ts @@ -1,8 +1,6 @@ -// @slop Claude Sonnet 4 - // @vitest-environment jsdom -import {describe, test, expect} from 'vitest'; +import {describe, test, assert} from 'vitest'; import {z} from 'zod'; import { @@ -18,71 +16,71 @@ import { const uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const test_uuid_a = '123e4567-e89b-12d3-a456-426614174000'; -const expect_parse_success = (schema: z.ZodType, input: unknown, expected?: T) => { +const assert_parse_success = (schema: z.ZodType, input: unknown, expected?: T) => { const result = schema.safeParse(input); - expect(result.success).toBe(true); - if (result.success && expected !== undefined) { - expect(result.data).toEqual(expected); + assert.ok(result.success); + if (expected !== undefined) { + assert.deepEqual(result.data, expected); } - return result.success ? result.data : undefined; + return result.data; }; -const expect_parse_failure = (schema: z.ZodType, input: unknown) => { +const assert_parse_failure = (schema: z.ZodType, input: unknown) => { const result = schema.safeParse(input); - expect(result.success).toBe(false); - return result.success ? undefined : result.error; + assert.ok(!result.success); + return result.error; }; describe('XmlAttributeKey', () => { test('accepts valid attribute names', () => { const valid_keys = ['attr', 'data-test', 'xml:lang', 'ns:element', 'class']; for (const key of valid_keys) { - expect_parse_success(XmlAttributeKey, key, key); + assert_parse_success(XmlAttributeKey, key, key); } }); test('trims whitespace', () => { - expect_parse_success(XmlAttributeKey, ' attr ', 'attr'); - expect_parse_success(XmlAttributeKey, '\t class \n', 'class'); + assert_parse_success(XmlAttributeKey, ' attr ', 'attr'); + assert_parse_success(XmlAttributeKey, '\t class \n', 'class'); }); test('rejects empty strings after trimming', () => { - expect_parse_failure(XmlAttributeKey, ''); - expect_parse_failure(XmlAttributeKey, ' '); - expect_parse_failure(XmlAttributeKey, '\t\n'); + assert_parse_failure(XmlAttributeKey, ''); + assert_parse_failure(XmlAttributeKey, ' '); + assert_parse_failure(XmlAttributeKey, '\t\n'); }); test('rejects non-strings', () => { - expect_parse_failure(XmlAttributeKey, null); - expect_parse_failure(XmlAttributeKey, undefined); - expect_parse_failure(XmlAttributeKey, 123); - expect_parse_failure(XmlAttributeKey, {}); + assert_parse_failure(XmlAttributeKey, null); + assert_parse_failure(XmlAttributeKey, undefined); + assert_parse_failure(XmlAttributeKey, 123); + assert_parse_failure(XmlAttributeKey, {}); }); test('handles special characters', () => { - expect_parse_success(XmlAttributeKey, 'data-123'); - expect_parse_success(XmlAttributeKey, 'xml_test'); - expect_parse_success(XmlAttributeKey, 'attr.value'); + assert_parse_success(XmlAttributeKey, 'data-123'); + assert_parse_success(XmlAttributeKey, 'xml_test'); + assert_parse_success(XmlAttributeKey, 'attr.value'); }); test('handles unicode', () => { - expect_parse_success(XmlAttributeKey, 'атрибут'); - expect_parse_success(XmlAttributeKey, '属性'); + assert_parse_success(XmlAttributeKey, 'атрибут'); + assert_parse_success(XmlAttributeKey, '属性'); }); }); describe('XmlAttributeKeyWithDefault', () => { test('provides default value', () => { - expect_parse_success(XmlAttributeKeyWithDefault, undefined, 'attr'); + assert_parse_success(XmlAttributeKeyWithDefault, undefined, 'attr'); }); test('accepts valid strings', () => { - expect_parse_success(XmlAttributeKeyWithDefault, 'custom', 'custom'); + assert_parse_success(XmlAttributeKeyWithDefault, 'custom', 'custom'); }); test('rejects empty strings', () => { - expect_parse_failure(XmlAttributeKeyWithDefault, ''); - expect_parse_failure(XmlAttributeKeyWithDefault, ' '); + assert_parse_failure(XmlAttributeKeyWithDefault, ''); + assert_parse_failure(XmlAttributeKeyWithDefault, ' '); }); }); @@ -90,36 +88,36 @@ describe('XmlAttributeValue', () => { test('accepts any string', () => { const values = ['', 'text', '123', 'true', 'special chars: <>&"\'']; for (const value of values) { - expect_parse_success(XmlAttributeValue, value, value); + assert_parse_success(XmlAttributeValue, value, value); } }); test('accepts unicode', () => { - expect_parse_success(XmlAttributeValue, '测试值'); - expect_parse_success(XmlAttributeValue, 'значение'); - expect_parse_success(XmlAttributeValue, '🔥💯'); + assert_parse_success(XmlAttributeValue, '测试值'); + assert_parse_success(XmlAttributeValue, 'значение'); + assert_parse_success(XmlAttributeValue, '🔥💯'); }); test('accepts very long strings', () => { const long_value = 'a'.repeat(10000); - expect_parse_success(XmlAttributeValue, long_value, long_value); + assert_parse_success(XmlAttributeValue, long_value, long_value); }); test('rejects non-strings', () => { - expect_parse_failure(XmlAttributeValue, null); - expect_parse_failure(XmlAttributeValue, undefined); - expect_parse_failure(XmlAttributeValue, 123); - expect_parse_failure(XmlAttributeValue, []); + assert_parse_failure(XmlAttributeValue, null); + assert_parse_failure(XmlAttributeValue, undefined); + assert_parse_failure(XmlAttributeValue, 123); + assert_parse_failure(XmlAttributeValue, []); }); }); describe('XmlAttributeValueWithDefault', () => { test('provides empty string default', () => { - expect_parse_success(XmlAttributeValueWithDefault, undefined, ''); + assert_parse_success(XmlAttributeValueWithDefault, undefined, ''); }); test('accepts valid strings', () => { - expect_parse_success(XmlAttributeValueWithDefault, 'test', 'test'); + assert_parse_success(XmlAttributeValueWithDefault, 'test', 'test'); }); }); @@ -131,80 +129,80 @@ describe('XmlAttribute', () => { }; test('accepts complete valid attributes', () => { - expect_parse_success(XmlAttribute, valid_base_attr); + assert_parse_success(XmlAttribute, valid_base_attr); }); test('requires all properties', () => { - expect_parse_failure(XmlAttribute, {id: test_uuid_a, key: 'class'}); - expect_parse_failure(XmlAttribute, {id: test_uuid_a, value: 'test'}); - expect_parse_failure(XmlAttribute, {key: 'class', value: 'test'}); + assert_parse_failure(XmlAttribute, {id: test_uuid_a, key: 'class'}); + assert_parse_failure(XmlAttribute, {id: test_uuid_a, value: 'test'}); + assert_parse_failure(XmlAttribute, {key: 'class', value: 'test'}); }); test('validates uuid format', () => { - expect_parse_failure(XmlAttribute, {...valid_base_attr, id: 'invalid-uuid'}); - expect_parse_failure(XmlAttribute, {...valid_base_attr, id: ''}); + assert_parse_failure(XmlAttribute, {...valid_base_attr, id: 'invalid-uuid'}); + assert_parse_failure(XmlAttribute, {...valid_base_attr, id: ''}); }); test('validates key constraints', () => { - expect_parse_failure(XmlAttribute, {...valid_base_attr, key: ''}); - expect_parse_failure(XmlAttribute, {...valid_base_attr, key: ' '}); + assert_parse_failure(XmlAttribute, {...valid_base_attr, key: ''}); + assert_parse_failure(XmlAttribute, {...valid_base_attr, key: ' '}); }); test('strict mode rejects extra properties', () => { const attr_with_extra = {...valid_base_attr, extra: 'property'}; - expect_parse_failure(XmlAttribute, attr_with_extra); + assert_parse_failure(XmlAttribute, attr_with_extra); }); test('accepts empty values', () => { - expect_parse_success(XmlAttribute, {...valid_base_attr, value: ''}); + assert_parse_success(XmlAttribute, {...valid_base_attr, value: ''}); }); }); describe('XmlAttributeWithDefaults', () => { test('accepts complete attributes', () => { const attr = {id: test_uuid_a, key: 'id', value: 'main'}; - expect_parse_success(XmlAttributeWithDefaults, attr); + assert_parse_success(XmlAttributeWithDefaults, attr); }); test('generates uuid when missing', () => { const attr_no_id = {key: 'class', value: 'test'}; - const result = expect_parse_success(XmlAttributeWithDefaults, attr_no_id); - expect(result?.id).toMatch(uuid_regex); + const result = assert_parse_success(XmlAttributeWithDefaults, attr_no_id); + assert.match(result.id, uuid_regex); }); test('applies key default when missing', () => { const attr_no_key = {id: test_uuid_a, value: 'test'}; - const result = expect_parse_success(XmlAttributeWithDefaults, attr_no_key); - expect(result?.key).toBe('attr'); + const result = assert_parse_success(XmlAttributeWithDefaults, attr_no_key); + assert.strictEqual(result.key, 'attr'); }); test('applies value default when missing', () => { const attr_no_value = {id: test_uuid_a, key: 'disabled'}; - const result = expect_parse_success(XmlAttributeWithDefaults, attr_no_value); - expect(result?.value).toBe(''); + const result = assert_parse_success(XmlAttributeWithDefaults, attr_no_value); + assert.strictEqual(result.value, ''); }); test('applies all defaults when minimal input', () => { - const result = expect_parse_success(XmlAttributeWithDefaults, {}); - expect(result?.id).toMatch(uuid_regex); - expect(result?.key).toBe('attr'); - expect(result?.value).toBe(''); + const result = assert_parse_success(XmlAttributeWithDefaults, {}); + assert.match(result.id, uuid_regex); + assert.strictEqual(result.key, 'attr'); + assert.strictEqual(result.value, ''); }); test('handles undefined id explicitly', () => { const attr = {id: undefined, key: 'test', value: 'value'}; - const result = expect_parse_success(XmlAttributeWithDefaults, attr); - expect(result?.id).toMatch(uuid_regex); + const result = assert_parse_success(XmlAttributeWithDefaults, attr); + assert.match(result.id, uuid_regex); }); test('strict mode rejects extra properties', () => { const attr_with_extra = {id: test_uuid_a, key: 'test', value: 'val', extra: 'prop'}; - expect_parse_failure(XmlAttributeWithDefaults, attr_with_extra); + assert_parse_failure(XmlAttributeWithDefaults, attr_with_extra); }); test('validates constraints after applying defaults', () => { const attr_empty_key = {id: test_uuid_a, key: '', value: 'test'}; - expect_parse_failure(XmlAttributeWithDefaults, attr_empty_key); + assert_parse_failure(XmlAttributeWithDefaults, attr_empty_key); }); }); @@ -213,7 +211,7 @@ describe('XML use cases', () => { const boolean_attrs = ['disabled', 'checked', 'selected', 'hidden']; for (const key of boolean_attrs) { const attr = {id: test_uuid_a, key, value: ''}; - expect_parse_success(XmlAttributeWithDefaults, attr); + assert_parse_success(XmlAttributeWithDefaults, attr); } }); @@ -221,7 +219,7 @@ describe('XML use cases', () => { const ns_attrs = ['xml:lang', 'xmlns:foo', 'xsi:type', 'data:custom']; for (const key of ns_attrs) { const attr = {id: test_uuid_a, key, value: 'test'}; - expect_parse_success(XmlAttributeWithDefaults, attr); + assert_parse_success(XmlAttributeWithDefaults, attr); } }); @@ -234,7 +232,7 @@ describe('XML use cases', () => { ]; for (const value of complex_values) { const attr = {id: test_uuid_a, key: 'style', value}; - expect_parse_success(XmlAttributeWithDefaults, attr); + assert_parse_success(XmlAttributeWithDefaults, attr); } }); @@ -244,14 +242,14 @@ describe('XML use cases', () => { {key: 'class', value: 'container'}, {key: 'id', value: 'main'}, ]; - const result = expect_parse_success(AttributeArray, attrs); - expect(result).toHaveLength(2); - const first_attr = result?.[0]; - const second_attr = result?.[1]; - expect(first_attr).toBeDefined(); - expect(second_attr).toBeDefined(); - expect(first_attr!.id).toMatch(uuid_regex); - expect(second_attr!.id).toMatch(uuid_regex); + const result = assert_parse_success(AttributeArray, attrs); + assert.strictEqual(result.length, 2); + const first_attr = result[0]; + const second_attr = result[1]; + assert.isDefined(first_attr); + assert.isDefined(second_attr); + assert.match(first_attr.id, uuid_regex); + assert.match(second_attr.id, uuid_regex); }); test('integration with record of attributes', () => { @@ -260,24 +258,24 @@ describe('XML use cases', () => { class_attr: {key: 'class', value: 'container'}, id_attr: {key: 'id', value: 'main'}, }; - expect_parse_success(AttributeRecord, attrs); + assert_parse_success(AttributeRecord, attrs); }); }); describe('error handling', () => { test('provides meaningful error messages', () => { const invalid_attr = {id: 'not-uuid', key: '', value: 123}; - const error = expect_parse_failure(XmlAttributeWithDefaults, invalid_attr); + const error = assert_parse_failure(XmlAttributeWithDefaults, invalid_attr); - const issue_paths = error?.issues.map((i) => i.path.join('.')) || []; - expect(issue_paths).toContain('id'); - expect(issue_paths).toContain('key'); - expect(issue_paths).toContain('value'); + const issue_paths = error.issues.map((i) => i.path.join('.')); + assert.include(issue_paths, 'id'); + assert.include(issue_paths, 'key'); + assert.include(issue_paths, 'value'); }); test('handles type coercion failures gracefully', () => { - expect_parse_failure(XmlAttributeWithDefaults, null); - expect_parse_failure(XmlAttributeWithDefaults, 'string'); - expect_parse_failure(XmlAttributeWithDefaults, []); + assert_parse_failure(XmlAttributeWithDefaults, null); + assert_parse_failure(XmlAttributeWithDefaults, 'string'); + assert_parse_failure(XmlAttributeWithDefaults, []); }); }); diff --git a/svelte.config.js b/svelte.config.js index 5a210d080..07fa5a96b 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,3 +1,4 @@ +import adapter from '@sveltejs/adapter-static'; import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'; import {svelte_preprocess_mdz} from '@fuzdev/fuz_ui/svelte_preprocess_mdz.js'; import {svelte_preprocess_fuz_code} from '@fuzdev/fuz_code/svelte_preprocess_fuz_code.js'; @@ -5,19 +6,6 @@ import {svelte_preprocess_fuz_code} from '@fuzdev/fuz_code/svelte_preprocess_fuz // import {create_csp_directives} from '@fuzdev/fuz_ui/csp.js'; // import {csp_trusted_sources_of_fuzdev} from '@fuzdev/fuz_ui/csp_of_fuzdev.js'; -// Dynamically import adapter based on the ZZZ_BUILD env var. -// ZZZ_BUILD=node for production Node server, otherwise static for GitHub Pages. -const build_mode_raw = process.env.ZZZ_BUILD; -// 'static' | 'node' -const build_mode = build_mode_raw === 'node' ? 'node' : 'static'; - -const adapter_module = - build_mode === 'node' - ? await import('@sveltejs/adapter-node') - : await import('@sveltejs/adapter-static'); - -const adapter = adapter_module.default; - /** @type {import('@sveltejs/kit').Config} */ export default { preprocess: [svelte_preprocess_mdz(), svelte_preprocess_fuz_code(), vitePreprocess()], @@ -25,7 +13,7 @@ export default { vitePlugin: {inspector: true}, kit: { adapter: adapter(), - paths: {relative: false}, // use root-absolute paths for SSR path comparison: https://kit.svelte.dev/docs/configuration#paths + paths: {relative: false}, // use root-absolute paths for SSR path comparison: https://svelte.dev/docs/kit/configuration#paths alias: {$routes: 'src/routes', '@fuzdev/zzz': 'src/lib'}, // csp: { // directives: create_csp_directives({ diff --git a/tsconfig.json b/tsconfig.json index b7da32344..0a2a54ecb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { - "types": ["@sveltejs/kit"], + "types": ["@sveltejs/kit", "deno"], "module": "nodenext", "moduleResolution": "nodenext", "strict": true, diff --git a/vite.config.ts b/vite.config.ts index 6bb66d6de..06bf6f5ef 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,36 @@ +import {availableParallelism} from 'node:os'; import {defineConfig} from 'vite'; import {sveltekit} from '@sveltejs/kit/vite'; import {vite_plugin_library_well_known} from '@fuzdev/fuz_ui/vite_plugin_library_well_known.js'; +const max_workers = Math.max(1, Math.ceil(availableParallelism() / 2)); + export default defineConfig(({mode}) => ({ plugins: [sveltekit(), vite_plugin_library_well_known()], + test: { + projects: [ + { + extends: true, + test: { + name: 'unit', + include: ['src/test/**/*.test.ts'], + exclude: ['src/test/**/*.db.test.ts'], + maxWorkers: max_workers, + sequence: {groupOrder: 1}, + }, + }, + { + extends: true, + test: { + name: 'db', + include: ['src/test/**/*.db.test.ts'], + isolate: false, + fileParallelism: false, + sequence: {groupOrder: 2}, + }, + }, + ], + }, // In test mode, use browser conditions so Svelte's mount() resolves to the client version resolve: mode === 'test' ? {conditions: ['browser']} : undefined, server: {