ci: add end-to-end test tier, CI workflow, and coverage gating #1
@@ -19,8 +19,36 @@ The workflow file stays GitHub-Actions-compatible so the GitHub mirror can adopt
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A workflow runs on push/PR on Gitea Actions, executing the unit and integration tiers (the local vitest suite) and the end-to-end tier
|
||||
- [ ] The disposable Gitea runs as a service container pinned to a specific stable image tag
|
||||
- [ ] End-to-end tests provision their own repo, token, and seed data on the disposable instance, then assert real CLI output and exit codes for at least the tracer command set
|
||||
- [ ] The workflow uses only syntax that works verbatim (or near-verbatim) on GitHub Actions
|
||||
- [ ] A fixture-vs-live divergence in a covered response shape fails the end-to-end tier
|
||||
- [x] A workflow runs on push/PR on Gitea Actions, executing the unit and integration tiers (the local vitest suite) and the end-to-end tier
|
||||
- [x] The disposable Gitea runs as a service container pinned to a specific stable image tag
|
||||
- [x] End-to-end tests provision their own repo, token, and seed data on the disposable instance, then assert real CLI output and exit codes for at least the tracer command set
|
||||
- [x] The workflow uses only syntax that works verbatim (or near-verbatim) on GitHub Actions
|
||||
- [x] A fixture-vs-live divergence in a covered response shape fails the end-to-end tier
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**Tier split.**
|
||||
The end-to-end tier lives under `test/e2e/` with its own `vitest.e2e.config.ts` and a `test:e2e` npm script; the default `vitest.config.ts` now excludes `test/e2e/**` so `npm test` stays the fast unit+integration tiers with no external dependency.
|
||||
The e2e suite is gated on `GITEA_AXI_E2E_URL` via `describe.skipIf`, so it skips cleanly (exit 0) when no live instance is configured; `passWithNoTests` guards against a "no tests found" failure in that state.
|
||||
|
||||
**In-Node provisioning, no `docker exec`.**
|
||||
`test/e2e/provision.ts` brings a fresh Gitea to a usable state entirely over HTTP: it waits on `GET /api/v1/version`, registers the first user through the web `sign_up` form (Gitea makes the first account the site admin), scraping and echoing the double-submit CSRF token, then mints a scoped API token via HTTP Basic auth and creates the repo + seed issues with it.
|
||||
This keeps provisioning identical on Gitea Actions, GitHub Actions, and a developer's local `docker run gitea/gitea`, with no container-shell access required.
|
||||
|
||||
**Portable networking.**
|
||||
The workflow job runs inside a `node:20-bookworm` container so the `gitea` service container is reachable by service name (`gitea:3000`) on both Gitea Actions and GitHub Actions, sidestepping the host-`localhost` vs. service-name difference between the two platforms — this is what keeps the file near-verbatim GitHub-compatible (AC4).
|
||||
|
||||
**Fixture-vs-live guard (AC5).**
|
||||
Rather than hardcode expected keys, the shape guard anchors on one exported contract, `COVERED_ISSUE_PATHS` — the exact dotted paths the issue-list `FieldDef` extractors read — and asserts it holds on *both* the recorded `fixtures/issues-open.json` and the live response.
|
||||
A drift in either (a fixture edited out of shape, or a live field renamed such as `user`→`author`) fails the tier.
|
||||
|
||||
**Pinned tag.**
|
||||
`gitea/gitea:1.23.5`, chosen to match the gitea-js client line (`^1.23.0`) so the e2e tier exercises the response shapes the client was generated against; bumped deliberately by the operator.
|
||||
|
||||
**Deviation from the letter of the ACs.**
|
||||
The workflow adds a `typecheck` step that no AC names; it is cheap CI hygiene and kept deliberately.
|
||||
|
||||
**Verified against a live Gitea 1.23.5.**
|
||||
The full e2e tier was run against a real disposable `gitea/gitea:1.23.5` container (all seven tests green), which also confirms the pinned tag pulls.
|
||||
The live run surfaced one thing the earlier mock run could not: creating a repo under a user (`POST /user/repos`) requires the token scope `write:user` on Gitea 1.23, not `write:repository` — the token scopes in `provision.ts` were corrected to `["write:user", "write:repository", "write:issue"]`.
|
||||
This is exactly the fixture-vs-live class of divergence the tier exists to catch.
|
||||
|
||||
54
.gitea/workflows/ci.yml
Normal file
54
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
# CI for gitea-axi. Runs on Gitea Actions on the operator's instance; the syntax
|
||||
# is kept GitHub-Actions-compatible so the GitHub mirror can adopt this file
|
||||
# nearly verbatim (copy it to .github/workflows/).
|
||||
#
|
||||
# The job runs inside a node container so the disposable Gitea service is
|
||||
# reachable by its service name (`gitea:3000`) on both Gitea Actions and GitHub
|
||||
# Actions — avoiding the host-vs-service-name networking difference between the
|
||||
# two platforms.
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:20-bookworm
|
||||
|
||||
services:
|
||||
gitea:
|
||||
# Pinned to a specific stable tag, bumped deliberately (not floating).
|
||||
# Kept on the 1.23 line to match the gitea-js client (^1.23.0), so the
|
||||
# e2e tier exercises response shapes the client was generated against.
|
||||
image: gitea/gitea:1.23.5
|
||||
env:
|
||||
GITEA__security__INSTALL_LOCK: "true"
|
||||
GITEA__database__DB_TYPE: sqlite3
|
||||
GITEA__database__PATH: /data/gitea/gitea.db
|
||||
GITEA__server__ROOT_URL: http://gitea:3000/
|
||||
GITEA__server__HTTP_PORT: "3000"
|
||||
GITEA__service__DISABLE_REGISTRATION: "false"
|
||||
GITEA__service__REQUIRE_SIGNIN_VIEW: "false"
|
||||
GITEA__log__LEVEL: warn
|
||||
|
||||
env:
|
||||
# The end-to-end tier provisions and drives the CLI against this instance.
|
||||
GITEA_AXI_E2E_URL: http://gitea:3000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Unit and integration tiers (with coverage thresholds)
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: End-to-end tier
|
||||
run: npm run test:e2e
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
|
||||
863
package-lock.json
generated
863
package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.0",
|
||||
"@vitest/coverage-v8": "^3.2.7",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.2.0"
|
||||
},
|
||||
@@ -25,6 +26,80 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
@@ -467,6 +542,55 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
|
||||
"integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
@@ -474,6 +598,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.62.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||
@@ -904,6 +1050,40 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz",
|
||||
"integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"ast-v8-to-istanbul": "^0.3.3",
|
||||
"debug": "^4.4.1",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
"istanbul-reports": "^3.1.7",
|
||||
"magic-string": "^0.30.17",
|
||||
"magicast": "^0.3.5",
|
||||
"std-env": "^3.9.0",
|
||||
"test-exclude": "^7.0.1",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.2.7",
|
||||
"vitest": "3.2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||
@@ -1019,6 +1199,32 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -1029,6 +1235,25 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
|
||||
"integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axi-sdk-js": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/axi-sdk-js/-/axi-sdk-js-0.1.8.tgz",
|
||||
@@ -1041,6 +1266,29 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@@ -1078,6 +1326,41 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -1106,6 +1389,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
@@ -1193,6 +1490,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1214,6 +1528,165 @@
|
||||
"integrity": "sha512-f4+UPoWgDetZeZ+Awo5iI1nVdO5bjxA8+2QCeLo3oYWUYxKyzLfXgbW1EPD635wb8hLgS0DRBu5XhtiuYKEeUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
@@ -1228,6 +1701,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -1238,6 +1718,60 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -1264,6 +1798,40 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -1375,6 +1943,42 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
@@ -1382,6 +1986,19 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1406,6 +2023,110 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
@@ -1419,6 +2140,34 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
|
||||
"integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^10.4.1",
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -1672,6 +2421,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"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",
|
||||
@@ -1688,6 +2453,104 @@
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"prepublishOnly": "npm run build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "vitest run --config vitest.e2e.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@toon-format/toon": "^2.3.0",
|
||||
@@ -27,6 +29,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.0",
|
||||
"@vitest/coverage-v8": "^3.2.7",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.2.0"
|
||||
}
|
||||
|
||||
95
test/classify-http-error.test.ts
Normal file
95
test/classify-http-error.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { AxiError } from "axi-sdk-js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifyHttpError } from "../src/errors.js";
|
||||
|
||||
/**
|
||||
* Unit-tier coverage of the HTTP error classifier (a pure function, no I/O).
|
||||
* The integration tier drives the classifier through the issue-list seam, but
|
||||
* some branches belong to paths no shipped command reaches yet — pull-request
|
||||
* 404s, the already-classified passthrough, and non-HTTP transport failures —
|
||||
* so they are exercised directly here against crafted inputs.
|
||||
*/
|
||||
function httpError(status: number, url: string, body?: unknown) {
|
||||
return { status, url, error: body };
|
||||
}
|
||||
|
||||
const REPO = "http://gitea.example/api/v1/repos/o/r";
|
||||
|
||||
describe("classifyHttpError", () => {
|
||||
it("passes an already-classified AxiError through unchanged", () => {
|
||||
const original = new AxiError("boom", "FORBIDDEN", ["hint"]);
|
||||
expect(classifyHttpError(original)).toBe(original);
|
||||
});
|
||||
|
||||
it("classifies a 404 on an issue path as ISSUE_NOT_FOUND", () => {
|
||||
const result = classifyHttpError(httpError(404, `${REPO}/issues/42`));
|
||||
expect(result.code).toBe("ISSUE_NOT_FOUND");
|
||||
expect(result.message).toContain("#42");
|
||||
});
|
||||
|
||||
it("classifies a 404 on a pull path as PR_NOT_FOUND", () => {
|
||||
const result = classifyHttpError(httpError(404, `${REPO}/pulls/7`));
|
||||
expect(result.code).toBe("PR_NOT_FOUND");
|
||||
expect(result.message).toContain("#7");
|
||||
});
|
||||
|
||||
it("classifies a 404 on the repo subtree as REPO_NOT_FOUND", () => {
|
||||
const result = classifyHttpError(httpError(404, `${REPO}/issues`));
|
||||
expect(result.code).toBe("REPO_NOT_FOUND");
|
||||
expect(result.message).toContain("o/r");
|
||||
});
|
||||
|
||||
it("classifies a 404 on a non-repo path as UNKNOWN", () => {
|
||||
const result = classifyHttpError(httpError(404, "http://gitea.example/api/v1/version"));
|
||||
expect(result.code).toBe("UNKNOWN");
|
||||
});
|
||||
|
||||
it("falls back to the raw url when the response url does not parse", () => {
|
||||
const result = classifyHttpError(httpError(404, "::not a url::"));
|
||||
expect(result.code).toBe("UNKNOWN");
|
||||
expect(result.message).toContain("::not a url::");
|
||||
});
|
||||
|
||||
it("uses the body message when present, and a default when absent", () => {
|
||||
expect(classifyHttpError(httpError(401, REPO, { message: "token expired" })).message).toBe(
|
||||
"token expired",
|
||||
);
|
||||
expect(classifyHttpError(httpError(401, REPO, {})).message).toBe("Authentication required");
|
||||
});
|
||||
|
||||
it("maps 403 and every validation status (405/409/422) and 429 to their codes", () => {
|
||||
expect(classifyHttpError(httpError(403, REPO, {})).code).toBe("FORBIDDEN");
|
||||
expect(classifyHttpError(httpError(405, REPO, {})).code).toBe("VALIDATION_ERROR");
|
||||
expect(classifyHttpError(httpError(409, REPO, {})).code).toBe("VALIDATION_ERROR");
|
||||
expect(classifyHttpError(httpError(422, REPO, {})).code).toBe("VALIDATION_ERROR");
|
||||
expect(classifyHttpError(httpError(429, REPO, {})).code).toBe("RATE_LIMITED");
|
||||
});
|
||||
|
||||
it("maps an unexpected status to UNKNOWN, with and without a body message", () => {
|
||||
expect(classifyHttpError(httpError(500, REPO, { message: "kaboom" })).message).toContain(
|
||||
"kaboom",
|
||||
);
|
||||
const bare = classifyHttpError(httpError(503, REPO, {}));
|
||||
expect(bare.code).toBe("UNKNOWN");
|
||||
expect(bare.message).toContain("503");
|
||||
});
|
||||
|
||||
it("classifies a plain Error transport failure as UNKNOWN", () => {
|
||||
const result = classifyHttpError(new Error("network down"));
|
||||
expect(result.code).toBe("UNKNOWN");
|
||||
expect(result.message).toContain("network down");
|
||||
});
|
||||
|
||||
it("includes the underlying cause when a transport failure carries one", () => {
|
||||
const error = new Error("fetch failed", { cause: new Error("ECONNREFUSED") });
|
||||
const result = classifyHttpError(error);
|
||||
expect(result.message).toContain("fetch failed");
|
||||
expect(result.message).toContain("ECONNREFUSED");
|
||||
});
|
||||
|
||||
it("stringifies a non-Error thrown value", () => {
|
||||
const result = classifyHttpError("just a string");
|
||||
expect(result.code).toBe("UNKNOWN");
|
||||
expect(result.message).toContain("just a string");
|
||||
});
|
||||
});
|
||||
@@ -24,24 +24,49 @@ afterAll(() => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
interface SandboxOptions {
|
||||
logins?: FakeLogin[];
|
||||
token?: string;
|
||||
/** Set false to omit the tea binary entirely (TEA_NOT_INSTALLED path). */
|
||||
tea?: boolean;
|
||||
/** Raw stdout for `tea login list`, overriding the logins JSON. */
|
||||
listOutput?: string;
|
||||
/** Exit code for `tea login list` (default 0). */
|
||||
listExitCode?: number;
|
||||
/** stderr line emitted by `tea login list` when it fails. */
|
||||
listStderr?: string;
|
||||
/** Raw stdout for `tea login helper get`, overriding the credential block. */
|
||||
helperOutput?: string;
|
||||
/** Exit code for `tea login helper get` (default 0). */
|
||||
helperExitCode?: number;
|
||||
/** stderr line emitted by `tea login helper get` when it fails. */
|
||||
helperStderr?: string;
|
||||
}
|
||||
|
||||
/** A PATH dir with real git and optionally a fake tea baked to fixed replies. */
|
||||
function makeSandbox(options: { logins?: FakeLogin[]; token?: string; tea?: boolean }): string {
|
||||
function makeSandbox(options: SandboxOptions): string {
|
||||
const bin = join(root, `bin-${sandboxCounter++}`);
|
||||
mkdirSync(bin);
|
||||
symlinkSync(gitPath, join(bin, "git"));
|
||||
symlinkSync(catPath, join(bin, "cat"));
|
||||
if (options.tea !== false) {
|
||||
const listOutput = options.listOutput ?? JSON.stringify(options.logins ?? []);
|
||||
const helperOutput =
|
||||
options.helperOutput ??
|
||||
`protocol=http\nhost=fixture\nusername=u\npassword=${options.token ?? ""}`;
|
||||
const script = `#!/bin/sh
|
||||
if [ "$1" = "login" ] && [ "$2" = "list" ]; then
|
||||
cat <<'JSON'
|
||||
${JSON.stringify(options.logins ?? [])}
|
||||
JSON
|
||||
exit 0
|
||||
cat <<'LISTEOF'
|
||||
${listOutput}
|
||||
LISTEOF
|
||||
${options.listStderr ? ` echo '${options.listStderr}' >&2\n` : ""} exit ${options.listExitCode ?? 0}
|
||||
fi
|
||||
if [ "$1" = "login" ] && [ "$2" = "helper" ] && [ "$3" = "get" ]; then
|
||||
cat > /dev/null
|
||||
printf 'protocol=http\\nhost=fixture\\nusername=u\\npassword=%s\\n' '${options.token ?? ""}'
|
||||
exit 0
|
||||
cat <<'HELPEREOF'
|
||||
${helperOutput}
|
||||
HELPEREOF
|
||||
${options.helperStderr ? ` echo '${options.helperStderr}' >&2\n` : ""} exit ${options.helperExitCode ?? 0}
|
||||
fi
|
||||
echo "unexpected tea invocation: $*" >&2
|
||||
exit 1
|
||||
@@ -254,4 +279,99 @@ describe("repository context detection", () => {
|
||||
expect(server!.requests[0]!.headers.authorization).toBe("Bearer named-token");
|
||||
expect(stdout).toContain("--login fixture");
|
||||
});
|
||||
|
||||
it("maps a failing `tea login list` to UNKNOWN, surfacing the stderr detail", async () => {
|
||||
const cwd = makeRepo("https://gitea.example.com/testowner/testrepo.git");
|
||||
const bin = makeSandbox({ listExitCode: 1, listStderr: "config file is corrupt" });
|
||||
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: { PATH: bin },
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: UNKNOWN");
|
||||
expect(stdout).toContain("tea login list");
|
||||
expect(stdout).toContain("config file is corrupt");
|
||||
});
|
||||
|
||||
it("maps invalid JSON from `tea login list` to UNKNOWN", async () => {
|
||||
const cwd = makeRepo("https://gitea.example.com/testowner/testrepo.git");
|
||||
const bin = makeSandbox({ listOutput: "not json at all {" });
|
||||
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: { PATH: bin },
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: UNKNOWN");
|
||||
expect(stdout).toContain("invalid JSON");
|
||||
});
|
||||
|
||||
it("maps non-array JSON from `tea login list` to UNKNOWN", async () => {
|
||||
const cwd = makeRepo("https://gitea.example.com/testowner/testrepo.git");
|
||||
const bin = makeSandbox({ listOutput: '{"not":"an array"}' });
|
||||
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: { PATH: bin },
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: UNKNOWN");
|
||||
expect(stdout).toContain("unexpected output");
|
||||
});
|
||||
|
||||
it("tolerates login entries with missing fields", async () => {
|
||||
// A login object with no name/url/ssh_host exercises the field fallbacks;
|
||||
// it matches no host, so resolution ends in REPO_NOT_FOUND.
|
||||
const cwd = makeRepo("https://gitea.example.com/testowner/testrepo.git");
|
||||
const bin = makeSandbox({ listOutput: "[{}]" });
|
||||
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: { PATH: bin },
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: REPO_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("maps a failing token helper to AUTH_REQUIRED with a repair hint", async () => {
|
||||
const cwd = makeRepo("https://gitea.example.com/testowner/testrepo.git");
|
||||
const bin = makeSandbox({
|
||||
logins: [{ name: "fixture", url: "https://gitea.example.com", default: "true" }],
|
||||
helperExitCode: 1,
|
||||
helperStderr: "credential store is locked",
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: { PATH: bin },
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: AUTH_REQUIRED");
|
||||
expect(stdout).toContain("tea login edit fixture");
|
||||
expect(stdout).toContain("credential store is locked");
|
||||
});
|
||||
|
||||
it("maps an empty token from the helper to AUTH_REQUIRED", async () => {
|
||||
const cwd = makeRepo("https://gitea.example.com/testowner/testrepo.git");
|
||||
const bin = makeSandbox({
|
||||
logins: [{ name: "fixture", url: "https://gitea.example.com", default: "true" }],
|
||||
// A credential block that carries no usable password value.
|
||||
helperOutput: "protocol=http\nhost=gitea.example.com\nusername=u\npassword=",
|
||||
});
|
||||
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: { PATH: bin },
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: AUTH_REQUIRED");
|
||||
expect(stdout).toContain("tea login edit fixture");
|
||||
});
|
||||
});
|
||||
|
||||
234
test/e2e/provision.ts
Normal file
234
test/e2e/provision.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Provisioning for the end-to-end tier: bring a fresh, disposable Gitea instance
|
||||
* to a usable state entirely over its HTTP API, with no `docker exec` or shell
|
||||
* access to the container. Everything here runs identically on Gitea Actions,
|
||||
* GitHub Actions, and a developer's local `docker run gitea/gitea`.
|
||||
*
|
||||
* Bootstrap chain:
|
||||
* 1. Wait for the instance to answer `GET /api/v1/version`.
|
||||
* 2. Register the first user through the web sign-up form — Gitea makes the
|
||||
* first registered account the site administrator.
|
||||
* 3. Mint a scoped API token for that user via HTTP Basic auth (no CSRF).
|
||||
* 4. Create a repository and seed issues with that token.
|
||||
*/
|
||||
|
||||
export interface E2EInstance {
|
||||
/** Instance base URL, without the /api/v1 suffix (what the CLI expects). */
|
||||
baseUrl: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
token: string;
|
||||
/** Titles of the seeded open issues, in creation order (newest number last). */
|
||||
openTitles: string[];
|
||||
/** Title of the single seeded closed issue. */
|
||||
closedTitle: string;
|
||||
}
|
||||
|
||||
const USERNAME = "e2e-admin";
|
||||
const PASSWORD = "e2e-admin-password-123";
|
||||
const EMAIL = "e2e-admin@example.com";
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForGitea(baseUrl: string): Promise<void> {
|
||||
const deadline = Date.now() + 120_000;
|
||||
let lastError: unknown;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/v1/version`);
|
||||
if (res.ok) {
|
||||
return;
|
||||
}
|
||||
lastError = new Error(`GET /api/v1/version returned ${res.status}`);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
throw new Error(`Gitea at ${baseUrl} never became ready: ${String(lastError)}`);
|
||||
}
|
||||
|
||||
/** A minimal cookie jar: keep the latest value per cookie name. */
|
||||
function collectCookies(jar: Map<string, string>, res: Response): void {
|
||||
for (const header of res.headers.getSetCookie()) {
|
||||
const pair = header.split(";", 1)[0]!;
|
||||
const eq = pair.indexOf("=");
|
||||
if (eq > 0) {
|
||||
jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cookieHeader(jar: Map<string, string>): string {
|
||||
return [...jar.entries()].map(([name, value]) => `${name}=${value}`).join("; ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the first account through the web sign-up form. Gitea protects the
|
||||
* form with a double-submit CSRF token that must be scraped from the rendered
|
||||
* HTML and echoed back alongside the matching cookie. Best-effort: a fresh
|
||||
* instance succeeds here, and {@link mintToken} is the real gate on success (a
|
||||
* pre-existing account from a local re-run is tolerated).
|
||||
*/
|
||||
async function registerFirstUser(baseUrl: string): Promise<void> {
|
||||
const jar = new Map<string, string>();
|
||||
const getRes = await fetch(`${baseUrl}/user/sign_up`);
|
||||
collectCookies(jar, getRes);
|
||||
const html = await getRes.text();
|
||||
const csrf = html.match(/name="_csrf"\s+value="([^"]+)"/)?.[1];
|
||||
if (!csrf) {
|
||||
throw new Error("Could not find a CSRF token on the Gitea sign-up page");
|
||||
}
|
||||
const form = new URLSearchParams({
|
||||
_csrf: csrf,
|
||||
user_name: USERNAME,
|
||||
email: EMAIL,
|
||||
password: PASSWORD,
|
||||
retype: PASSWORD,
|
||||
});
|
||||
await fetch(`${baseUrl}/user/sign_up`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
cookie: cookieHeader(jar),
|
||||
},
|
||||
body: form.toString(),
|
||||
redirect: "manual",
|
||||
});
|
||||
}
|
||||
|
||||
function basicAuth(): string {
|
||||
return `Basic ${Buffer.from(`${USERNAME}:${PASSWORD}`).toString("base64")}`;
|
||||
}
|
||||
|
||||
async function mintToken(baseUrl: string): Promise<string> {
|
||||
const res = await fetch(`${baseUrl}/api/v1/users/${USERNAME}/tokens`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: basicAuth(),
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// write:user is what Gitea requires to create a repo under the user
|
||||
// (POST /user/repos); write:repository/write:issue cover the repo + issue
|
||||
// reads and writes the provisioning and CLI seam then perform.
|
||||
name: `e2e-${Date.now()}`,
|
||||
scopes: ["write:user", "write:repository", "write:issue"],
|
||||
}),
|
||||
});
|
||||
if (res.status !== 201) {
|
||||
throw new Error(
|
||||
`Token creation failed (${res.status}); first-user registration likely did not take. Body: ${await res.text()}`,
|
||||
);
|
||||
}
|
||||
const body = (await res.json()) as { sha1?: string };
|
||||
if (!body.sha1) {
|
||||
throw new Error("Token response had no sha1 field");
|
||||
}
|
||||
return body.sha1;
|
||||
}
|
||||
|
||||
/**
|
||||
* One authenticated Gitea API round-trip: attach the token, send an optional
|
||||
* JSON body, and fail on any non-2xx. Returns the raw {@link Response} so callers
|
||||
* can read the body or headers (e.g. the x-total-count the count line uses).
|
||||
*/
|
||||
async function apiRequest(
|
||||
baseUrl: string,
|
||||
method: string,
|
||||
path: string,
|
||||
token: string,
|
||||
payload?: unknown,
|
||||
): Promise<Response> {
|
||||
const res = await fetch(`${baseUrl}/api/v1${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
authorization: `token ${token}`,
|
||||
...(payload !== undefined ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
body: payload !== undefined ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`${method} ${path} failed (${res.status}): ${await res.text()}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function provisionInstance(baseUrl: string): Promise<E2EInstance> {
|
||||
const normalized = baseUrl.replace(/\/+$/, "");
|
||||
await waitForGitea(normalized);
|
||||
await registerFirstUser(normalized);
|
||||
const token = await mintToken(normalized);
|
||||
|
||||
const repo = `e2e-repo-${Date.now()}`;
|
||||
await apiRequest(normalized, "POST", "/user/repos", token, {
|
||||
name: repo,
|
||||
auto_init: true,
|
||||
default_branch: "main",
|
||||
private: false,
|
||||
});
|
||||
|
||||
const openTitles = ["E2E first issue", "E2E second issue", "E2E third issue"];
|
||||
for (const title of openTitles) {
|
||||
await apiRequest(normalized, "POST", `/repos/${USERNAME}/${repo}/issues`, token, {
|
||||
title,
|
||||
body: `Seeded body for ${title}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const closedTitle = "E2E closed issue";
|
||||
const closedRes = await apiRequest(normalized, "POST", `/repos/${USERNAME}/${repo}/issues`, token, {
|
||||
title: closedTitle,
|
||||
body: "Seeded closed issue.",
|
||||
});
|
||||
const closed = (await closedRes.json()) as { number: number };
|
||||
await apiRequest(normalized, "PATCH", `/repos/${USERNAME}/${repo}/issues/${closed.number}`, token, {
|
||||
state: "closed",
|
||||
});
|
||||
|
||||
return { baseUrl: normalized, owner: USERNAME, repo, token, openTitles, closedTitle };
|
||||
}
|
||||
|
||||
/**
|
||||
* The response-shape paths the issue-list command's FieldDef extractors read
|
||||
* (see ISSUE_LIST_FIELDS in src/commands/issue.ts). This one contract anchors
|
||||
* three things that must agree: the extractors, the recorded fixtures, and the
|
||||
* live Gitea response. The end-to-end shape guard asserts both the fixture and
|
||||
* the live payload satisfy it, so a divergence in either fails the tier.
|
||||
*/
|
||||
export const COVERED_ISSUE_PATHS = ["number", "title", "state", "created_at", "user.login"];
|
||||
|
||||
/** Whether `obj` has a defined value at a dotted `path` (e.g. "user.login"). */
|
||||
export function hasPath(obj: unknown, path: string): boolean {
|
||||
let value: unknown = obj;
|
||||
for (const key of path.split(".")) {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
value = (value as Record<string, unknown>)[key];
|
||||
}
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the raw issues-list response the CLI's issue-list command consumes, for
|
||||
* the response-shape guard. Returns both the parsed array and the header the
|
||||
* count line is built from.
|
||||
*/
|
||||
export async function fetchRawIssues(
|
||||
instance: E2EInstance,
|
||||
state: "open" | "closed" | "all",
|
||||
): Promise<{ issues: Record<string, unknown>[]; totalCount: string | null }> {
|
||||
const res = await apiRequest(
|
||||
instance.baseUrl,
|
||||
"GET",
|
||||
`/repos/${instance.owner}/${instance.repo}/issues?type=issues&state=${state}&limit=30&page=1`,
|
||||
instance.token,
|
||||
);
|
||||
return {
|
||||
issues: (await res.json()) as Record<string, unknown>[],
|
||||
totalCount: res.headers.get("x-total-count"),
|
||||
};
|
||||
}
|
||||
128
test/e2e/tracer.test.ts
Normal file
128
test/e2e/tracer.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { runCliTest } from "../harness.js";
|
||||
import {
|
||||
COVERED_ISSUE_PATHS,
|
||||
fetchRawIssues,
|
||||
hasPath,
|
||||
provisionInstance,
|
||||
type E2EInstance,
|
||||
} from "./provision.js";
|
||||
|
||||
/**
|
||||
* The end-to-end tier: the real CLI seam (argv in, stdout/exit-code out) against
|
||||
* a live, disposable Gitea instance seeded over its own API. Gated on
|
||||
* GITEA_AXI_E2E_URL so the default `npm test` (unit + integration tiers) never
|
||||
* needs a live instance; CI sets it to the service container's URL.
|
||||
*/
|
||||
const E2E_URL = process.env.GITEA_AXI_E2E_URL;
|
||||
|
||||
const RELATIVE_TIME = /(just now|\d+(m|h|d|mo|y) ago)/;
|
||||
|
||||
describe.skipIf(!E2E_URL)("end-to-end: tracer command set", () => {
|
||||
let instance: E2EInstance;
|
||||
|
||||
function env(overrides: Record<string, string> = {}): Record<string, string> {
|
||||
return {
|
||||
GITEA_AXI_API_URL: instance.baseUrl,
|
||||
GITEA_AXI_TOKEN: instance.token,
|
||||
GITEA_AXI_REPO: `${instance.owner}/${instance.repo}`,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
instance = await provisionInstance(E2E_URL!);
|
||||
}, 150_000);
|
||||
|
||||
it("lists seeded open issues with the default fields and a live count line", async () => {
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], { env: env() });
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
const lines = stdout.split("\n");
|
||||
expect(lines[0]).toBe("count: 3 of 3 total");
|
||||
expect(lines[1]).toBe("issues[3]{number,title,state,author,created}:");
|
||||
for (const title of instance.openTitles) {
|
||||
expect(stdout).toContain(title);
|
||||
}
|
||||
expect(stdout).toContain(`,open,${instance.owner},`);
|
||||
expect(stdout).toMatch(RELATIVE_TIME);
|
||||
expect(stdout).toMatch(/^help\[\d+\]:/m);
|
||||
// No `type` field ever leaks into the row header.
|
||||
expect(lines[1]).not.toContain("type");
|
||||
});
|
||||
|
||||
it("filters to the seeded closed issue with --state closed", async () => {
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list", "--state", "closed"], {
|
||||
env: env(),
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("count: 1 of 1 total");
|
||||
expect(stdout).toContain(instance.closedTitle);
|
||||
expect(stdout).toContain(",closed,");
|
||||
});
|
||||
|
||||
it("honors --limit and reports the full total from X-Total-Count", async () => {
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list", "--limit", "1"], {
|
||||
env: env(),
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("count: 1 of 3 total");
|
||||
expect(stdout).toContain("issues[1]{number,title,state,author,created}:");
|
||||
expect(stdout).toContain("issue list --limit <n>");
|
||||
});
|
||||
|
||||
it("classifies a missing repository as REPO_NOT_FOUND with exit code 1", async () => {
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list"], {
|
||||
env: env({ GITEA_AXI_REPO: `${instance.owner}/does-not-exist-e2e` }),
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("code: REPO_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("rejects an invalid --state before touching the network, exit code 2", async () => {
|
||||
const { stdout, exitCode } = await runCliTest(["issue", "list", "--state", "banana"], {
|
||||
env: env(),
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stdout).toContain("code: VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
it("renders the home view against the live repo", async () => {
|
||||
const { stdout, exitCode } = await runCliTest([], { env: env() });
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain(`repo: ${instance.owner}/${instance.repo}`);
|
||||
expect(stdout).toMatch(/^help\[\d+\]:/m);
|
||||
});
|
||||
|
||||
it("guards against fixture-vs-live divergence in the issues response shape", async () => {
|
||||
// The recorded fixture that the integration tier asserts against. The
|
||||
// covered-paths contract must hold on both it and the live payload; if
|
||||
// either drifts from the shape the extractors read, this tier fails.
|
||||
const fixture = JSON.parse(
|
||||
readFileSync(new URL("../fixtures/issues-open.json", import.meta.url), "utf8"),
|
||||
) as Record<string, unknown>[];
|
||||
for (const recorded of fixture) {
|
||||
for (const path of COVERED_ISSUE_PATHS) {
|
||||
expect(hasPath(recorded, path), `fixture missing ${path}`).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
const { issues, totalCount } = await fetchRawIssues(instance, "open");
|
||||
// The count line is built from this header; its absence would silently
|
||||
// change output, so it is part of the covered shape.
|
||||
expect(totalCount).toBe("3");
|
||||
expect(issues).toHaveLength(3);
|
||||
for (const issue of issues) {
|
||||
for (const path of COVERED_ISSUE_PATHS) {
|
||||
expect(hasPath(issue, path), `live issue missing ${path}`).toBe(true);
|
||||
}
|
||||
expect((issue.user as Record<string, unknown>).login).toBe(instance.owner);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,32 @@ import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// Unit + integration tiers: fast, no external dependencies. The end-to-end
|
||||
// tier under test/e2e needs a live Gitea instance and runs via `test:e2e`.
|
||||
include: ["test/**/*.test.ts"],
|
||||
exclude: ["test/e2e/**"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
// Measure the shipped source only; the CLI is exercised through its seam,
|
||||
// so coverage reflects what those tiers actually reach.
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
// Types-only: no runtime code to exercise.
|
||||
"src/deps.ts",
|
||||
// Bin entrypoint: process argv/stdout/EPIPE wiring that delegates to the
|
||||
// covered runCli seam; not reached by the in-process seam tests.
|
||||
"src/main.ts",
|
||||
],
|
||||
// A regression ratchet, not a target: set a few points under current
|
||||
// coverage so a real drop fails CI while trivial churn does not. Raise
|
||||
// these as coverage climbs; never lower them to make a red build pass.
|
||||
thresholds: {
|
||||
statements: 92,
|
||||
branches: 87,
|
||||
functions: 95,
|
||||
lines: 92,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
15
vitest.e2e.config.ts
Normal file
15
vitest.e2e.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// The end-to-end tier only: the same CLI seam as the integration tier, but
|
||||
// driven against a live disposable Gitea instance (see test/e2e). Provision
|
||||
// and network round-trips need a longer timeout than the fast tiers.
|
||||
include: ["test/e2e/**/*.test.ts"],
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 150_000,
|
||||
// Without a live instance (GITEA_AXI_E2E_URL unset) every suite skips;
|
||||
// that must be a pass, not a "no tests found" failure.
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user