77fe133e
build(web): console logic answers to a test suite
a73x 2026-08-11 10:18
Commit message
Makefile
| Old | New | ||
|---|---|---|---|
| @@ -12,7 +12,7 @@ LINT_WARN := errcheck,revive,gocyclo,funlen,gocritic,misspell,unconvert,nakedret | |||
| 12 | 12 | ||
| 13 | .PHONY: build build-go build-darwin web test vet proto clean \ | 13 | .PHONY: build build-go build-darwin web test vet proto clean \ |
| 14 | lint lint-extra arch cover fmt fmt-check tidy-check proto-check shape shape-check api api-check \ | 14 | lint lint-extra arch cover fmt fmt-check tidy-check proto-check shape shape-check api api-check \ |
| 15 | site site-check ci deadcode \ | 15 | site site-check ci deadcode web-test \ |
| 16 | deploy release ship hooks site-image server-image | 16 | deploy release ship hooks site-image server-image |
| 17 | 17 | ||
| 18 | # Enable the repo's client-side merge gate: point git at .githooks, whose | 18 | # Enable the repo's client-side merge gate: point git at .githooks, whose |
| @@ -196,6 +196,19 @@ site-image: | |||
| 196 | server-image: web | 196 | server-image: web |
| 197 | ./scripts/server-image.sh | 197 | ./scripts/server-image.sh |
| 198 | 198 | ||
| 199 | # Console logic under test (web/src/**/*.test.ts, run by vitest). New logic in | ||
| 200 | # the SPA lands with tests; markup does not need them. | ||
| 201 | # | ||
| 202 | # Skips (does not fail) when the web toolchain is absent, like `make api` — | ||
| 203 | # `make ci` still runs on a machine with no Node. CI runs `make web` first, so | ||
| 204 | # the gate is enforced there. | ||
| 205 | web-test: | ||
| 206 | @if [ -x web/node_modules/.bin/vitest ]; then \ | ||
| 207 | cd web && npm test; \ | ||
| 208 | else \ | ||
| 209 | echo "web-test: vitest not installed (run 'make web') — SKIPPING (enforced in CI)"; \ | ||
| 210 | fi | ||
| 211 | |||
| 199 | # Whole-program dead-code gate: fails on any function unreachable from a real | 212 | # Whole-program dead-code gate: fails on any function unreachable from a real |
| 200 | # entrypoint — every main() in cmd/. Rooting at the binaries (NOT -test) is what | 213 | # entrypoint — every main() in cmd/. Rooting at the binaries (NOT -test) is what |
| 201 | # catches production code kept alive only by its own tests; the fix is to remove | 214 | # catches production code kept alive only by its own tests; the fix is to remove |
| @@ -227,7 +240,7 @@ deadcode: | |||
| 227 | # The merge gate. Mirrors the required checks in CI. `test` is the authoritative | 240 | # The merge gate. Mirrors the required checks in CI. `test` is the authoritative |
| 228 | # race-detector run; `cover` re-runs without -race to enforce the ratchet; `arch` | 241 | # race-detector run; `cover` re-runs without -race to enforce the ratchet; `arch` |
| 229 | # re-runs the fitness tests with -count=1 (the race run may serve them cached). | 242 | # re-runs the fitness tests with -count=1 (the race run may serve them cached). |
| 230 | ci: vet build-go build-darwin arch lint fmt-check test cover tidy-check proto-check api-check shape-check deadcode site-check | 243 | ci: vet build-go build-darwin arch lint fmt-check test cover tidy-check proto-check api-check shape-check deadcode site-check web-test |
| 231 | 244 | ||
| 232 | # Compile every Go package (no Node/web build needed — the embed dir ships a | 245 | # Compile every Go package (no Node/web build needed — the embed dir ships a |
| 233 | # placeholder, so the server builds and serves a "UI not built" notice). | 246 | # placeholder, so the server builds and serves a "UI not built" notice). |
web/package-lock.json
| Old | New | ||
|---|---|---|---|
| @@ -20,7 +20,8 @@ | |||
| 20 | "svelte": "^5.56.1", | 20 | "svelte": "^5.56.1", |
| 21 | "svelte-check": "^4.6.0", | 21 | "svelte-check": "^4.6.0", |
| 22 | "typescript": "^6.0.3", | 22 | "typescript": "^6.0.3", |
| 23 | "vite": "^8.0.16" | 23 | "vite": "^8.0.16", |
| 24 | "vitest": "^4.1.10" | ||
| 24 | } | 25 | } |
| 25 | }, | 26 | }, |
| 26 | "node_modules/@babel/code-frame": { | 27 | "node_modules/@babel/code-frame": { |
| @@ -616,6 +617,17 @@ | |||
| 616 | "tslib": "^2.4.0" | 617 | "tslib": "^2.4.0" |
| 617 | } | 618 | } |
| 618 | }, | 619 | }, |
| 620 | "node_modules/@types/chai": { | ||
| 621 | "version": "5.2.3", | ||
| 622 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", | ||
| 623 | "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", | ||
| 624 | "dev": true, | ||
| 625 | "license": "MIT", | ||
| 626 | "dependencies": { | ||
| 627 | "@types/deep-eql": "*", | ||
| 628 | "assertion-error": "^2.0.1" | ||
| 629 | } | ||
| 630 | }, | ||
| 619 | "node_modules/@types/cookie": { | 631 | "node_modules/@types/cookie": { |
| 620 | "version": "0.6.0", | 632 | "version": "0.6.0", |
| 621 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", | 633 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", |
| @@ -623,6 +635,13 @@ | |||
| 623 | "dev": true, | 635 | "dev": true, |
| 624 | "license": "MIT" | 636 | "license": "MIT" |
| 625 | }, | 637 | }, |
| 638 | "node_modules/@types/deep-eql": { | ||
| 639 | "version": "4.0.2", | ||
| 640 | "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", | ||
| 641 | "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", | ||
| 642 | "dev": true, | ||
| 643 | "license": "MIT" | ||
| 644 | }, | ||
| 626 | "node_modules/@types/estree": { | 645 | "node_modules/@types/estree": { |
| 627 | "version": "1.0.9", | 646 | "version": "1.0.9", |
| 628 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", | 647 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", |
| @@ -647,6 +666,119 @@ | |||
| 647 | "dev": true, | 666 | "dev": true, |
| 648 | "license": "MIT" | 667 | "license": "MIT" |
| 649 | }, | 668 | }, |
| 669 | "node_modules/@vitest/expect": { | ||
| 670 | "version": "4.1.10", | ||
| 671 | "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", | ||
| 672 | "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", | ||
| 673 | "dev": true, | ||
| 674 | "license": "MIT", | ||
| 675 | "dependencies": { | ||
| 676 | "@standard-schema/spec": "^1.1.0", | ||
| 677 | "@types/chai": "^5.2.2", | ||
| 678 | "@vitest/spy": "4.1.10", | ||
| 679 | "@vitest/utils": "4.1.10", | ||
| 680 | "chai": "^6.2.2", | ||
| 681 | "tinyrainbow": "^3.1.0" | ||
| 682 | }, | ||
| 683 | "funding": { | ||
| 684 | "url": "https://opencollective.com/vitest" | ||
| 685 | } | ||
| 686 | }, | ||
| 687 | "node_modules/@vitest/mocker": { | ||
| 688 | "version": "4.1.10", | ||
| 689 | "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", | ||
| 690 | "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", | ||
| 691 | "dev": true, | ||
| 692 | "license": "MIT", | ||
| 693 | "dependencies": { | ||
| 694 | "@vitest/spy": "4.1.10", | ||
| 695 | "estree-walker": "^3.0.3", | ||
| 696 | "magic-string": "^0.30.21" | ||
| 697 | }, | ||
| 698 | "funding": { | ||
| 699 | "url": "https://opencollective.com/vitest" | ||
| 700 | }, | ||
| 701 | "peerDependencies": { | ||
| 702 | "msw": "^2.4.9", | ||
| 703 | "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" | ||
| 704 | }, | ||
| 705 | "peerDependenciesMeta": { | ||
| 706 | "msw": { | ||
| 707 | "optional": true | ||
| 708 | }, | ||
| 709 | "vite": { | ||
| 710 | "optional": true | ||
| 711 | } | ||
| 712 | } | ||
| 713 | }, | ||
| 714 | "node_modules/@vitest/pretty-format": { | ||
| 715 | "version": "4.1.10", | ||
| 716 | "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", | ||
| 717 | "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", | ||
| 718 | "dev": true, | ||
| 719 | "license": "MIT", | ||
| 720 | "dependencies": { | ||
| 721 | "tinyrainbow": "^3.1.0" | ||
| 722 | }, | ||
| 723 | "funding": { | ||
| 724 | "url": "https://opencollective.com/vitest" | ||
| 725 | } | ||
| 726 | }, | ||
| 727 | "node_modules/@vitest/runner": { | ||
| 728 | "version": "4.1.10", | ||
| 729 | "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", | ||
| 730 | "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", | ||
| 731 | "dev": true, | ||
| 732 | "license": "MIT", | ||
| 733 | "dependencies": { | ||
| 734 | "@vitest/utils": "4.1.10", | ||
| 735 | "pathe": "^2.0.3" | ||
| 736 | }, | ||
| 737 | "funding": { | ||
| 738 | "url": "https://opencollective.com/vitest" | ||
| 739 | } | ||
| 740 | }, | ||
| 741 | "node_modules/@vitest/snapshot": { | ||
| 742 | "version": "4.1.10", | ||
| 743 | "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", | ||
| 744 | "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", | ||
| 745 | "dev": true, | ||
| 746 | "license": "MIT", | ||
| 747 | "dependencies": { | ||
| 748 | "@vitest/pretty-format": "4.1.10", | ||
| 749 | "@vitest/utils": "4.1.10", | ||
| 750 | "magic-string": "^0.30.21", | ||
| 751 | "pathe": "^2.0.3" | ||
| 752 | }, | ||
| 753 | "funding": { | ||
| 754 | "url": "https://opencollective.com/vitest" | ||
| 755 | } | ||
| 756 | }, | ||
| 757 | "node_modules/@vitest/spy": { | ||
| 758 | "version": "4.1.10", | ||
| 759 | "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", | ||
| 760 | "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", | ||
| 761 | "dev": true, | ||
| 762 | "license": "MIT", | ||
| 763 | "funding": { | ||
| 764 | "url": "https://opencollective.com/vitest" | ||
| 765 | } | ||
| 766 | }, | ||
| 767 | "node_modules/@vitest/utils": { | ||
| 768 | "version": "4.1.10", | ||
| 769 | "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", | ||
| 770 | "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", | ||
| 771 | "dev": true, | ||
| 772 | "license": "MIT", | ||
| 773 | "dependencies": { | ||
| 774 | "@vitest/pretty-format": "4.1.10", | ||
| 775 | "convert-source-map": "^2.0.0", | ||
| 776 | "tinyrainbow": "^3.1.0" | ||
| 777 | }, | ||
| 778 | "funding": { | ||
| 779 | "url": "https://opencollective.com/vitest" | ||
| 780 | } | ||
| 781 | }, | ||
| 650 | "node_modules/@xterm/xterm": { | 782 | "node_modules/@xterm/xterm": { |
| 651 | "version": "6.0.0", | 783 | "version": "6.0.0", |
| 652 | "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", | 784 | "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", |
| @@ -706,6 +838,16 @@ | |||
| 706 | "node": ">= 0.4" | 838 | "node": ">= 0.4" |
| 707 | } | 839 | } |
| 708 | }, | 840 | }, |
| 841 | "node_modules/assertion-error": { | ||
| 842 | "version": "2.0.1", | ||
| 843 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", | ||
| 844 | "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", | ||
| 845 | "dev": true, | ||
| 846 | "license": "MIT", | ||
| 847 | "engines": { | ||
| 848 | "node": ">=12" | ||
| 849 | } | ||
| 850 | }, | ||
| 709 | "node_modules/axobject-query": { | 851 | "node_modules/axobject-query": { |
| 710 | "version": "4.1.0", | 852 | "version": "4.1.0", |
| 711 | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", | 853 | "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", |
| @@ -733,6 +875,16 @@ | |||
| 733 | "balanced-match": "^1.0.0" | 875 | "balanced-match": "^1.0.0" |
| 734 | } | 876 | } |
| 735 | }, | 877 | }, |
| 878 | "node_modules/chai": { | ||
| 879 | "version": "6.2.2", | ||
| 880 | "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", | ||
| 881 | "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", | ||
| 882 | "dev": true, | ||
| 883 | "license": "MIT", | ||
| 884 | "engines": { | ||
| 885 | "node": ">=18" | ||
| 886 | } | ||
| 887 | }, | ||
| 736 | "node_modules/change-case": { | 888 | "node_modules/change-case": { |
| 737 | "version": "5.4.4", | 889 | "version": "5.4.4", |
| 738 | "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", | 890 | "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", |
| @@ -773,6 +925,13 @@ | |||
| 773 | "dev": true, | 925 | "dev": true, |
| 774 | "license": "MIT" | 926 | "license": "MIT" |
| 775 | }, | 927 | }, |
| 928 | "node_modules/convert-source-map": { | ||
| 929 | "version": "2.0.0", | ||
| 930 | "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", | ||
| 931 | "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", | ||
| 932 | "dev": true, | ||
| 933 | "license": "MIT" | ||
| 934 | }, | ||
| 776 | "node_modules/cookie": { | 935 | "node_modules/cookie": { |
| 777 | "version": "0.6.0", | 936 | "version": "0.6.0", |
| 778 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", | 937 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", |
| @@ -828,6 +987,13 @@ | |||
| 828 | "dev": true, | 987 | "dev": true, |
| 829 | "license": "MIT" | 988 | "license": "MIT" |
| 830 | }, | 989 | }, |
| 990 | "node_modules/es-module-lexer": { | ||
| 991 | "version": "2.3.1", | ||
| 992 | "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", | ||
| 993 | "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", | ||
| 994 | "dev": true, | ||
| 995 | "license": "MIT" | ||
| 996 | }, | ||
| 831 | "node_modules/esm-env": { | 997 | "node_modules/esm-env": { |
| 832 | "version": "1.2.2", | 998 | "version": "1.2.2", |
| 833 | "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", | 999 | "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", |
| @@ -853,6 +1019,26 @@ | |||
| 853 | } | 1019 | } |
| 854 | } | 1020 | } |
| 855 | }, | 1021 | }, |
| 1022 | "node_modules/estree-walker": { | ||
| 1023 | "version": "3.0.3", | ||
| 1024 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", | ||
| 1025 | "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", | ||
| 1026 | "dev": true, | ||
| 1027 | "license": "MIT", | ||
| 1028 | "dependencies": { | ||
| 1029 | "@types/estree": "^1.0.0" | ||
| 1030 | } | ||
| 1031 | }, | ||
| 1032 | "node_modules/expect-type": { | ||
| 1033 | "version": "1.4.0", | ||
| 1034 | "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", | ||
| 1035 | "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", | ||
| 1036 | "dev": true, | ||
| 1037 | "license": "Apache-2.0", | ||
| 1038 | "engines": { | ||
| 1039 | "node": ">=12.0.0" | ||
| 1040 | } | ||
| 1041 | }, | ||
| 856 | "node_modules/fast-deep-equal": { | 1042 | "node_modules/fast-deep-equal": { |
| 857 | "version": "3.1.3", | 1043 | "version": "3.1.3", |
| 858 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | 1044 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", |
| @@ -1389,6 +1575,13 @@ | |||
| 1389 | "url": "https://github.com/sponsors/sindresorhus" | 1575 | "url": "https://github.com/sponsors/sindresorhus" |
| 1390 | } | 1576 | } |
| 1391 | }, | 1577 | }, |
| 1578 | "node_modules/pathe": { | ||
| 1579 | "version": "2.0.3", | ||
| 1580 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", | ||
| 1581 | "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", | ||
| 1582 | "dev": true, | ||
| 1583 | "license": "MIT" | ||
| 1584 | }, | ||
| 1392 | "node_modules/picocolors": { | 1585 | "node_modules/picocolors": { |
| 1393 | "version": "1.1.1", | 1586 | "version": "1.1.1", |
| 1394 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", | 1587 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", |
| @@ -1526,6 +1719,13 @@ | |||
| 1526 | "dev": true, | 1719 | "dev": true, |
| 1527 | "license": "MIT" | 1720 | "license": "MIT" |
| 1528 | }, | 1721 | }, |
| 1722 | "node_modules/siginfo": { | ||
| 1723 | "version": "2.0.0", | ||
| 1724 | "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", | ||
| 1725 | "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", | ||
| 1726 | "dev": true, | ||
| 1727 | "license": "ISC" | ||
| 1728 | }, | ||
| 1529 | "node_modules/sirv": { | 1729 | "node_modules/sirv": { |
| 1530 | "version": "3.0.2", | 1730 | "version": "3.0.2", |
| 1531 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", | 1731 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", |
| @@ -1551,6 +1751,20 @@ | |||
| 1551 | "node": ">=0.10.0" | 1751 | "node": ">=0.10.0" |
| 1552 | } | 1752 | } |
| 1553 | }, | 1753 | }, |
| 1754 | "node_modules/stackback": { | ||
| 1755 | "version": "0.0.2", | ||
| 1756 | "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", | ||
| 1757 | "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", | ||
| 1758 | "dev": true, | ||
| 1759 | "license": "MIT" | ||
| 1760 | }, | ||
| 1761 | "node_modules/std-env": { | ||
| 1762 | "version": "4.2.0", | ||
| 1763 | "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", | ||
| 1764 | "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", | ||
| 1765 | "dev": true, | ||
| 1766 | "license": "MIT" | ||
| 1767 | }, | ||
| 1554 | "node_modules/supports-color": { | 1768 | "node_modules/supports-color": { |
| 1555 | "version": "10.2.2", | 1769 | "version": "10.2.2", |
| 1556 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", | 1770 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", |
| @@ -1617,6 +1831,23 @@ | |||
| 1617 | "typescript": ">=5.0.0" | 1831 | "typescript": ">=5.0.0" |
| 1618 | } | 1832 | } |
| 1619 | }, | 1833 | }, |
| 1834 | "node_modules/tinybench": { | ||
| 1835 | "version": "2.9.0", | ||
| 1836 | "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", | ||
| 1837 | "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", | ||
| 1838 | "dev": true, | ||
| 1839 | "license": "MIT" | ||
| 1840 | }, | ||
| 1841 | "node_modules/tinyexec": { | ||
| 1842 | "version": "1.3.0", | ||
| 1843 | "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", | ||
| 1844 | "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", | ||
| 1845 | "dev": true, | ||
| 1846 | "license": "MIT", | ||
| 1847 | "engines": { | ||
| 1848 | "node": ">=18" | ||
| 1849 | } | ||
| 1850 | }, | ||
| 1620 | "node_modules/tinyglobby": { | 1851 | "node_modules/tinyglobby": { |
| 1621 | "version": "0.2.17", | 1852 | "version": "0.2.17", |
| 1622 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", | 1853 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", |
| @@ -1634,6 +1865,16 @@ | |||
| 1634 | "url": "https://github.com/sponsors/SuperchupuDev" | 1865 | "url": "https://github.com/sponsors/SuperchupuDev" |
| 1635 | } | 1866 | } |
| 1636 | }, | 1867 | }, |
| 1868 | "node_modules/tinyrainbow": { | ||
| 1869 | "version": "3.1.1", | ||
| 1870 | "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", | ||
| 1871 | "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", | ||
| 1872 | "dev": true, | ||
| 1873 | "license": "MIT", | ||
| 1874 | "engines": { | ||
| 1875 | "node": ">=14.0.0" | ||
| 1876 | } | ||
| 1877 | }, | ||
| 1637 | "node_modules/totalist": { | 1878 | "node_modules/totalist": { |
| 1638 | "version": "3.0.1", | 1879 | "version": "3.0.1", |
| 1639 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", | 1880 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", |
| @@ -1791,6 +2032,113 @@ | |||
| 1791 | } | 2032 | } |
| 1792 | } | 2033 | } |
| 1793 | }, | 2034 | }, |
| 2035 | "node_modules/vitest": { | ||
| 2036 | "version": "4.1.10", | ||
| 2037 | "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", | ||
| 2038 | "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", | ||
| 2039 | "dev": true, | ||
| 2040 | "license": "MIT", | ||
| 2041 | "dependencies": { | ||
| 2042 | "@vitest/expect": "4.1.10", | ||
| 2043 | "@vitest/mocker": "4.1.10", | ||
| 2044 | "@vitest/pretty-format": "4.1.10", | ||
| 2045 | "@vitest/runner": "4.1.10", | ||
| 2046 | "@vitest/snapshot": "4.1.10", | ||
| 2047 | "@vitest/spy": "4.1.10", | ||
| 2048 | "@vitest/utils": "4.1.10", | ||
| 2049 | "es-module-lexer": "^2.0.0", | ||
| 2050 | "expect-type": "^1.3.0", | ||
| 2051 | "magic-string": "^0.30.21", | ||
| 2052 | "obug": "^2.1.1", | ||
| 2053 | "pathe": "^2.0.3", | ||
| 2054 | "picomatch": "^4.0.3", | ||
| 2055 | "std-env": "^4.0.0-rc.1", | ||
| 2056 | "tinybench": "^2.9.0", | ||
| 2057 | "tinyexec": "^1.0.2", | ||
| 2058 | "tinyglobby": "^0.2.15", | ||
| 2059 | "tinyrainbow": "^3.1.0", | ||
| 2060 | "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", | ||
| 2061 | "why-is-node-running": "^2.3.0" | ||
| 2062 | }, | ||
| 2063 | "bin": { | ||
| 2064 | "vitest": "vitest.mjs" | ||
| 2065 | }, | ||
| 2066 | "engines": { | ||
| 2067 | "node": "^20.0.0 || ^22.0.0 || >=24.0.0" | ||
| 2068 | }, | ||
| 2069 | "funding": { | ||
| 2070 | "url": "https://opencollective.com/vitest" | ||
| 2071 | }, | ||
| 2072 | "peerDependencies": { | ||
| 2073 | "@edge-runtime/vm": "*", | ||
| 2074 | "@opentelemetry/api": "^1.9.0", | ||
| 2075 | "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", | ||
| 2076 | "@vitest/browser-playwright": "4.1.10", | ||
| 2077 | "@vitest/browser-preview": "4.1.10", | ||
| 2078 | "@vitest/browser-webdriverio": "4.1.10", | ||
| 2079 | "@vitest/coverage-istanbul": "4.1.10", | ||
| 2080 | "@vitest/coverage-v8": "4.1.10", | ||
| 2081 | "@vitest/ui": "4.1.10", | ||
| 2082 | "happy-dom": "*", | ||
| 2083 | "jsdom": "*", | ||
| 2084 | "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" | ||
| 2085 | }, | ||
| 2086 | "peerDependenciesMeta": { | ||
| 2087 | "@edge-runtime/vm": { | ||
| 2088 | "optional": true | ||
| 2089 | }, | ||
| 2090 | "@opentelemetry/api": { | ||
| 2091 | "optional": true | ||
| 2092 | }, | ||
| 2093 | "@types/node": { | ||
| 2094 | "optional": true | ||
| 2095 | }, | ||
| 2096 | "@vitest/browser-playwright": { | ||
| 2097 | "optional": true | ||
| 2098 | }, | ||
| 2099 | "@vitest/browser-preview": { | ||
| 2100 | "optional": true | ||
| 2101 | }, | ||
| 2102 | "@vitest/browser-webdriverio": { | ||
| 2103 | "optional": true | ||
| 2104 | }, | ||
| 2105 | "@vitest/coverage-istanbul": { | ||
| 2106 | "optional": true | ||
| 2107 | }, | ||
| 2108 | "@vitest/coverage-v8": { | ||
| 2109 | "optional": true | ||
| 2110 | }, | ||
| 2111 | "@vitest/ui": { | ||
| 2112 | "optional": true | ||
| 2113 | }, | ||
| 2114 | "happy-dom": { | ||
| 2115 | "optional": true | ||
| 2116 | }, | ||
| 2117 | "jsdom": { | ||
| 2118 | "optional": true | ||
| 2119 | }, | ||
| 2120 | "vite": { | ||
| 2121 | "optional": false | ||
| 2122 | } | ||
| 2123 | } | ||
| 2124 | }, | ||
| 2125 | "node_modules/why-is-node-running": { | ||
| 2126 | "version": "2.3.0", | ||
| 2127 | "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", | ||
| 2128 | "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", | ||
| 2129 | "dev": true, | ||
| 2130 | "license": "MIT", | ||
| 2131 | "dependencies": { | ||
| 2132 | "siginfo": "^2.0.0", | ||
| 2133 | "stackback": "0.0.2" | ||
| 2134 | }, | ||
| 2135 | "bin": { | ||
| 2136 | "why-is-node-running": "cli.js" | ||
| 2137 | }, | ||
| 2138 | "engines": { | ||
| 2139 | "node": ">=8" | ||
| 2140 | } | ||
| 2141 | }, | ||
| 1794 | "node_modules/yaml-ast-parser": { | 2142 | "node_modules/yaml-ast-parser": { |
| 1795 | "version": "0.0.43", | 2143 | "version": "0.0.43", |
| 1796 | "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", | 2144 | "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", |
web/package.json
| Old | New | ||
|---|---|---|---|
| @@ -10,7 +10,8 @@ | |||
| 10 | "prepare": "svelte-kit sync || echo ''", | 10 | "prepare": "svelte-kit sync || echo ''", |
| 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", | 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", |
| 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", | 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", |
| 13 | "gen:api": "openapi-typescript ../docs/openapi.json -o src/lib/api-types.ts" | 13 | "gen:api": "openapi-typescript ../docs/openapi.json -o src/lib/api-types.ts", |
| 14 | "test": "vitest run" | ||
| 14 | }, | 15 | }, |
| 15 | "overrides": { | 16 | "overrides": { |
| 16 | "openapi-typescript": { | 17 | "openapi-typescript": { |
| @@ -27,7 +28,8 @@ | |||
| 27 | "svelte": "^5.56.1", | 28 | "svelte": "^5.56.1", |
| 28 | "svelte-check": "^4.6.0", | 29 | "svelte-check": "^4.6.0", |
| 29 | "typescript": "^6.0.3", | 30 | "typescript": "^6.0.3", |
| 30 | "vite": "^8.0.16" | 31 | "vite": "^8.0.16", |
| 32 | "vitest": "^4.1.10" | ||
| 31 | }, | 33 | }, |
| 32 | "dependencies": { | 34 | "dependencies": { |
| 33 | "@xterm/xterm": "^6.0.0" | 35 | "@xterm/xterm": "^6.0.0" |
web/src/lib/fleet.test.ts
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,90 @@ | |||
| 1 | import { beforeEach, describe, expect, test } from 'vitest'; | ||
| 2 | import { fleet, vmTrustStale, type UserCA, type VM } from './fleet.svelte'; | ||
| 3 | import type { components } from './api-types'; | ||
| 4 | |||
| 5 | type TrustedCA = components['schemas']['TrustedCA']; | ||
| 6 | |||
| 7 | const alpha = 'SHA256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; | ||
| 8 | const beta = 'SHA256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; | ||
| 9 | |||
| 10 | function userCA(fingerprint: string, label = 'ca'): UserCA { | ||
| 11 | return { fingerprint, label, pubkey: `ssh-ed25519 AAAA ${label}` }; | ||
| 12 | } | ||
| 13 | |||
| 14 | /** vmTrusting is a VM as the fleet page sees one, carrying the CA set recorded | ||
| 15 | * at its create. Pass null for a VM created before the set was recorded. */ | ||
| 16 | function vmTrusting(trusted_cas: TrustedCA[] | null): VM { | ||
| 17 | return { | ||
| 18 | actual_power: 'running', | ||
| 19 | assigned_ip: '10.0.0.2', | ||
| 20 | created_at: '2026-08-11T09:00:00Z', | ||
| 21 | deleted: false, | ||
| 22 | destroy_at: 0, | ||
| 23 | disk_gb: 10, | ||
| 24 | host_id: 'host-1', | ||
| 25 | id: 'vm-1', | ||
| 26 | image_url: 'https://example.invalid/img.raw', | ||
| 27 | last_error: '', | ||
| 28 | lifecycle: 'ready', | ||
| 29 | mem_mb: 1024, | ||
| 30 | name: 'guest', | ||
| 31 | persistent: false, | ||
| 32 | phase: 'ready', | ||
| 33 | power_state: 'running', | ||
| 34 | status: 'ready', | ||
| 35 | trusted_cas, | ||
| 36 | vcpus: 1 | ||
| 37 | }; | ||
| 38 | } | ||
| 39 | |||
| 40 | beforeEach(() => { | ||
| 41 | fleet.userCAs = []; | ||
| 42 | fleet.userCAsLoaded = false; | ||
| 43 | }); | ||
| 44 | |||
| 45 | describe('vmTrustStale', () => { | ||
| 46 | test('a guest is stale when the tenant holds a CA the guest does not', () => { | ||
| 47 | fleet.userCAs = [userCA(alpha), userCA(beta)]; | ||
| 48 | fleet.userCAsLoaded = true; | ||
| 49 | expect(vmTrustStale(vmTrusting([{ fingerprint: alpha, label: 'ca' }]))).toBe(true); | ||
| 50 | }); | ||
| 51 | |||
| 52 | test('a guest holding every CA the tenant holds is current', () => { | ||
| 53 | fleet.userCAs = [userCA(alpha), userCA(beta)]; | ||
| 54 | fleet.userCAsLoaded = true; | ||
| 55 | expect( | ||
| 56 | vmTrustStale( | ||
| 57 | vmTrusting([ | ||
| 58 | { fingerprint: alpha, label: 'ca' }, | ||
| 59 | { fingerprint: beta, label: 'ca' } | ||
| 60 | ]) | ||
| 61 | ) | ||
| 62 | ).toBe(false); | ||
| 63 | }); | ||
| 64 | |||
| 65 | test('no guest is stale before the tenant CAs have been fetched', () => { | ||
| 66 | fleet.userCAs = [userCA(alpha)]; | ||
| 67 | fleet.userCAsLoaded = false; | ||
| 68 | expect(vmTrustStale(vmTrusting([]))).toBe(false); | ||
| 69 | }); | ||
| 70 | |||
| 71 | test('a guest that recorded no set is never stale, only silent', () => { | ||
| 72 | fleet.userCAs = [userCA(alpha)]; | ||
| 73 | fleet.userCAsLoaded = true; | ||
| 74 | expect(vmTrustStale(vmTrusting(null))).toBe(false); | ||
| 75 | }); | ||
| 76 | |||
| 77 | test('an unreadable fingerprint on either side is not counted as missing', () => { | ||
| 78 | fleet.userCAs = [userCA(alpha), userCA('')]; | ||
| 79 | fleet.userCAsLoaded = true; | ||
| 80 | expect(vmTrustStale(vmTrusting([{ fingerprint: alpha, label: 'ca' }]))).toBe(false); | ||
| 81 | expect( | ||
| 82 | vmTrustStale( | ||
| 83 | vmTrusting([ | ||
| 84 | { fingerprint: alpha, label: 'ca' }, | ||
| 85 | { fingerprint: '', label: 'ca' } | ||
| 86 | ]) | ||
| 87 | ) | ||
| 88 | ).toBe(false); | ||
| 89 | }); | ||
| 90 | }); | ||
web/vite.config.ts
| Old | New | ||
|---|---|---|---|
| @@ -1,6 +1,6 @@ | |||
| 1 | import adapter from '@sveltejs/adapter-static'; | 1 | import adapter from '@sveltejs/adapter-static'; |
| 2 | import { sveltekit } from '@sveltejs/kit/vite'; | 2 | import { sveltekit } from '@sveltejs/kit/vite'; |
| 3 | import { defineConfig } from 'vite'; | 3 | import { defineConfig } from 'vitest/config'; |
| 4 | 4 | ||
| 5 | export default defineConfig({ | 5 | export default defineConfig({ |
| 6 | plugins: [ | 6 | plugins: [ |
| @@ -20,5 +20,13 @@ export default defineConfig({ | |||
| 20 | proxy: { | 20 | proxy: { |
| 21 | '/api': 'http://localhost:8080' | 21 | '/api': 'http://localhost:8080' |
| 22 | } | 22 | } |
| 23 | }, | ||
| 24 | // Console logic is tested where it lives: the same plugin pipeline compiles | ||
| 25 | // the .svelte.ts modules for the test run, so a test imports the real export | ||
| 26 | // rather than a copy of it. Node environment — the rules under test are | ||
| 27 | // pure, and the modules that do touch the DOM guard on `typeof window`. | ||
| 28 | test: { | ||
| 29 | environment: 'node', | ||
| 30 | include: ['src/**/*.test.ts'] | ||
| 23 | } | 31 | } |
| 24 | }); | 32 | }); |