From b1d5f480d872f1b129e7bdedbe11bc731dd6a228 Mon Sep 17 00:00:00 2001 From: Barney Hussey-Yeo Date: Tue, 31 Mar 2026 08:55:30 +0100 Subject: [PATCH 01/19] Add Vitest test harness with in-memory SQLite isolation and socket mocks Set up the test infrastructure for integration testing: - Add vitest, supertest, and @types/supertest as dev dependencies - Create vitest.config.ts with --experimental-sqlite support - Create shared test helpers: setupTestDb/cleanupTestDb for fresh :memory: databases, createSocketMock for capturing emitted events, and factory helpers for projects/workflows/jobs - Add 11 smoke tests proving DB isolation, socket mocking, fixture factories, and WorkflowManager importability all work - Add "test" and "test:watch" scripts to package.json Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 1474 +++++++++++++++++++++++++++++++++++++++- package.json | 9 +- src/test/helpers.ts | 193 ++++++ src/test/smoke.test.ts | 187 +++++ vitest.config.ts | 19 + 5 files changed, 1854 insertions(+), 28 deletions(-) create mode 100644 src/test/helpers.ts create mode 100644 src/test/smoke.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 53a0f2a..3698cef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,15 +28,18 @@ "@types/node": "^20.19.35", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", + "@types/supertest": "^7.2.0", "@types/uuid": "^9.0.8", "@vitejs/plugin-react": "^4.7.0", "concurrently": "^8.2.2", "react": "^18.3.1", "react-dom": "^18.3.1", "socket.io-client": "^4.8.3", + "supertest": "^7.2.2", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^5.4.21" + "vite": "^5.4.21", + "vitest": "^4.1.2" } }, "node_modules/@anthropic-ai/sdk": { @@ -350,6 +353,43 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1156,6 +1196,331 @@ "node": ">= 0.6" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1519,6 +1884,24 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1574,6 +1957,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/compression": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", @@ -1593,6 +1987,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -1602,6 +2003,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1639,6 +2047,13 @@ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1724,6 +2139,30 @@ "@types/node": "*" } }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -1752,6 +2191,92 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xterm/addon-fit": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", @@ -1845,6 +2370,30 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -2014,6 +2563,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2079,6 +2638,29 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -2204,6 +2786,13 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2276,6 +2865,16 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2295,6 +2894,27 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2399,6 +3019,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2411,6 +3038,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2469,6 +3112,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2499,6 +3152,16 @@ "node": ">=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -2584,6 +3247,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -2600,6 +3270,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -2633,6 +3321,41 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2779,6 +3502,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2917,32 +3656,305 @@ "ts-algebra": "^2.0.0" }, "engines": { - "node": ">=16" + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lodash": { @@ -2975,6 +3987,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3122,6 +4144,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3176,6 +4209,13 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3183,6 +4223,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -3193,9 +4246,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==", "dev": true, "funding": [ { @@ -3355,6 +4408,47 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -3648,6 +4742,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/socket.io": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", @@ -3721,6 +4822,13 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3730,6 +4838,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3758,6 +4873,65 @@ "node": ">=8" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3774,6 +4948,50 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4420,6 +5638,193 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "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": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "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", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4435,6 +5840,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 12a80da..8bbf43d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "server:dev": "NODE_OPTIONS='--experimental-sqlite' tsx watch --ignore './data/**' src/server/index.ts", "client:dev": "vite", "build": "tsc -p tsconfig.server.json && vite build", - "server:start": "node dist/server/index.js" + "server:start": "node dist/server/index.js", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@anthropic-ai/sdk": "^0.78.0", @@ -31,14 +33,17 @@ "@types/node": "^20.19.35", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", + "@types/supertest": "^7.2.0", "@types/uuid": "^9.0.8", "@vitejs/plugin-react": "^4.7.0", "concurrently": "^8.2.2", "react": "^18.3.1", "react-dom": "^18.3.1", "socket.io-client": "^4.8.3", + "supertest": "^7.2.2", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vite": "^5.4.21" + "vite": "^5.4.21", + "vitest": "^4.1.2" } } diff --git a/src/test/helpers.ts b/src/test/helpers.ts new file mode 100644 index 0000000..3cbce61 --- /dev/null +++ b/src/test/helpers.ts @@ -0,0 +1,193 @@ +/** + * Shared test helpers for Hurlicane integration tests. + * + * Provides: + * - Fresh in-memory SQLite database per test via setupTestDb / cleanupTestDb + * - SocketManager mock that captures all emitted events + * - Factory helpers for inserting test fixtures + */ +import { vi } from 'vitest'; +import { randomUUID } from 'crypto'; + +// ─── Database Helpers ───────────────────────────────────────────────────────── + +/** + * Initialize a fresh in-memory database with the full schema + migrations. + * This calls the real initDb() from database.ts with ':memory:' which works + * because path.dirname(':memory:') returns '.' and mkdirSync('.', {recursive:true}) + * is a no-op. + * + * Call cleanupTestDb() in afterEach to tear it down. + */ +export async function setupTestDb() { + const { initDb } = await import('../server/db/database.js'); + return initDb(':memory:'); +} + +/** + * Close and discard the current in-memory database. + */ +export async function cleanupTestDb() { + const { closeDb } = await import('../server/db/database.js'); + closeDb(); +} + +// ─── Socket Mock ────────────────────────────────────────────────────────────── + +export interface SocketMockCalls { + emitJobNew: any[]; + emitJobUpdate: any[]; + emitWorkflowNew: any[]; + emitWorkflowUpdate: any[]; + emitDebateNew: any[]; + emitDebateUpdate: any[]; + emitSnapshot: any[]; + emitAgentNew: any[]; + emitAgentUpdate: any[]; + emitProjectNew: any[]; + [key: string]: any[]; +} + +/** + * Create a vi.mock factory for '../socket/SocketManager.js' (or the path you + * need). Returns an object whose keys are the emit function names and values + * are arrays of call arguments — so you can assert exactly what was emitted. + * + * Usage in a test file: + * ```ts + * vi.mock('../server/socket/SocketManager.js', () => createSocketMock()); + * ``` + * + * Access the mock's recorded calls via the returned object, or via + * `vi.mocked(socket.emitJobNew).mock.calls`. + */ +export function createSocketMock() { + return { + initSocketManager: vi.fn(), + getIo: vi.fn(() => ({ emit: vi.fn() })), + emitSnapshot: vi.fn(), + emitAgentNew: vi.fn(), + emitAgentUpdate: vi.fn(), + emitAgentOutput: vi.fn(), + emitQuestionNew: vi.fn(), + emitQuestionAnswered: vi.fn(), + emitLockAcquired: vi.fn(), + emitLockReleased: vi.fn(), + emitProjectNew: vi.fn(), + emitJobNew: vi.fn(), + emitJobUpdate: vi.fn(), + emitPtyData: vi.fn(), + emitPtyClosed: vi.fn(), + emitDebateNew: vi.fn(), + emitDebateUpdate: vi.fn(), + emitWorkflowNew: vi.fn(), + emitWorkflowUpdate: vi.fn(), + emitWarningNew: vi.fn(), + emitDiscussionNew: vi.fn(), + emitDiscussionMessage: vi.fn(), + emitDiscussionUpdate: vi.fn(), + emitProposalNew: vi.fn(), + emitProposalUpdate: vi.fn(), + emitProposalMessage: vi.fn(), + emitPrNew: vi.fn(), + emitPrReviewNew: vi.fn(), + emitPrReviewUpdate: vi.fn(), + emitPrReviewMessage: vi.fn(), + }; +} + +// ─── Fixture Factories ──────────────────────────────────────────────────────── + +/** + * Insert a minimal project into the DB and return its id. + */ +export async function insertTestProject(overrides: { id?: string; name?: string } = {}) { + const { insertProject } = await import('../server/db/queries.js'); + const id = overrides.id ?? randomUUID(); + return insertProject({ + id, + name: overrides.name ?? 'Test Project', + description: 'test project', + created_at: Date.now(), + updated_at: Date.now(), + }); +} + +/** + * Insert a minimal workflow into the DB and return it. + */ +export async function insertTestWorkflow(overrides: Partial<{ + id: string; + title: string; + task: string; + work_dir: string | null; + implementer_model: string; + reviewer_model: string; + max_cycles: number; + current_cycle: number; + current_phase: string; + status: string; + milestones_total: number; + milestones_done: number; + project_id: string | null; + template_id: string | null; + use_worktree: number; +}> = {}) { + const { insertWorkflow } = await import('../server/db/queries.js'); + const id = overrides.id ?? randomUUID(); + return insertWorkflow({ + id, + title: overrides.title ?? 'Test Workflow', + task: overrides.task ?? 'Test task', + work_dir: overrides.work_dir ?? '/tmp/test', + implementer_model: overrides.implementer_model ?? 'claude-sonnet-4-6', + reviewer_model: overrides.reviewer_model ?? 'codex', + max_cycles: overrides.max_cycles ?? 10, + current_cycle: overrides.current_cycle ?? 0, + current_phase: (overrides.current_phase ?? 'idle') as any, + status: (overrides.status ?? 'running') as any, + milestones_total: overrides.milestones_total ?? 0, + milestones_done: overrides.milestones_done ?? 0, + project_id: overrides.project_id ?? null, + max_turns_assess: 50, + max_turns_review: 30, + max_turns_implement: 100, + template_id: overrides.template_id ?? null, + use_worktree: overrides.use_worktree ?? 0, + created_at: Date.now(), + updated_at: Date.now(), + }); +} + +/** + * Insert a minimal job into the DB and return it. + */ +export async function insertTestJob(overrides: Partial<{ + id: string; + title: string; + description: string; + status: string; + priority: number; + workflow_id: string | null; + workflow_cycle: number | null; + workflow_phase: string | null; + project_id: string | null; + work_dir: string | null; + model: string | null; +}> = {}) { + const { insertJob } = await import('../server/db/queries.js'); + return insertJob({ + id: overrides.id ?? randomUUID(), + title: overrides.title ?? 'Test Job', + description: overrides.description ?? 'Test job description', + context: null, + priority: overrides.priority ?? 0, + status: (overrides.status ?? 'queued') as any, + workflow_id: overrides.workflow_id ?? null, + workflow_cycle: overrides.workflow_cycle ?? null, + workflow_phase: (overrides.workflow_phase ?? null) as any, + project_id: overrides.project_id ?? null, + work_dir: overrides.work_dir ?? null, + model: overrides.model ?? null, + }); +} diff --git a/src/test/smoke.test.ts b/src/test/smoke.test.ts new file mode 100644 index 0000000..a8433d9 --- /dev/null +++ b/src/test/smoke.test.ts @@ -0,0 +1,187 @@ +/** + * Smoke tests proving the test harness works: + * - In-memory SQLite DB initializes with full schema + * - Queries work against the in-memory DB + * - Each test gets an isolated DB (no cross-test state leakage) + * - SocketManager mock captures emitted events + * - Module-level singleton state can be reset between tests + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { setupTestDb, cleanupTestDb, createSocketMock, insertTestProject, insertTestWorkflow, insertTestJob } from './helpers.js'; + +// Mock SocketManager before any module that imports it +vi.mock('../server/socket/SocketManager.js', () => createSocketMock()); + +describe('Test Harness: In-memory DB', () => { + beforeEach(async () => { + await setupTestDb(); + }); + + afterEach(async () => { + await cleanupTestDb(); + }); + + it('initializes the full schema in :memory:', async () => { + const { getDb } = await import('../server/db/database.js'); + const db = getDb(); + // Verify key tables exist + const tables = db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + ).all() as { name: string }[]; + const tableNames = tables.map((t) => t.name); + expect(tableNames).toContain('jobs'); + expect(tableNames).toContain('agents'); + expect(tableNames).toContain('workflows'); + expect(tableNames).toContain('notes'); + expect(tableNames).toContain('debates'); + expect(tableNames).toContain('projects'); + }); + + it('inserts and retrieves a job via queries module', async () => { + const { insertJob, getJobById } = await import('../server/db/queries.js'); + const job = insertJob({ + id: 'test-job-1', + title: 'Smoke Test Job', + description: 'A job for testing', + context: null, + priority: 5, + }); + expect(job.id).toBe('test-job-1'); + expect(job.title).toBe('Smoke Test Job'); + expect(job.status).toBe('queued'); + expect(job.priority).toBe(5); + + const fetched = getJobById('test-job-1'); + expect(fetched).not.toBeNull(); + expect(fetched!.title).toBe('Smoke Test Job'); + }); + + it('inserts and retrieves a workflow', async () => { + const project = await insertTestProject(); + const workflow = await insertTestWorkflow({ project_id: project.id }); + expect(workflow.id).toBeTruthy(); + expect(workflow.status).toBe('running'); + expect(workflow.current_phase).toBe('idle'); + + const { getWorkflowById } = await import('../server/db/queries.js'); + const fetched = getWorkflowById(workflow.id); + expect(fetched).not.toBeNull(); + expect(fetched!.title).toBe('Test Workflow'); + }); + + it('provides isolation between tests (no leftover data)', async () => { + const { getJobById } = await import('../server/db/queries.js'); + // This job was inserted in the previous test — it should not exist here + const ghost = getJobById('test-job-1'); + expect(ghost).toBeNull(); + }); +}); + +describe('Test Harness: Socket Mock', () => { + beforeEach(async () => { + await setupTestDb(); + vi.clearAllMocks(); + }); + + afterEach(async () => { + await cleanupTestDb(); + }); + + it('captures emitJobNew calls', async () => { + const socket = await import('../server/socket/SocketManager.js'); + const { insertJob } = await import('../server/db/queries.js'); + + const job = insertJob({ + id: 'mock-test-job', + title: 'Mock Test', + description: 'Testing socket mock', + context: null, + priority: 0, + }); + socket.emitJobNew(job); + + expect(socket.emitJobNew).toHaveBeenCalledTimes(1); + expect(vi.mocked(socket.emitJobNew).mock.calls[0][0].id).toBe('mock-test-job'); + }); + + it('captures emitWorkflowUpdate calls', async () => { + const socket = await import('../server/socket/SocketManager.js'); + const project = await insertTestProject(); + const workflow = await insertTestWorkflow({ project_id: project.id }); + + socket.emitWorkflowUpdate(workflow); + + expect(socket.emitWorkflowUpdate).toHaveBeenCalledTimes(1); + expect(vi.mocked(socket.emitWorkflowUpdate).mock.calls[0][0].id).toBe(workflow.id); + }); +}); + +describe('Test Harness: Fixture Factories', () => { + beforeEach(async () => { + await setupTestDb(); + }); + + afterEach(async () => { + await cleanupTestDb(); + }); + + it('insertTestJob creates a job with overrides', async () => { + const job = await insertTestJob({ + title: 'Custom Job', + status: 'running', + priority: 10, + }); + expect(job.title).toBe('Custom Job'); + expect(job.status).toBe('running'); + expect(job.priority).toBe(10); + }); + + it('insertTestWorkflow creates a workflow linked to a project', async () => { + const project = await insertTestProject({ name: 'My Project' }); + const workflow = await insertTestWorkflow({ + project_id: project.id, + max_cycles: 5, + current_phase: 'assess', + }); + expect(workflow.project_id).toBe(project.id); + expect(workflow.max_cycles).toBe(5); + expect(workflow.current_phase).toBe('assess'); + }); + + it('insertTestJob supports workflow fields', async () => { + const project = await insertTestProject(); + const workflow = await insertTestWorkflow({ project_id: project.id }); + const job = await insertTestJob({ + workflow_id: workflow.id, + workflow_cycle: 1, + workflow_phase: 'review', + }); + expect(job.workflow_id).toBe(workflow.id); + expect(job.workflow_cycle).toBe(1); + expect(job.workflow_phase).toBe('review'); + }); +}); + +describe('Test Harness: Module state reset', () => { + beforeEach(async () => { + await setupTestDb(); + }); + + afterEach(async () => { + await cleanupTestDb(); + }); + + it('parseMilestones is importable and works with test data', async () => { + const { parseMilestones } = await import('../server/orchestrator/WorkflowManager.js'); + const result = parseMilestones('- [x] Done\n- [ ] Not done\n- [ ] Also not done'); + expect(result.total).toBe(3); + expect(result.done).toBe(1); + }); + + it('parseMilestones returns zeros for empty input', async () => { + const { parseMilestones } = await import('../server/orchestrator/WorkflowManager.js'); + const result = parseMilestones(''); + expect(result.total).toBe(0); + expect(result.done).toBe(0); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..6803bd5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + resolve: { + alias: { + '@shared': path.resolve(__dirname, 'src/shared'), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['src/test/**/*.test.ts'], + // Each test file gets a fresh module graph so singleton state doesn't leak + isolate: true, + // node:sqlite requires this flag (vitest v4 top-level config) + execArgv: ['--experimental-sqlite'], + }, +}); From f7cb787537dbf49c6e74523c37cabf8662b73de5 Mon Sep 17 00:00:00 2001 From: Barney Hussey-Yeo Date: Tue, 31 Mar 2026 09:07:55 +0100 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20add=20autonomous=20agent=20workfl?= =?UTF-8?q?ow=20system=20(assess=20=E2=86=92=20review=20=E2=86=92=20implem?= =?UTF-8?q?ent=20cycle)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the structured plan/review/implement cycle from autonomous-coding-agents into Hurlicane as a first-class feature. Each workflow runs Claude as implementer and Codex as reviewer in a repeating cycle until all milestones are complete. Key changes: - New workflows table + job columns (workflow_id, workflow_cycle, workflow_phase) - WorkflowManager: event-driven orchestrator modelled after DebateManager - WorkflowPrompts: phase-specific prompts using notes as shared artifacts - Single shared worktree per workflow (created once at start, all phases share one branch so changes accumulate linearly across cycles) - Auto PR creation + worktree cleanup on workflow completion via gh CLI - Codex review phase (cycle 2+) does full code review via git diff before updating the plan — adds Fix: milestones for any quality issues found - isAutoExitJob() helper replaces debate_role checks across 8 call sites so workflow phase jobs also exit cleanly without calling finish_job - PtyManager: PTY attach failure is a warning (not job failure) for auto-exit jobs since tailing already captures output - REST API: GET/POST /api/workflows, cancel, resume endpoints - UI: Autonomous Agents button, WorkflowForm, WorkflowDetailModal with milestone progress bar, plan/worklog/jobs tabs, View PR link Co-Authored-By: Claude Sonnet 4.6 --- src/client/App.tsx | 44 +- src/client/components/Header.tsx | 34 +- src/client/components/WorkflowDetailModal.tsx | 256 +++++++++++ src/client/components/WorkflowForm.tsx | 195 ++++++++ src/client/hooks/useSocket.ts | 6 +- src/client/hooks/useWorkflows.ts | 20 + src/server/api/router.ts | 2 + src/server/api/workflows.ts | 143 ++++++ src/server/db/database.ts | 52 ++- src/server/db/queries.ts | 66 ++- src/server/index.ts | 1 + src/server/orchestrator/AgentRunner.ts | 2 + src/server/orchestrator/PtyManager.ts | 21 +- src/server/orchestrator/StuckJobWatchdog.ts | 6 +- src/server/orchestrator/WorkQueueManager.ts | 6 +- src/server/orchestrator/WorkflowManager.ts | 417 ++++++++++++++++++ src/server/orchestrator/WorkflowPrompts.ts | 215 +++++++++ src/server/orchestrator/recovery.ts | 8 +- src/server/socket/SocketManager.ts | 10 +- src/shared/types.ts | 62 +++ 20 files changed, 1539 insertions(+), 27 deletions(-) create mode 100644 src/client/components/WorkflowDetailModal.tsx create mode 100644 src/client/components/WorkflowForm.tsx create mode 100644 src/client/hooks/useWorkflows.ts create mode 100644 src/server/api/workflows.ts create mode 100644 src/server/orchestrator/WorkflowManager.ts create mode 100644 src/server/orchestrator/WorkflowPrompts.ts diff --git a/src/client/App.tsx b/src/client/App.tsx index a90f61a..cb3b99d 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -20,6 +20,8 @@ const ProjectSelector = lazy(() => import('./components/ProjectSelector').then(m const SettingsModal = lazy(() => import('./components/SettingsModal').then(m => ({ default: m.SettingsModal }))); const DebateForm = lazy(() => import('./components/DebateForm').then(m => ({ default: m.DebateForm }))); const DebateDetailModal = lazy(() => import('./components/DebateDetailModal').then(m => ({ default: m.DebateDetailModal }))); +const WorkflowForm = lazy(() => import('./components/WorkflowForm').then(m => ({ default: m.WorkflowForm }))); +const WorkflowDetailModal = lazy(() => import('./components/WorkflowDetailModal').then(m => ({ default: m.WorkflowDetailModal }))); const KnowledgeBaseModal = lazy(() => import('./components/KnowledgeBaseModal').then(m => ({ default: m.KnowledgeBaseModal }))); import { useSocket } from './hooks/useSocket'; import { useAgents } from './hooks/useAgents'; @@ -27,10 +29,11 @@ import { useJobs } from './hooks/useJobs'; import { useLocks } from './hooks/useLocks'; import { useProjects } from './hooks/useProjects'; import { useDebates } from './hooks/useDebates'; +import { useWorkflows } from './hooks/useWorkflows'; import { useToasts } from './hooks/useToasts'; import { ToastFeed } from './components/ToastFeed'; import socket from './socket'; -import type { AgentWithJob, AgentOutput, CreateJobRequest, CreateDebateRequest, Debate, Job, Template, BatchTemplate, Discussion, Proposal } from '@shared/types'; +import type { AgentWithJob, AgentOutput, CreateJobRequest, CreateDebateRequest, CreateWorkflowRequest, Debate, Workflow, Job, Template, BatchTemplate, Discussion, Proposal } from '@shared/types'; export default function App() { const { agents, setInitial: setInitialAgents, addAgent, updateAgent } = useAgents(); @@ -38,6 +41,7 @@ export default function App() { const { locks, setInitial: setInitialLocks, addLock, removeLock } = useLocks(); const { projects, setInitial: setInitialProjects, addProject, updateProject, removeProject } = useProjects(); const { debates, setInitial: setInitialDebates, addDebate, updateDebate: updateDebateState } = useDebates(); + const { workflows, setInitial: setInitialWorkflows, addWorkflow, updateWorkflow: updateWorkflowState } = useWorkflows(); const { toasts, dismiss: dismissToast } = useToasts(); const [templates, setTemplates] = useState([]); @@ -54,6 +58,8 @@ export default function App() { const [showDebateForm, setShowDebateForm] = useState(false); const [debateFormInitial, setDebateFormInitial] = useState | undefined>(); const [selectedDebate, setSelectedDebate] = useState(null); + const [showWorkflowForm, setShowWorkflowForm] = useState(false); + const [selectedWorkflow, setSelectedWorkflow] = useState(null); const [showKnowledgeBase, setShowKnowledgeBase] = useState(false); const [activeProjectId, setActiveProjectId] = useState(null); const [archivedJobs, setArchivedJobs] = useState([]); @@ -153,6 +159,7 @@ export default function App() { setTemplates(snapshot.templates ?? []); setInitialProjects(snapshot.projects ?? []); setInitialDebates(snapshot.debates ?? []); + setInitialWorkflows(snapshot.workflows ?? []); setDiscussions(snapshot.discussions ?? []); setProposals(snapshot.proposals ?? []); }, @@ -174,6 +181,8 @@ export default function App() { onProjectNew: addProject, onDebateNew: addDebate, onDebateUpdate: updateDebateState, + onWorkflowNew: addWorkflow, + onWorkflowUpdate: updateWorkflowState, onDiscussionNew: (discussion: Discussion) => setDiscussions(prev => [discussion, ...prev.filter(d => d.id !== discussion.id)]), onDiscussionUpdate: (discussion: Discussion) => setDiscussions(prev => prev.map(d => d.id === discussion.id ? discussion : d)), onProposalNew: (proposal: Proposal) => setProposals(prev => [proposal, ...prev.filter(p => p.id !== proposal.id)]), @@ -328,6 +337,21 @@ export default function App() { } }, [activeProjectId]); + const handleSubmitWorkflow = useCallback(async (req: CreateWorkflowRequest) => { + const res = await fetch('/api/workflows', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error ?? 'Failed to create workflow'); + } + const data = await res.json(); + addProject(data.project); + setActiveProjectId(data.project.id); + }, [addProject]); + const handleSubmitDebate = useCallback(async (req: CreateDebateRequest) => { const res = await fetch('/api/debates', { method: 'POST', @@ -381,7 +405,7 @@ export default function App() { return (
-
setShowJobForm(true)} onTemplates={() => setShowTemplates(true)} onBatchTemplates={() => setShowBatchTemplates(true)} onUsage={() => setShowUsage(true)} onSearch={() => setShowSearch(true)} onTimeline={() => setShowGantt(true)} onDag={() => setShowDag(true)} onProjects={() => setShowProjects(true)} onSettings={() => setShowSettings(true)} onDebate={() => { setDebateFormInitial(undefined); setShowDebateForm(true); }} onDebates={debates.length > 0 ? debates : undefined} onSelectDebate={(d) => setSelectedDebate(d)} onKnowledgeBase={() => setShowKnowledgeBase(true)} onEye={() => setShowEye(v => !v)} eyeEnabled={eyeEnabled} eyeActive={showEye} eyeBadgeCount={showEye ? 0 : discussions.filter(d => d.needs_reply).length + proposals.filter(p => p.needs_reply).length} onHome={() => { setSelectedAgent(null); setActiveProjectId(null); setShowJobForm(false); setShowTemplates(false); setShowBatchTemplates(false); setShowUsage(false); setShowSearch(false); setShowGantt(false); setShowDag(false); setShowProjects(false); setShowSettings(false); setShowDebateForm(false); setShowKnowledgeBase(false); setShowEye(false); }} currentProjectName={activeProjectName} onClearProject={() => setActiveProjectId(null)} todayClaudeCost={todayClaudeCost ?? undefined} todayCodexCost={todayCodexCost ?? undefined} costAutoUpdate={costAutoUpdate} onToggleCostAutoUpdate={() => setCostAutoUpdate(v => !v)} /> +
setShowJobForm(true)} onTemplates={() => setShowTemplates(true)} onBatchTemplates={() => setShowBatchTemplates(true)} onUsage={() => setShowUsage(true)} onSearch={() => setShowSearch(true)} onTimeline={() => setShowGantt(true)} onDag={() => setShowDag(true)} onProjects={() => setShowProjects(true)} onSettings={() => setShowSettings(true)} onDebate={() => { setDebateFormInitial(undefined); setShowDebateForm(true); }} onDebates={debates.length > 0 ? debates : undefined} onSelectDebate={(d) => setSelectedDebate(d)} onWorkflow={() => setShowWorkflowForm(true)} onWorkflows={workflows.length > 0 ? workflows : undefined} onSelectWorkflow={(w) => setSelectedWorkflow(w)} onKnowledgeBase={() => setShowKnowledgeBase(true)} onEye={() => setShowEye(v => !v)} eyeEnabled={eyeEnabled} eyeActive={showEye} eyeBadgeCount={showEye ? 0 : discussions.filter(d => d.needs_reply).length + proposals.filter(p => p.needs_reply).length} onHome={() => { setSelectedAgent(null); setActiveProjectId(null); setShowJobForm(false); setShowTemplates(false); setShowBatchTemplates(false); setShowUsage(false); setShowSearch(false); setShowGantt(false); setShowDag(false); setShowProjects(false); setShowSettings(false); setShowDebateForm(false); setShowWorkflowForm(false); setShowKnowledgeBase(false); setShowEye(false); }} currentProjectName={activeProjectName} onClearProject={() => setActiveProjectId(null)} todayClaudeCost={todayClaudeCost ?? undefined} todayCodexCost={todayCodexCost ?? undefined} costAutoUpdate={costAutoUpdate} onToggleCostAutoUpdate={() => setCostAutoUpdate(v => !v)} />
@@ -504,6 +528,22 @@ export default function App() { setShowSettings(false)} eyeEnabled={eyeEnabled} onEyeEnabledChange={setEyeEnabled} /> )} + {showWorkflowForm && ( + setShowWorkflowForm(false)} + /> + )} + + {selectedWorkflow && ( + w.id === selectedWorkflow.id) ?? selectedWorkflow} + agents={agents} + onClose={() => setSelectedWorkflow(null)} + onWorkflowUpdate={updateWorkflowState} + /> + )} + {showDebateForm && ( void; @@ -14,6 +14,9 @@ interface HeaderProps { onDebate: () => void; onDebates?: Debate[]; onSelectDebate?: (debate: Debate) => void; + onWorkflow: () => void; + onWorkflows?: Workflow[]; + onSelectWorkflow?: (workflow: Workflow) => void; onKnowledgeBase: () => void; onEye: () => void; eyeActive?: boolean; @@ -53,12 +56,14 @@ function HurlicaLogo() { ); } -export function Header({ onNewJob, onTemplates, onBatchTemplates, onUsage, onSearch, onTimeline, onDag, onProjects, onSettings, onDebate, onDebates, onSelectDebate, onKnowledgeBase, onEye, eyeActive, eyeBadgeCount, eyeEnabled, onHome, currentProjectName, onClearProject, todayClaudeCost, todayCodexCost, costAutoUpdate, onToggleCostAutoUpdate }: HeaderProps) { +export function Header({ onNewJob, onTemplates, onBatchTemplates, onUsage, onSearch, onTimeline, onDag, onProjects, onSettings, onDebate, onDebates, onSelectDebate, onWorkflow, onWorkflows, onSelectWorkflow, onKnowledgeBase, onEye, eyeActive, eyeBadgeCount, eyeEnabled, onHome, currentProjectName, onClearProject, todayClaudeCost, todayCodexCost, costAutoUpdate, onToggleCostAutoUpdate }: HeaderProps) { const hasCost = (todayClaudeCost != null && todayClaudeCost > 0) || (todayCodexCost != null && todayCodexCost > 0); const [moreOpen, setMoreOpen] = useState(false); const moreRef = useRef(null); const [debateMenuOpen, setDebateMenuOpen] = useState(false); const debateMenuRef = useRef(null); + const [workflowMenuOpen, setWorkflowMenuOpen] = useState(false); + const workflowMenuRef = useRef(null); useEffect(() => { if (!moreOpen) return; const handler = (e: MouseEvent) => { if (!moreRef.current?.contains(e.target as Node)) setMoreOpen(false); }; @@ -124,6 +129,31 @@ export function Header({ onNewJob, onTemplates, onBatchTemplates, onUsage, onSea )} )} +
+ + {onWorkflows && onWorkflows.length > 0 && ( +
+ + {workflowMenuOpen && ( +
+
Autonomous Agents
+ {[...onWorkflows].sort((a, b) => b.updated_at - a.updated_at).map(w => { + const statusColor = w.status === 'running' ? 'var(--status-running)' : w.status === 'complete' ? 'var(--status-done)' : w.status === 'blocked' ? '#f59e0b' : 'var(--status-failed)'; + return ( + + ); + })} +
+ )} +
+ )} +
{onDebates && onDebates.length > 0 && ( diff --git a/src/client/components/WorkflowDetailModal.tsx b/src/client/components/WorkflowDetailModal.tsx new file mode 100644 index 0000000..ee6eff1 --- /dev/null +++ b/src/client/components/WorkflowDetailModal.tsx @@ -0,0 +1,256 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import type { Workflow, Job, AgentWithJob } from '@shared/types'; + +interface WorkflowDetail extends Workflow { + plan: string | null; + contract: string | null; + worklogs: Array<{ key: string; value: string; updated_at: number }>; +} + +interface WorkflowDetailModalProps { + workflow: Workflow; + agents: AgentWithJob[]; + onClose: () => void; + onWorkflowUpdate: (workflow: Workflow) => void; +} + +const STATUS_COLORS: Record = { + running: '#22c55e', + complete: '#3b82f6', + blocked: '#f59e0b', + failed: '#ef4444', + cancelled: '#6b7280', +}; + +const PHASE_LABELS: Record = { + idle: 'Idle', + assess: 'Assess', + review: 'Review', + implement: 'Implement', +}; + +export function WorkflowDetailModal({ workflow, agents, onClose, onWorkflowUpdate }: WorkflowDetailModalProps) { + const [detail, setDetail] = useState(null); + const [jobs, setJobs] = useState([]); + const [activeTab, setActiveTab] = useState<'progress' | 'plan' | 'worklog' | 'jobs'>('progress'); + const [loading, setLoading] = useState(true); + const [acting, setActing] = useState(false); + + const fetchDetail = useCallback(async () => { + try { + const [detailRes, jobsRes] = await Promise.all([ + fetch(`/api/workflows/${workflow.id}`), + fetch(`/api/workflows/${workflow.id}/jobs`), + ]); + if (detailRes.ok) setDetail(await detailRes.json()); + if (jobsRes.ok) setJobs(await jobsRes.json()); + } catch { /* ignore */ } finally { + setLoading(false); + } + }, [workflow.id]); + + useEffect(() => { fetchDetail(); }, [fetchDetail]); + + const handleCancel = async () => { + if (!confirm('Cancel this workflow?')) return; + setActing(true); + try { + const res = await fetch(`/api/workflows/${workflow.id}/cancel`, { method: 'POST' }); + if (res.ok) { + const updated = await res.json(); + onWorkflowUpdate(updated); + onClose(); + } + } finally { setActing(false); } + }; + + const handleResume = async () => { + setActing(true); + try { + const res = await fetch(`/api/workflows/${workflow.id}/resume`, { method: 'POST' }); + if (res.ok) { + const data = await res.json(); + onWorkflowUpdate(data.workflow); + await fetchDetail(); + } + } finally { setActing(false); } + }; + + const milestonePercent = workflow.milestones_total > 0 + ? Math.round((workflow.milestones_done / workflow.milestones_total) * 100) + : 0; + + const statusColor = STATUS_COLORS[workflow.status] ?? '#6b7280'; + + return ( +
+
e.stopPropagation()}> +
+
+

{workflow.title}

+
+ {workflow.status.toUpperCase()} + Cycle {workflow.current_cycle}/{workflow.max_cycles} + Phase: {PHASE_LABELS[workflow.current_phase] ?? workflow.current_phase} + {workflow.implementer_model} + {workflow.reviewer_model} +
+
+
+ {workflow.pr_url && ( + + View PR ↗ + + )} + {workflow.status === 'blocked' && ( + + )} + {(workflow.status === 'running' || workflow.status === 'blocked') && ( + + )} + +
+
+ + {/* Milestone progress bar */} +
+
+ Milestones: {workflow.milestones_done}/{workflow.milestones_total} + {milestonePercent}% +
+
+
+
+
+ + {/* Tabs */} +
+ {(['progress', 'plan', 'worklog', 'jobs'] as const).map(tab => ( + + ))} +
+ +
+ {loading ? ( +
Loading...
+ ) : ( + <> + {activeTab === 'progress' && ( +
+
+

Task

+
+ {workflow.task} +
+
+ +

Phase Timeline

+
+ {jobs.map(job => { + const agent = agents.find(a => a.job_id === job.id); + const statusDot: Record = { done: '#22c55e', failed: '#ef4444', running: '#3b82f6', queued: '#6b7280', assigned: '#f59e0b', cancelled: '#6b7280' }; + return ( +
+ + {job.title} + {job.status} + {agent?.cost_usd != null && ${agent.cost_usd.toFixed(4)}} +
+ ); + })} + {jobs.length === 0 &&
No jobs yet.
} +
+
+ )} + + {activeTab === 'plan' && ( +
+ {detail?.plan ? ( +
+                      {detail.plan}
+                    
+ ) : ( +
+ No plan written yet. The assess phase will create it. +
+ )} +
+ )} + + {activeTab === 'worklog' && ( +
+ {detail?.worklogs && detail.worklogs.length > 0 ? ( + detail.worklogs.map(entry => ( +
+
+ {entry.key.split('/').pop()} — {new Date(entry.updated_at).toLocaleString()} +
+
+                          {entry.value}
+                        
+
+ )) + ) : ( +
+ No worklog entries yet. The implement phase will write them. +
+ )} +
+ )} + + {activeTab === 'jobs' && ( +
+ + + + + + + + + + + + + {jobs.map(job => { + const agent = agents.find(a => a.job_id === job.id); + return ( + + + + + + + + + ); + })} + {jobs.length === 0 && ( + + )} + +
JobPhaseCycleStatusModelCost
{job.title}{job.workflow_phase ?? '-'}{job.workflow_cycle ?? '-'}{job.status}{job.model ?? 'auto'}{agent?.cost_usd != null ? `$${agent.cost_usd.toFixed(4)}` : '-'}
No jobs yet
+
+ )} + + )} +
+
+
+ ); +} diff --git a/src/client/components/WorkflowForm.tsx b/src/client/components/WorkflowForm.tsx new file mode 100644 index 0000000..6f71004 --- /dev/null +++ b/src/client/components/WorkflowForm.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect } from 'react'; +import type { CreateWorkflowRequest, Template } from '@shared/types'; +import { useModels } from '../hooks/useModels'; + +interface WorkflowFormProps { + onSubmit: (req: CreateWorkflowRequest) => Promise; + onClose: () => void; +} + +export function WorkflowForm({ onSubmit, onClose }: WorkflowFormProps) { + const { claude: claudeModels, codex: codexModels } = useModels(); + const [title, setTitle] = useState(''); + const [task, setTask] = useState(''); + const [workDir, setWorkDir] = useState(''); + const [implementerModel, setImplementerModel] = useState('claude-sonnet-4-6[1m]'); + const [reviewerModel, setReviewerModel] = useState('codex'); + const [maxCycles, setMaxCycles] = useState(10); + const [templateId, setTemplateId] = useState(''); + const [useWorktree, setUseWorktree] = useState(true); + const [showAdvanced, setShowAdvanced] = useState(false); + const [maxTurnsAssess, setMaxTurnsAssess] = useState(50); + const [maxTurnsReview, setMaxTurnsReview] = useState(30); + const [maxTurnsImplement, setMaxTurnsImplement] = useState(100); + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + fetch('/api/templates').then(r => r.json()).then(setTemplates).catch(console.error); + }, []); + + const handleTemplateChange = (newTemplateId: string) => { + setTemplateId(newTemplateId); + const tpl = templates.find(t => t.id === newTemplateId); + if (tpl?.work_dir) setWorkDir(tpl.work_dir); + if (tpl?.model) setImplementerModel(tpl.model); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!task.trim()) return; + setLoading(true); + setError(''); + try { + await onSubmit({ + title: title.trim() || undefined, + task: task.trim(), + workDir: workDir.trim() || undefined, + implementerModel, + reviewerModel, + maxCycles, + templateId: templateId || undefined, + useWorktree, + maxTurnsAssess, + maxTurnsReview, + maxTurnsImplement, + }); + onClose(); + } catch (err: any) { + setError(err.message ?? 'Failed to create workflow'); + } finally { + setLoading(false); + } + }; + + return ( +
+
e.stopPropagation()}> +
+

New Autonomous Agent Run

+ +
+
+
+ + setTitle(e.target.value)} + placeholder="Auto-generated from task if blank" + autoFocus + /> +
+ +
+ +