diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 236c3bd..6bd682b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,7 @@ updates: patterns: - '@biomejs/*' - '@types/*' + - '@vitest/*' - typescript - vitest ignore: diff --git a/AGENTS.md b/AGENTS.md index 3b034bf..81e1032 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ provider SPI, mock adapter, SDK-facing types, and conformance checks. ## Commands - Install: `pnpm install` -- Full check (including build, coverage, docs, and package dry-run): `pnpm check` +- Full check (including build, coverage, docs, and actual tarball validation): `pnpm check` - Build: `pnpm build` - Tests: `pnpm test` - Coverage: `pnpm test:coverage` diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb79ce..377fd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +- allow generation-fenced termination during ambiguous pause/resume transitions; +- bound every conformance Provider call, validate timeout options, and verify cleanup state; +- use unique conformance create-intent keys and reject unsuccessful recovery before data-plane probes; +- upgrade development tooling with matching Vitest/coverage versions and group future upgrades; +- update the transitive development dependency `source-map-js` to 1.2.2 for + [GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q) and gate all dependencies + against high-severity advisories; +- package documentation and diagrams, and validate actual tarball links and runtime entrypoints; +- clarify shipped SDK/transport behavior and add verified v0.1 release evidence. - add three validated AnyCap explanatory diagrams and two deterministic contract diagrams; - add text equivalents, visual provenance, integrity hashes, multi-size validation evidence, and generated-image metadata scanning. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0adbb8..5ebfcae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,8 @@ pnpm test:coverage pnpm pack:check ``` -Use Node.js 22 or later. Keep each change scoped to one protocol or provider concern and add tests +Use Node.js 22.12+ (22.x) or 24.x for development. Keep each change scoped to one protocol or provider +concern and add tests for observable behavior. ## Contract changes diff --git a/README.md b/README.md index 2408138..54e63fc 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This project separates those concerns into: - an embeddable in-memory reference runtime; - a deterministic mock provider; - provider conformance checks; -- future transport mappings and client SDKs. +- HTTP/SSE mappings, a TypeScript client SDK, and a development CLI. ## Relationship to Harness Runtime API @@ -68,7 +68,7 @@ must never execute untrusted code. ## Quick Start -Requirements: Node.js 22 or later and pnpm 10. +Development requirements: Node.js 22.12+ (22.x) or 24.x and pnpm 10. CI verifies both lines. ```bash pnpm install diff --git a/README.zh-CN.md b/README.zh-CN.md index d2378a3..71a6dc8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -28,7 +28,7 @@ Agent 应用通常需要相似的执行能力,但不同 Provider 对生命周 - 可嵌入的内存参考 Runtime; - 确定性的 Mock Provider; - Provider 一致性测试; -- 后续的传输协议映射和客户端 SDK。 +- 已提供的 HTTP/SSE 映射、TypeScript 客户端 SDK 和开发 CLI。 ## 与 Harness Runtime API 的关系 @@ -55,7 +55,7 @@ Harness 执行;Sandbox Runtime API 规范 Harness 或工具运行所需的隔 ## 快速开始 -需要 Node.js 22+ 和 pnpm 10。 +开发环境要求 Node.js 22.12+(22.x)或 24.x,pnpm 10。CI 验证这两个版本线。 ```bash pnpm install diff --git a/biome.json b/biome.json index 3fc8a14..4cab690 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json", "files": { "includes": ["**", "!**/dist", "!**/coverage", "!**/node_modules"] }, diff --git a/docs/00-v0.1-summary.md b/docs/00-v0.1-summary.md index 51da63d..7749a98 100644 --- a/docs/00-v0.1-summary.md +++ b/docs/00-v0.1-summary.md @@ -4,7 +4,7 @@ authority: process status: active title: v0.1 completion summary genre: summary -last_verified: 2026-09-02 +last_verified: 2026-10-09 --- # v0.1 Completion Summary @@ -21,7 +21,7 @@ through command, file, event, HTTP, SDK, Local Provider, and conformance behavio - TypeScript SDK, CLI, reference server, Mock Provider, and unsafe Local Provider; - local and CI verification plus public-content and clean-room controls. -## Verification snapshot +## Original implementation verification snapshot As of 2026-09-02 on the candidate branch: @@ -36,8 +36,24 @@ As of 2026-09-02 on the candidate branch: - a fresh read-only contributor session selected `AGENTS.md` → `DOCS-INDEX.md` → canonical specs, found no competing entry or private-source instruction, and routed future cloud work to a public RFC. -GitHub CI and the final merge SHA are release-time evidence and are not asserted by this pre-merge -snapshot. +This dated snapshot describes the original implementation, not the latest test count. + +## Verified release evidence + +As of 2026-10-09, the original MVP has shipped as +[v0.1.0](https://github.com/capa-cloud/sandbox-runtime-api/releases/tag/v0.1.0): + +- [Implementation PR #2](https://github.com/capa-cloud/sandbox-runtime-api/pull/2) merged as + `b6f434050e4c59cd42063c759905c43c1f810f74`; +- [main CI for that commit](https://github.com/capa-cloud/sandbox-runtime-api/actions/runs/33595024138) + passed on Node.js 22 and 24; +- [Documentation PR #3](https://github.com/capa-cloud/sandbox-runtime-api/pull/3) merged as + `0b513eae4d88ac1c7b60f46b00ad7bd5b587e652`, with + [passing main CI](https://github.com/capa-cloud/sandbox-runtime-api/actions/runs/33732582026). + +Maintenance after the tag is tracked under [Unreleased](../CHANGELOG.md). It does not change the +contents of the existing v0.1.0 tag. Use [Testing and conformance](testing.md) for current verification +commands; the main-branch CI badge in the README shows current repository health. ## Architecture decision diff --git a/docs/testing.md b/docs/testing.md index 8aa9435..51f876c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -4,7 +4,7 @@ authority: reference status: active title: Testing and provider conformance genre: how-to -last_verified: 2026-09-03 +last_verified: 2026-10-09 --- # Testing and Provider Conformance @@ -18,11 +18,15 @@ pnpm sanitize pnpm docs:check pnpm test:coverage pnpm pack:check +pnpm audit:dependencies ``` `pnpm check` runs formatting, lint, type checking, the complete test suite, documentation validation, -build, coverage thresholds, and package dry-run. CI runs the same gates on Node.js 22 and 24, followed -by the public-content scan and production dependency audit. +build, coverage thresholds, and actual tarball validation. The package gate checks exports, CLI, +documentation, and every packaged Markdown local link, including image assets. CI runs the same gates +on Node.js 22 and 24. The final check gate audits all dependencies, including development tooling, +against high-severity advisories and requires registry access. CI then runs the public-content scan +and a separate production dependency audit. ## Test dimensions @@ -43,13 +47,40 @@ by the public-content scan and production dependency audit. `runProviderConformance(provider, options)` checks behavior declared by the Provider manifest. A Provider that declares `commandExecution` must supply `options.commandRequest` with a safe command known to exist in that runtime; the portable core does not assume POSIX utilities or a particular -language runtime. Each invocation uses a unique synthetic resource identity and cleans it up. A failed +language runtime. Each invocation uses unique synthetic resource and create-intent identities and +attempts bounded cleanup after every provisioning attempt, including a lost response. A failed check means the Provider must not claim compatibility. Passing conformance does not certify isolation, production availability, or performance. Text equivalent: each run uses a unique synthetic resource, validates `describe`, `provision`, and bounded readiness, checks declared command and file behavior as independent cases, validates complete -result shapes, and calls `terminate` before reporting pass or fail. +result shapes, and calls `terminate` before reporting pass or fail. Pause and resume must return +`paused` and `ready` respectively; later data-plane probes are skipped after an ambiguous lifecycle +failure. Cleanup is successful only when termination returns `terminated`. + +```ts +const results = await runProviderConformance(provider, { + operationTimeoutMs: 5_000, + readinessTimeoutMs: 5_000, + cleanupTimeoutMs: 5_000, + pollIntervalMs: 25, + commandRequest: { argv: ['safe-provider-specific-probe'] }, +}) +``` + +Each Provider call has a bounded deadline and receives an `AbortSignal`. Readiness shares one +deadline across all observations; cleanup has its own budget even after another call times out. +Timeout and poll values must be positive integers no larger than `2147483647`. Invalid options fail +before any Provider call. These are per-operation budgets, not one total run budget: a full run is +bounded by at most eight operation budgets, one readiness budget, and one cleanup budget. + +Timeouts stop waiting, not arbitrary Provider work. Providers must honor cancellation and make +termination idempotent, including for an unknown resource. A timed-out provisioning call may allocate +after cleanup if cancellation is ignored; failed cleanup or late allocation requires Provider-side +reconciliation and must not be described as successful cleanup. + +Vitest and its coverage package are pinned to the same version and upgraded together by Dependabot. +The Biome configuration schema must also match the installed Biome version. Provider-specific real-account tests must be opt-in, use environment-provided credentials, and stay outside default CI. diff --git a/package.json b/package.json index 43c0b4c..a8655db 100644 --- a/package.json +++ b/package.json @@ -31,35 +31,43 @@ }, "files": [ "dist", + "docs", + "spec", + "DOCS-INDEX.md", "README.md", "README.zh-CN.md", "LICENSE", "CHANGELOG.md", - "spec/openapi.yaml" + "CONTRIBUTING.md", + "NON_GOALS.md", + "ORIGIN_AND_PROVENANCE.md", + "ROADMAP.md", + "SECURITY.md" ], "packageManager": "pnpm@10.33.0", "engines": { "node": ">=22" }, "scripts": { + "audit:dependencies": "pnpm audit --audit-level high", "build": "tsc -p tsconfig.build.json", - "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm docs:check && pnpm build && pnpm test:coverage && pnpm pack:check", + "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm docs:check && pnpm build && pnpm test:coverage && pnpm pack:check && pnpm audit:dependencies", "docs:check": "node scripts/check-docs.mjs", "format": "biome format --write .", "format:check": "biome format .", "lint": "biome lint .", - "pack:check": "pnpm build && pnpm pack --dry-run", + "pack:check": "pnpm build && node scripts/check-package.mjs", "sanitize": "bash scripts/scan-public.sh .", "test": "vitest run", "test:coverage": "vitest run --coverage", "typecheck": "tsc -p tsconfig.tests.json --noEmit" }, "devDependencies": { - "@biomejs/biome": "2.5.11", - "@types/node": "22.20.1", - "@vitest/coverage-v8": "4.1.11", + "@biomejs/biome": "2.5.14", + "@types/node": "22.20.4", + "@vitest/coverage-v8": "5.0.2", "typescript": "7.0.2", - "vitest": "4.1.11", - "yaml": "2.9.0" + "vitest": "5.0.2", + "yaml": "2.9.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27340a4..bca666c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,23 +9,23 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: 2.5.11 - version: 2.5.11 + specifier: 2.5.14 + version: 2.5.14 '@types/node': - specifier: 22.20.1 - version: 22.20.1 + specifier: 22.20.4 + version: 22.20.4 '@vitest/coverage-v8': - specifier: 4.1.11 - version: 4.1.11(vitest@4.1.11) + specifier: 5.0.2 + version: 5.0.2(vitest@5.0.2) typescript: specifier: 7.0.2 version: 7.0.2 vitest: - specifier: 4.1.11 - version: 4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@22.20.1)(yaml@2.9.0)) + specifier: 5.0.2 + version: 5.0.2(@types/node@22.20.4)(@vitest/coverage-v8@5.0.2)(vite@8.2.2(@types/node@22.20.4)(yaml@2.9.1)) yaml: - specifier: 2.9.0 - version: 2.9.0 + specifier: 2.9.1 + version: 2.9.1 packages: @@ -50,59 +50,59 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.5.11': - resolution: {integrity: sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA==} + '@biomejs/biome@2.5.14': + resolution: {integrity: sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.11': - resolution: {integrity: sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg==} + '@biomejs/cli-darwin-arm64@2.5.14': + resolution: {integrity: sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.11': - resolution: {integrity: sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw==} + '@biomejs/cli-darwin-x64@2.5.14': + resolution: {integrity: sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.11': - resolution: {integrity: sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw==} + '@biomejs/cli-linux-arm64-musl@2.5.14': + resolution: {integrity: sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.11': - resolution: {integrity: sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow==} + '@biomejs/cli-linux-arm64@2.5.14': + resolution: {integrity: sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.11': - resolution: {integrity: sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w==} + '@biomejs/cli-linux-x64-musl@2.5.14': + resolution: {integrity: sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.11': - resolution: {integrity: sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA==} + '@biomejs/cli-linux-x64@2.5.14': + resolution: {integrity: sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.11': - resolution: {integrity: sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw==} + '@biomejs/cli-win32-arm64@2.5.14': + resolution: {integrity: sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.11': - resolution: {integrity: sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q==} + '@biomejs/cli-win32-x64@2.5.14': + resolution: {integrity: sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -117,101 +117,101 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@oxc-project/types@0.147.0': - resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} - '@rolldown/binding-android-arm-eabi@1.2.6': - resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + '@rolldown/binding-android-arm-eabi@1.2.13': + resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.6': - resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + '@rolldown/binding-android-arm64@1.2.13': + resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.6': - resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + '@rolldown/binding-darwin-arm64@1.2.13': + resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.6': - resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + '@rolldown/binding-darwin-x64@1.2.13': + resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.6': - resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + '@rolldown/binding-freebsd-x64@1.2.13': + resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.6': - resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': + resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.6': - resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + '@rolldown/binding-linux-arm64-gnu@1.2.13': + resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.6': - resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + '@rolldown/binding-linux-arm64-musl@1.2.13': + resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.6': - resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + '@rolldown/binding-linux-ppc64-gnu@1.2.13': + resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.6': - resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + '@rolldown/binding-linux-s390x-gnu@1.2.13': + resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.6': - resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + '@rolldown/binding-linux-x64-gnu@1.2.13': + resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.6': - resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + '@rolldown/binding-linux-x64-musl@1.2.13': + resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.6': - resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + '@rolldown/binding-openharmony-arm64@1.2.13': + resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.6': - resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + '@rolldown/binding-win32-arm64-msvc@1.2.13': + resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.6': - resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + '@rolldown/binding-win32-x64-msvc@1.2.13': + resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -219,9 +219,6 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -231,8 +228,8 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/node@22.20.1': - resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/node@22.20.4': + resolution: {integrity: sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==} '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} @@ -354,20 +351,25 @@ packages: cpu: [x64] os: [win32] - '@vitest/coverage-v8@4.1.11': - resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} + '@vitest/coverage-v8@5.0.2': + resolution: {integrity: sha512-3ffHBEi8DOOBLwIGBhOBZbRfYFYWjMUuxZicONdhuFEFEG5AVsMOSrVeuROskqnqpbOmEJwxM2eTVZ9N3a1t+A==} peerDependencies: - '@vitest/browser': 4.1.11 - vitest: 4.1.11 + '@vitest/browser': 5.0.2 + vitest: 5.0.2 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/istanbul-lib-coverage@1.0.2': + resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==} + engines: {node: '>=22'} + + '@vitest/istanbul-lib-report@1.0.2': + resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==} + engines: {node: '>=22'} - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@5.0.2': + resolution: {integrity: sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -377,20 +379,8 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@vitest/spy@5.0.2': + resolution: {integrity: sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} @@ -403,9 +393,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -434,25 +421,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -530,18 +498,14 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} magicast@0.5.4: resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -549,9 +513,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -559,39 +520,25 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} engines: {node: ^10 || ^12 || >=14} - rolldown@1.2.6: - resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + rolldown@1.2.13: + resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} - hasBin: true - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.2.1: + resolution: {integrity: sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==} + engines: {node: '>=20.0.0'} tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} @@ -656,23 +603,23 @@ packages: yaml: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.2: + resolution: {integrity: sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true 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.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.2 + '@vitest/browser-preview': 5.0.2 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.2 + '@vitest/coverage-v8': 5.0.2 + '@vitest/ui': 5.0.2 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -697,13 +644,13 @@ packages: jsdom: optional: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.2: + resolution: {integrity: sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==} + engines: {node: '>=20.11'} hasBin: true - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} hasBin: true @@ -724,39 +671,39 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.5.11': + '@biomejs/biome@2.5.14': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.11 - '@biomejs/cli-darwin-x64': 2.5.11 - '@biomejs/cli-linux-arm64': 2.5.11 - '@biomejs/cli-linux-arm64-musl': 2.5.11 - '@biomejs/cli-linux-x64': 2.5.11 - '@biomejs/cli-linux-x64-musl': 2.5.11 - '@biomejs/cli-win32-arm64': 2.5.11 - '@biomejs/cli-win32-x64': 2.5.11 - - '@biomejs/cli-darwin-arm64@2.5.11': + '@biomejs/cli-darwin-arm64': 2.5.14 + '@biomejs/cli-darwin-x64': 2.5.14 + '@biomejs/cli-linux-arm64': 2.5.14 + '@biomejs/cli-linux-arm64-musl': 2.5.14 + '@biomejs/cli-linux-x64': 2.5.14 + '@biomejs/cli-linux-x64-musl': 2.5.14 + '@biomejs/cli-win32-arm64': 2.5.14 + '@biomejs/cli-win32-x64': 2.5.14 + + '@biomejs/cli-darwin-arm64@2.5.14': optional: true - '@biomejs/cli-darwin-x64@2.5.11': + '@biomejs/cli-darwin-x64@2.5.14': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.11': + '@biomejs/cli-linux-arm64-musl@2.5.14': optional: true - '@biomejs/cli-linux-arm64@2.5.11': + '@biomejs/cli-linux-arm64@2.5.14': optional: true - '@biomejs/cli-linux-x64-musl@2.5.11': + '@biomejs/cli-linux-x64-musl@2.5.14': optional: true - '@biomejs/cli-linux-x64@2.5.11': + '@biomejs/cli-linux-x64@2.5.14': optional: true - '@biomejs/cli-win32-arm64@2.5.11': + '@biomejs/cli-win32-arm64@2.5.14': optional: true - '@biomejs/cli-win32-x64@2.5.11': + '@biomejs/cli-win32-x64@2.5.14': optional: true '@jridgewell/resolve-uri@3.1.2': {} @@ -768,57 +715,55 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 - '@oxc-project/types@0.147.0': {} + '@oxc-project/types@0.153.0': {} - '@rolldown/binding-android-arm-eabi@1.2.6': + '@rolldown/binding-android-arm-eabi@1.2.13': optional: true - '@rolldown/binding-android-arm64@1.2.6': + '@rolldown/binding-android-arm64@1.2.13': optional: true - '@rolldown/binding-darwin-arm64@1.2.6': + '@rolldown/binding-darwin-arm64@1.2.13': optional: true - '@rolldown/binding-darwin-x64@1.2.6': + '@rolldown/binding-darwin-x64@1.2.13': optional: true - '@rolldown/binding-freebsd-x64@1.2.6': + '@rolldown/binding-freebsd-x64@1.2.13': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.6': + '@rolldown/binding-linux-arm64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.6': + '@rolldown/binding-linux-arm64-musl@1.2.13': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.6': + '@rolldown/binding-linux-ppc64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.6': + '@rolldown/binding-linux-s390x-gnu@1.2.13': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.6': + '@rolldown/binding-linux-x64-gnu@1.2.13': optional: true - '@rolldown/binding-linux-x64-musl@1.2.6': + '@rolldown/binding-linux-x64-musl@1.2.13': optional: true - '@rolldown/binding-openharmony-arm64@1.2.6': + '@rolldown/binding-openharmony-arm64@1.2.13': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.6': + '@rolldown/binding-win32-arm64-msvc@1.2.13': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.6': + '@rolldown/binding-win32-x64-msvc@1.2.13': optional: true '@rolldown/pluginutils@1.0.1': {} - '@standard-schema/spec@1.1.0': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -828,7 +773,7 @@ snapshots: '@types/estree@1.0.9': {} - '@types/node@22.20.1': + '@types/node@22.20.4': dependencies: undici-types: 6.21.0 @@ -892,60 +837,34 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': + '@vitest/coverage-v8@5.0.2(vitest@5.0.2)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.11 + '@vitest/istanbul-lib-coverage': 1.0.2 + '@vitest/istanbul-lib-report': 1.0.2 ast-v8-to-istanbul: 1.0.5 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 magicast: 0.5.4 obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@22.20.1)(yaml@2.9.0)) + vitest: 5.0.2(@types/node@22.20.4)(@vitest/coverage-v8@5.0.2)(vite@8.2.2(@types/node@22.20.4)(yaml@2.9.1)) - '@vitest/expect@4.1.11': + '@vitest/istanbul-lib-coverage@1.0.2': {} + + '@vitest/istanbul-lib-report@1.0.2': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 - tinyrainbow: 3.1.1 + '@vitest/istanbul-lib-coverage': 1.0.2 - '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@22.20.1)(yaml@2.9.0))': + '@vitest/mocker@5.0.2(vite@8.2.2(@types/node@22.20.4)(yaml@2.9.1))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.2 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: - vite: 8.2.2(@types/node@22.20.1)(yaml@2.9.0) - - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 + vite: 8.2.2(@types/node@22.20.4)(yaml@2.9.1) - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.11': {} - - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + '@vitest/spy@5.0.2': {} assertion-error@2.0.1: {} @@ -957,8 +876,6 @@ snapshots: chai@6.2.2: {} - convert-source-map@2.0.0: {} - detect-libc@2.1.2: {} es-module-lexer@2.3.2: {} @@ -976,23 +893,6 @@ snapshots: fsevents@2.3.3: optional: true - has-flag@4.0.0: {} - - html-escaper@2.0.2: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - js-tokens@10.0.0: {} lightningcss-android-arm64@1.33.0: @@ -1044,7 +944,7 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - magic-string@0.30.21: + magic-string@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -1052,64 +952,48 @@ snapshots: dependencies: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 - source-map-js: 1.2.1 - - make-dir@4.0.0: - dependencies: - semver: 7.8.5 + source-map-js: 1.2.2 - nanoid@3.3.18: {} + nanoid@3.3.20: {} obug@2.1.4: {} - pathe@2.0.3: {} - picocolors@1.1.1: {} picomatch@4.0.7: {} - postcss@8.5.26: + postcss@8.5.29: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.20 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 - rolldown@1.2.6: + rolldown@1.2.13: dependencies: - '@oxc-project/types': 0.147.0 + '@oxc-project/types': 0.153.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.6 - '@rolldown/binding-android-arm64': 1.2.6 - '@rolldown/binding-darwin-arm64': 1.2.6 - '@rolldown/binding-darwin-x64': 1.2.6 - '@rolldown/binding-freebsd-x64': 1.2.6 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 - '@rolldown/binding-linux-arm64-gnu': 1.2.6 - '@rolldown/binding-linux-arm64-musl': 1.2.6 - '@rolldown/binding-linux-ppc64-gnu': 1.2.6 - '@rolldown/binding-linux-s390x-gnu': 1.2.6 - '@rolldown/binding-linux-x64-gnu': 1.2.6 - '@rolldown/binding-linux-x64-musl': 1.2.6 - '@rolldown/binding-openharmony-arm64': 1.2.6 - '@rolldown/binding-win32-arm64-msvc': 1.2.6 - '@rolldown/binding-win32-x64-msvc': 1.2.6 - - semver@7.8.5: {} - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} + '@rolldown/binding-android-arm-eabi': 1.2.13 + '@rolldown/binding-android-arm64': 1.2.13 + '@rolldown/binding-darwin-arm64': 1.2.13 + '@rolldown/binding-darwin-x64': 1.2.13 + '@rolldown/binding-freebsd-x64': 1.2.13 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.13 + '@rolldown/binding-linux-arm64-gnu': 1.2.13 + '@rolldown/binding-linux-arm64-musl': 1.2.13 + '@rolldown/binding-linux-ppc64-gnu': 1.2.13 + '@rolldown/binding-linux-s390x-gnu': 1.2.13 + '@rolldown/binding-linux-x64-gnu': 1.2.13 + '@rolldown/binding-linux-x64-musl': 1.2.13 + '@rolldown/binding-openharmony-arm64': 1.2.13 + '@rolldown/binding-win32-arm64-msvc': 1.2.13 + '@rolldown/binding-win32-x64-msvc': 1.2.13 + + source-map-js@1.2.2: {} std-env@4.2.0: {} - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - tinybench@2.9.0: {} + tinybench@6.2.1: {} tinyexec@1.3.0: {} @@ -1145,49 +1029,40 @@ snapshots: undici-types@6.21.0: {} - vite@8.2.2(@types/node@22.20.1)(yaml@2.9.0): + vite@8.2.2(@types/node@22.20.4)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.26 - rolldown: 1.2.6 + postcss: 8.5.29 + rolldown: 1.2.13 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 22.20.1 + '@types/node': 22.20.4 fsevents: 2.3.3 - yaml: 2.9.0 + yaml: 2.9.1 - vitest@4.1.11(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@22.20.1)(yaml@2.9.0)): + vitest@5.0.2(@types/node@22.20.4)(@vitest/coverage-v8@5.0.2)(vite@8.2.2(@types/node@22.20.4)(yaml@2.9.1)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@22.20.1)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.2(vite@8.2.2(@types/node@22.20.4)(yaml@2.9.1)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.1.4 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.2.1 tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.2.2(@types/node@22.20.1)(yaml@2.9.0) - why-is-node-running: 2.3.0 + vite: 8.2.2(@types/node@22.20.4)(yaml@2.9.1) + why-is-node-running: 3.2.2 optionalDependencies: - '@types/node': 22.20.1 - '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) + '@types/node': 22.20.4 + '@vitest/coverage-v8': 5.0.2(vitest@5.0.2) transitivePeerDependencies: - msw - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.2: {} - yaml@2.9.0: {} + yaml@2.9.1: {} diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs new file mode 100644 index 0000000..9c58d72 --- /dev/null +++ b/scripts/check-package.mjs @@ -0,0 +1,51 @@ +import { execFileSync } from 'node:child_process' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, posix, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const temporary = await mkdtemp(join(tmpdir(), 'sandbox-runtime-pack-')) + +try { + const archive = join(temporary, 'package.tgz') + execFileSync('pnpm', ['pack', '--out', archive], { cwd: root, stdio: 'pipe' }) + const files = new Set( + execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }).trim().split('\n'), + ) + const read = (path) => execFileSync('tar', ['-xOzf', archive, path], { encoding: 'utf8' }) + const packaged = JSON.parse(read('package/package.json')) + const source = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) + const missing = [] + const required = [ + packaged.exports['.'].import, + packaged.exports['.'].types, + ...Object.values(packaged.bin), + 'README.md', + 'README.zh-CN.md', + 'DOCS-INDEX.md', + 'LICENSE', + 'spec/openapi.yaml', + ] + for (const path of required) { + if (!files.has(posix.join('package', path))) missing.push(path) + } + if (packaged.name !== source.name || packaged.version !== source.version) { + missing.push('package identity differs from source') + } + for (const path of files) { + if (!path.endsWith('.md')) continue + for (const match of read(path).matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) { + const target = match[1] + if (!target || target.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue + const resolved = posix.join(posix.dirname(path), decodeURIComponent(target.split('#')[0])) + if (!files.has(resolved)) missing.push(`${path} -> ${target}`) + } + } + if (missing.length) throw new Error(`package validation failed:\n${missing.join('\n')}`) + process.stdout.write( + `package validation passed: ${files.size} entries, exports, CLI, docs, and local links\n`, + ) +} finally { + await rm(temporary, { recursive: true, force: true }) +} diff --git a/src/conformance.ts b/src/conformance.ts index 0ecc846..ccbe778 100644 --- a/src/conformance.ts +++ b/src/conformance.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'node:crypto' -import type { ProviderObservation, SandboxProvider } from './provider.js' +import type { ProviderContext, SandboxProvider } from './provider.js' import { capabilityNames, type CommandRequest, @@ -16,6 +16,8 @@ export type ConformanceResult = Readonly<{ }> export type ProviderConformanceOptions = Readonly<{ + operationTimeoutMs?: number + cleanupTimeoutMs?: number readinessTimeoutMs?: number pollIntervalMs?: number commandRequest?: Omit @@ -73,23 +75,23 @@ const validFileEntry = (entry: FileEntry): boolean => entry.size >= 0, ) -const observeBefore = async ( - provider: SandboxProvider, - key: Readonly<{ sandboxId: string; generation: number }>, +const callBefore = async ( + operation: (context: ProviderContext) => Promise, requestId: string, deadline: number, -): Promise => { + timeoutMessage: string, +): Promise => { const remainingMs = deadline - Date.now() - if (remainingMs <= 0) throw new Error('readiness observation timed out') + if (remainingMs <= 0) throw new Error(timeoutMessage) const controller = new AbortController() let timer: ReturnType | undefined try { return await Promise.race([ - provider.observe(key, { requestId, signal: controller.signal }), + Promise.resolve().then(() => operation({ requestId, signal: controller.signal })), new Promise((_resolve, reject) => { timer = setTimeout(() => { + reject(new Error(timeoutMessage)) controller.abort() - reject(new Error('readiness observation timed out')) }, remainingMs) }), ]) @@ -106,156 +108,209 @@ export const runProviderConformance = async ( const context = { requestId: `conformance-${runId}` } const key = { sandboxId: `conformance-${runId}`, generation: 1 } const results: ConformanceResult[] = [] - const readinessTimeoutMs = Math.max(1, options.readinessTimeoutMs ?? 5_000) - const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 25) + const operationTimeoutMs = options.operationTimeoutMs ?? 5_000 + const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 5_000 + const readinessTimeoutMs = options.readinessTimeoutMs ?? 5_000 + const pollIntervalMs = options.pollIntervalMs ?? 25 + for (const [name, value] of Object.entries({ + operationTimeoutMs, + cleanupTimeoutMs, + readinessTimeoutMs, + pollIntervalMs, + })) { + if (!Number.isInteger(value) || value <= 0 || value > 2 ** 31 - 1) { + return [ + failure( + 'conformance options are valid', + new Error(`${name} must be an integer between 1 and 2147483647`), + ), + ] + } + } + const call = ( + name: string, + operation: (ctx: ProviderContext) => Promise, + timeoutMs = operationTimeoutMs, + ): Promise => + callBefore(operation, context.requestId, Date.now() + timeoutMs, `${name} timed out`) let manifest: ProviderManifest try { - manifest = await provider.describe(context) + manifest = await call('describe', (ctx) => provider.describe(ctx)) } catch (error) { return [failure('provider manifest is readable', error)] } results.push({ name: 'provider manifest is complete', passed: validManifest(manifest) }) if (!validManifest(manifest)) return results - let provisioned: ProviderObservation + let cleanupName = 'terminate reaches terminated' try { - provisioned = await provider.provision( - { ...key, spec: { clientRequestId: 'conformance-create' } }, - context, - ) - results.push({ - name: 'provision returns an observable non-terminal state', - passed: !['failed', 'terminated'].includes(provisioned.state), - ...(['failed', 'terminated'].includes(provisioned.state) - ? { detail: `observed ${provisioned.state}` } - : {}), - }) - } catch (error) { - results.push(failure('provision returns an observation', error)) try { - await provider.terminate(key, context) - results.push({ name: 'ambiguous provision failure is cleaned up', passed: true }) - } catch (cleanupError) { - results.push(failure('ambiguous provision failure is cleaned up', cleanupError)) - } - return results - } - - let ready = false - try { - const deadline = Date.now() + readinessTimeoutMs - let observed = await observeBefore(provider, key, context.requestId, deadline) - while (!['ready', 'failed', 'terminated'].includes(observed.state) && Date.now() < deadline) { - await new Promise((resolve) => - setTimeout(resolve, Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))), + const provisioned = await call('provision', (ctx) => + provider.provision( + { ...key, spec: { clientRequestId: `conformance-create-${runId}` } }, + ctx, + ), + ) + const valid = Boolean( + provisioned && ['requested', 'starting', 'ready'].includes(provisioned.state), ) - observed = await observeBefore(provider, key, context.requestId, deadline) - } - ready = observed.state === 'ready' - results.push({ - name: 'provision reaches ready within the bounded deadline', - passed: ready, - ...(!ready ? { detail: `last observed state ${observed.state}` } : {}), - }) - } catch (error) { - results.push(failure('provision reaches ready within the bounded deadline', error)) - } - - if (ready && manifest.capabilities.pauseResume) { - if (!provider.pause || !provider.resume) { results.push({ - name: 'pauseResume capability has SPI methods', - passed: false, - detail: 'manifest declares pauseResume without pause and resume methods', + name: 'provision returns an observable non-terminal state', + passed: valid, + ...(!valid ? { detail: `observed ${provisioned?.state ?? 'missing state'}` } : {}), }) - } else { - try { - const paused = await provider.pause(key, context) - results.push({ name: 'pause reaches paused', passed: paused.state === 'paused' }) - const resumed = await provider.resume(key, context) - results.push({ name: 'resume leaves paused', passed: resumed.state !== 'paused' }) - } catch (error) { - results.push(failure('pause and resume complete', error)) - } + if (!valid) return results + } catch (error) { + results.push(failure('provision returns an observation', error)) + cleanupName = 'ambiguous provision failure is cleaned up' + return results } - } - if (ready && manifest.capabilities.commandExecution) { - if (!provider.execute) { - results.push({ name: 'commandExecution capability has an execute method', passed: false }) - } else if (!options.commandRequest) { + let ready = false + try { + const deadline = Date.now() + readinessTimeoutMs + const observe = () => + callBefore( + (ctx) => provider.observe(key, ctx), + context.requestId, + deadline, + 'readiness observation timed out', + ) + let observed = await observe() + while (!['ready', 'failed', 'terminated'].includes(observed.state) && Date.now() < deadline) { + await new Promise((resolve) => + setTimeout(resolve, Math.min(pollIntervalMs, Math.max(0, deadline - Date.now()))), + ) + observed = await observe() + } + ready = observed.state === 'ready' results.push({ - name: 'command execution probe is configured', - passed: false, - detail: 'commandRequest is required because no command is portable across providers', + name: 'provision reaches ready within the bounded deadline', + passed: ready, + ...(!ready ? { detail: `last observed state ${observed.state}` } : {}), }) - } else { - try { - const command = await provider.execute( - key, - { ...options.commandRequest, expectedGeneration: key.generation }, - context, - ) + } catch (error) { + results.push(failure('provision reaches ready within the bounded deadline', error)) + } + + if (ready && manifest.capabilities.pauseResume) { + if (!provider.pause || !provider.resume) { results.push({ - name: 'command execution returns a complete successful result', - passed: - validCommandResult(command) && - command.exitCode === 0 && - !command.timedOut && - !command.cancelled, + name: 'pauseResume capability has SPI methods', + passed: false, + detail: 'manifest declares pauseResume without pause and resume methods', }) - } catch (error) { - results.push(failure('command execution completes', error)) + } else { + try { + ready = false + const pause = provider.pause + const resume = provider.resume + const paused = await call('pause', (ctx) => pause.call(provider, key, ctx)) + results.push({ name: 'pause reaches paused', passed: paused.state === 'paused' }) + if (paused.state === 'paused') { + const resumed = await call('resume', (ctx) => resume.call(provider, key, ctx)) + ready = resumed.state === 'ready' + results.push({ name: 'resume leaves paused', passed: ready }) + } + } catch (error) { + results.push(failure('pause and resume complete', error)) + } } } - } - if (ready && manifest.capabilities.fileAccess) { - if (!provider.writeFile || !provider.readFile || !provider.listFiles) { - results.push({ - name: 'fileAccess capability has read, write, and list methods', - passed: false, - }) - } else { - try { - const contentBase64 = Buffer.from('conformance').toString('base64') - const expectedSize = Buffer.byteLength('conformance') - const written = await provider.writeFile( - key, - { expectedGeneration: key.generation, path: 'probe.txt', contentBase64 }, - context, - ) - const read = await provider.readFile(key, 'probe.txt', context) - const listed = await provider.listFiles(key, '.', context) + if (ready && manifest.capabilities.commandExecution) { + if (!provider.execute) { + results.push({ name: 'commandExecution capability has an execute method', passed: false }) + } else if (!options.commandRequest) { results.push({ - name: 'file round-trip returns complete metadata, bytes, and listing', - passed: - validFileResult(written, 'probe.txt', expectedSize) && - written.contentBase64 === contentBase64 && - validFileResult(read, 'probe.txt', expectedSize) && - read.contentBase64 === contentBase64 && - listed.every(validFileEntry) && - listed.some( - (entry) => - entry.path === 'probe.txt' && entry.kind === 'file' && entry.size === expectedSize, + name: 'command execution probe is configured', + passed: false, + detail: 'commandRequest is required because no command is portable across providers', + }) + } else { + try { + const execute = provider.execute + const commandRequest = options.commandRequest + const command = await call('execute', (ctx) => + execute.call( + provider, + key, + { ...commandRequest, expectedGeneration: key.generation }, + ctx, ), + ) + results.push({ + name: 'command execution returns a complete successful result', + passed: + validCommandResult(command) && + command.exitCode === 0 && + !command.timedOut && + !command.cancelled, + }) + } catch (error) { + results.push(failure('command execution completes', error)) + } + } + } + + if (ready && manifest.capabilities.fileAccess) { + if (!provider.writeFile || !provider.readFile || !provider.listFiles) { + results.push({ + name: 'fileAccess capability has read, write, and list methods', + passed: false, }) - } catch (error) { - results.push(failure('file round-trip completes', error)) + } else { + try { + const contentBase64 = Buffer.from('conformance').toString('base64') + const expectedSize = Buffer.byteLength('conformance') + const writeFile = provider.writeFile + const readFile = provider.readFile + const listFiles = provider.listFiles + const written = await call('writeFile', (ctx) => + writeFile.call( + provider, + key, + { expectedGeneration: key.generation, path: 'probe.txt', contentBase64 }, + ctx, + ), + ) + const read = await call('readFile', (ctx) => + readFile.call(provider, key, 'probe.txt', ctx), + ) + const listed = await call('listFiles', (ctx) => listFiles.call(provider, key, '.', ctx)) + results.push({ + name: 'file round-trip returns complete metadata, bytes, and listing', + passed: + validFileResult(written, 'probe.txt', expectedSize) && + written.contentBase64 === contentBase64 && + validFileResult(read, 'probe.txt', expectedSize) && + read.contentBase64 === contentBase64 && + listed.every(validFileEntry) && + listed.some( + (entry) => + entry.path === 'probe.txt' && + entry.kind === 'file' && + entry.size === expectedSize, + ), + }) + } catch (error) { + results.push(failure('file round-trip completes', error)) + } } } - } - try { - const terminated = await provider.terminate(key, context) - results.push({ - name: 'terminate reaches terminated', - passed: terminated.state === 'terminated', - }) - } catch (error) { - results.push(failure('terminate completes', error)) + return results + } finally { + try { + const terminated = await call( + 'terminate', + (ctx) => provider.terminate(key, ctx), + cleanupTimeoutMs, + ) + results.push({ name: cleanupName, passed: terminated?.state === 'terminated' }) + } catch (error) { + results.push(failure(cleanupName, error)) + } } - - return results } diff --git a/src/runtime.ts b/src/runtime.ts index f760f9c..f55c0f5 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -227,7 +227,7 @@ export class InMemorySandboxRuntime { const current = this.get(id) this.#assertGeneration(current, expectedGeneration) if (current.state === 'terminated') return current - if (!['requested', 'starting', 'ready', 'paused', 'failed'].includes(current.state)) { + if (!transitions[current.state].includes('terminating')) { throw new RuntimeError( 'invalid_state', `sandbox ${id} cannot terminate from ${current.state}`, diff --git a/tests/conformance-deadlines.test.ts b/tests/conformance-deadlines.test.ts new file mode 100644 index 0000000..7604068 --- /dev/null +++ b/tests/conformance-deadlines.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, it, vi } from 'vitest' +import { MockSandboxProvider, runProviderConformance } from '../src/index.js' +import type { ProviderContext, SandboxProvider } from '../src/provider.js' + +const fixture = (): SandboxProvider => { + const base = new MockSandboxProvider() + return { + describe: (ctx) => base.describe(ctx), + provision: (request, ctx) => base.provision(request, ctx), + observe: (key, ctx) => base.observe(key, ctx), + terminate: (key, ctx) => base.terminate(key, ctx), + pause: (key, ctx) => base.pause(key, ctx), + resume: (key, ctx) => base.resume(key, ctx), + execute: (key, request, ctx) => base.execute(key, request, ctx), + writeFile: (key, request, ctx) => base.writeFile(key, request, ctx), + readFile: (key, path, ctx) => base.readFile(key, path, ctx), + listFiles: (key, path, ctx) => base.listFiles(key, path, ctx), + } +} + +describe('conformance deadlines and cleanup', () => { + it.each([ + 'describe', + 'provision', + 'observe', + 'pause', + 'resume', + 'execute', + 'writeFile', + 'readFile', + 'listFiles', + 'terminate', + ] as const)('bounds a hung %s call and attempts cleanup when needed', async (operation) => { + const provider = fixture() + let terminations = 0 + let signal: AbortSignal | undefined + const terminate = provider.terminate + provider.terminate = (key, ctx) => { + expect(ctx.signal?.aborted).toBe(false) + terminations += 1 + return terminate(key, ctx) + } + Object.assign(provider, { + [operation]: (...args: unknown[]) => { + if (operation === 'terminate') terminations += 1 + signal = (args.at(-1) as ProviderContext).signal + return new Promise(() => {}) + }, + }) + vi.useFakeTimers() + try { + const running = runProviderConformance(provider, { + operationTimeoutMs: 100, + cleanupTimeoutMs: 100, + readinessTimeoutMs: 100, + commandRequest: { argv: ['probe'] }, + }) + await vi.runAllTimersAsync() + const results = await running + expect(results.some((result) => !result.passed && result.detail?.includes('timed out'))).toBe( + true, + ) + expect(signal?.aborted).toBe(true) + expect(terminations).toBe(operation === 'describe' ? 0 : 1) + } finally { + vi.clearAllTimers() + vi.useRealTimers() + } + }) + + it('does not report cleanup success for a non-terminated observation', async () => { + const provider = fixture() + provider.provision = () => Promise.reject(new Error('response lost')) + provider.terminate = () => Promise.resolve({ state: 'terminating' }) + const results = await runProviderConformance(provider) + expect(results).toContainEqual( + expect.objectContaining({ name: 'ambiguous provision failure is cleaned up', passed: false }), + ) + }) + + it('does not accept failed resume as successful recovery', async () => { + const provider = fixture() + provider.resume = () => Promise.resolve({ state: 'failed' }) + provider.execute = () => { + throw new Error('data-plane probe ran after failed recovery') + } + const results = await runProviderConformance(provider, { commandRequest: { argv: ['probe'] } }) + expect(results).toContainEqual( + expect.objectContaining({ name: 'resume leaves paused', passed: false }), + ) + expect(results.some((result) => result.name.includes('command execution'))).toBe(false) + }) + + it('retains timeout diagnosis when the Provider rejects on cancellation', async () => { + const provider = fixture() + provider.describe = (ctx) => + new Promise((_resolve, reject) => { + ctx.signal?.addEventListener('abort', () => reject(new Error('cancelled')), { once: true }) + }) + await expect(runProviderConformance(provider, { operationTimeoutMs: 10 })).resolves.toEqual([ + { name: 'provider manifest is readable', passed: false, detail: 'describe timed out' }, + ]) + }) + + it('handles a late rejection after returning a timeout result', async () => { + const provider = fixture() + provider.describe = () => + new Promise((_resolve, reject) => { + setTimeout(() => reject(new Error('late failure')), 30) + }) + const results = await runProviderConformance(provider, { operationTimeoutMs: 10 }) + await new Promise((resolve) => setTimeout(resolve, 40)) + expect(results).toEqual([ + { name: 'provider manifest is readable', passed: false, detail: 'describe timed out' }, + ]) + }) + + it.each(['failed', 'paused', 'not-a-state', undefined])( + 'rejects invalid provision state %s and still cleans up', + async (state) => { + const provider = fixture() + let cleaned = false + provider.provision = () => Promise.resolve({ state } as never) + provider.terminate = async () => { + cleaned = true + return { state: 'terminated' } + } + const results = await runProviderConformance(provider) + expect(results).toContainEqual( + expect.objectContaining({ + name: 'provision returns an observable non-terminal state', + passed: false, + }), + ) + expect(cleaned).toBe(true) + expect(results).toContainEqual({ name: 'terminate reaches terminated', passed: true }) + }, + ) + + it.each([0, -1, Number.NaN, Number.POSITIVE_INFINITY, 2 ** 31])( + 'rejects invalid timeout %s before allocating', + async (timeout) => { + const provider = fixture() + provider.describe = () => { + throw new Error('provider must not be called') + } + for (const option of [ + 'operationTimeoutMs', + 'cleanupTimeoutMs', + 'readinessTimeoutMs', + 'pollIntervalMs', + ]) { + const results = await runProviderConformance(provider, { [option]: timeout }) + expect(results).toContainEqual( + expect.objectContaining({ name: 'conformance options are valid', passed: false }), + ) + } + }, + ) +}) diff --git a/tests/runtime.test.ts b/tests/runtime.test.ts index d04f4c6..52b8b76 100644 --- a/tests/runtime.test.ts +++ b/tests/runtime.test.ts @@ -187,6 +187,71 @@ describe('InMemorySandboxRuntime lifecycle', () => { expect(terminations).toBe(1) }) + it.each(['pausing', 'resuming'] as const)( + 'terminates from %s after a lost response while fencing stale generations', + async (state) => { + const base = new MockSandboxProvider() + let terminations = 0 + const provider: SandboxProvider = { + describe: (ctx) => base.describe(ctx), + provision: (request, ctx) => base.provision(request, ctx), + observe: (key, ctx) => base.observe(key, ctx), + terminate: (key, ctx) => { + terminations += 1 + return base.terminate(key, ctx) + }, + pause: async (key, ctx) => { + const observation = await base.pause(key, ctx) + if (state === 'pausing') throw new Error('pause response lost') + return observation + }, + resume: async (key, ctx) => { + await base.resume(key, ctx) + throw new Error('resume response lost') + }, + } + const runtime = new InMemorySandboxRuntime(provider, { idFactory: () => 'sandbox-1' }) + await runtime.create({ clientRequestId: 'transition-cleanup' }, context) + if (state === 'resuming') await runtime.pause('sandbox-1', 1, context) + const transition = state === 'pausing' ? 'pause' : 'resume' + await expect(runtime[transition]('sandbox-1', 1, context)).rejects.toMatchObject({ + code: 'provider_unavailable', + }) + expect(runtime.get('sandbox-1').state).toBe(state) + await expect(runtime.terminate('sandbox-1', 2, context)).rejects.toMatchObject({ + code: 'generation_conflict', + }) + expect(terminations).toBe(0) + const resources = await Promise.all([ + runtime.terminate('sandbox-1', 1, context), + runtime.terminate('sandbox-1', 1, context), + ]) + expect(resources.every((resource) => resource.state === 'terminated')).toBe(true) + expect(terminations).toBe(1) + }, + ) + + it('retries termination while the provider still reports terminating', async () => { + const base = new MockSandboxProvider() + let terminations = 0 + const provider: SandboxProvider = { + describe: (ctx) => base.describe(ctx), + provision: (request, ctx) => base.provision(request, ctx), + observe: (key, ctx) => base.observe(key, ctx), + terminate: (key, ctx) => { + terminations += 1 + return terminations === 1 + ? Promise.resolve({ state: 'terminating' }) + : base.terminate(key, ctx) + }, + } + const runtime = new InMemorySandboxRuntime(provider, { idFactory: () => 'sandbox-1' }) + await runtime.create({ clientRequestId: 'termination-retry' }, context) + expect((await runtime.terminate('sandbox-1', 1, context)).state).toBe('terminating') + expect((await runtime.terminate('sandbox-1', 1, context)).state).toBe('terminated') + expect(terminations).toBe(2) + }) + it('reconciles ambiguous pause and resume failures from transitional state', async () => { const base = new MockSandboxProvider() const provider: SandboxProvider = { @@ -468,10 +533,12 @@ describe('provider conformance', () => { it('isolates concurrent conformance runs with unique resource identities', async () => { const base = new MockSandboxProvider() const sandboxIds: string[] = [] + const createIntents: string[] = [] const provider: SandboxProvider = { describe: (ctx) => base.describe(ctx), provision: async (request, ctx) => { sandboxIds.push(request.sandboxId) + createIntents.push(request.spec.clientRequestId) await Promise.resolve() return base.provision(request, ctx) }, @@ -489,6 +556,7 @@ describe('provider conformance', () => { runProviderConformance(provider, { commandRequest: { argv: ['second'] } }), ]) expect(new Set(sandboxIds).size).toBe(2) + expect(new Set(createIntents).size).toBe(2) expect(runs.flat().filter((result) => !result.passed)).toEqual([]) })