mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
Migrate apps/vscode from npm/node to bun (#11632)
* chore(vscode): migrate package management & build from npm/node to bun
Fold apps/vscode (+ webview-ui, testing-platform) into the root bun
workspace so the extension consumes the local @cline/* SDK packages via
workspace symlinks instead of pinned published versions, eliminating the
SDK vendoring cycle. Node remains the runtime (extension host, standalone
cline-core, esbuild platform:node, prebuild-install ABI target).
- root: drop "!apps/vscode", add nested members, relocate overrides to
root, add trustedDependencies [better-sqlite3, grpc-tools]
- apps/vscode: @cline/* -> workspace:*, scripts -> bun/bunx,
npm-run-all -> bun --parallel, drop cross-env; keep esbuild + vite;
declare previously-hoisted phantom deps (nice-grpc-common, playwright)
- package-standalone.mjs: npm install -> bun install (isolated dist dir)
- CI: setup-bun + single root bun install --frozen-lockfile, build:sdk
before extension build, better-sqlite3 binary + zero-test guards;
publish workflows intentionally keep setup-node for vsce/ovsx
- docs/comments: curated pass (keep-list vs rewrite-list), add
apps/vscode/docs/bun-migration-notes.md guard doc
- delete npm lockfiles (root bun.lock authoritative)
Deferred to follow-up PRs: test-runner migration to bun test (Phase 4)
and devDep cleanup (Phase 6).
* test(vscode): add bun test foundation for the vitest-native unit suites
Phase 4a of the test-runner migration. Adds a bun test runner that
reaches full parity (582 pass / 0 fail / 50 files) with the existing
vitest SDK-adapter + model-catalog suite, without touching the
@vscode/test-cli integration tests or the webview vitest suite.
- bunfig.toml: [test] preload
- src/test/bun-test-preload.ts: mock.module() shadows `vscode` and
`@cline/core` with their unit-test stubs (bun's onResolve plugin hook
does not intercept host/symlinked specifiers); seeds real @cline/core
export names as undefined to satisfy bun's strict ESM named-import
linking; full vitest->bun:test shim (vi.fn/mocked/spyOn, describe/it/
expect/before*/after*)
- scripts/run-bun-tests.ts: mirrors vitest.config.ts include[] exactly and
runs with --parallel for per-file mock isolation (bun test's single-process
default lets mock.module clobber across files)
- test:bun script
* test(vscode): migrate node-side unit suite from mocha to bun test
Phase 4b of the test-runner migration. The standalone mocha unit runner
(.mocharc spec: __tests__/* + test/services/**) was already broken under
bun (mocha was a phantom dependency — only @types/mocha/ts-node were
declared, npm hoisted mocha transitively). Migrate it to `bun test`.
- codemod 77 files: import { ... } from "mocha" -> "bun:test", renaming
before->beforeAll / after->afterAll at imports and call-sites; chai,
should and sinon kept as libraries (they work under bun test)
- convert sinon.stub() on ESM namespace exports to mock.module()/spyOn
(bun loads real ESM: "ES Modules cannot be stubbed")
- scripts/run-bun-unit-tests.ts: runs the .mocharc spec set with one
isolated `bun test` process per file (Bun.spawn + concurrency pool),
restoring vitest-forks module-registry isolation (bun's single-process
default lets mock.module leak across files)
- scripts/codemod-mocha-{to-bun,this}.ts: one-shot migration tooling
- test:unit now runs the bun unit runner; CI calls bun + a non-zero
pass-count guard instead of `bunx nyc ... mocha`
- tsconfig: add root node_modules/@types to typeRoots so `bun:test`
types resolve under tsc; cast loose os.userInfo mocks in shell.test
Result: unit suite 58 files / 880 pass / 0 fail; vitest set still
582/0. @vscode/test-cli integration tests and webview vitest unchanged.
* chore(vscode): remove dead mocha-runner deps and artifacts
Phase 6 cleanup after the bun test migration. The standalone mocha unit
runner is gone (replaced by scripts/run-bun-unit-tests.ts), so its
config and now-unused devDependencies are removed.
- remove dead files: .mocharc.json, tsconfig.unit-test.json,
src/test/requires.ts, .nycrc.unit.json
- remove unused devDeps: @types/mocha, @types/proxyquire, ts-node,
tsconfig-paths, cross-env, npm-run-all, nyc, proxyquire, husky
(root owns the husky hook; chai/should/sinon stay — used as libs)
- install:all -> single root `bun install` (workspace covers webview-ui)
- drop .mocharc.json / .nycrc*.json from CI paths-filters and
.vscodeignore; add bunfig.toml to the filters
Verified: check-types clean, unit 880/0, vitest 582/0.
* fix(vscode): import bun:test globals in tests that relied on ambient @types/mocha
CI Quality Checks (clean `bun install` without @types/mocha) surfaced
TS2582/TS2304 "Cannot find name 'describe'/'it'/'beforeEach'" in test
files that used the global mocha/jest test functions without importing
them. The Phase 4b codemod only rewrote files that imported from
"mocha"; these used ambient globals, so they were missed (and passed
locally because a stale @types/mocha lingered in node_modules).
Add explicit `bun:test` imports (before->beforeAll, after->afterAll in
TelemetryService.test.ts). chai/sinon stay as libraries.
Verified against a clean tree (no @types/mocha): check-types 0 errors,
unit suite 58 files / 880 pass / 0 fail.
* style(vscode): biome-format migrated test files + codemod scripts
The mocha->bun:test codemod and manual import edits left formatting that
didn't match biome (the CI `format` check, which validates files changed
since main, flagged them). Also narrow setup.ts's bun:test import to the
actually-used beforeEach/afterEach (describe/it only appear in a JSDoc
example), fixing a noUnusedImports lint error.
ci:check-all (check-types + lint + format) now passes locally.
* fix(webview-ui): declare phantom deps + pin React 18 types under bun workspace
Folding webview-ui into the bun workspace changed its install topology
from an isolated npm flat tree to the shared hoisted store, surfacing
two classes of pre-existing latent issues that npm hoisting had masked:
1. Phantom dependencies: src imports `marked`, `unist`, `unist-util-visit`
and `@heroui/theme` directly but never declared them. Declared them
(marked ^15, unist-util-visit ^5, @types/unist ^3, @heroui/theme 2.4.26).
2. React types: @testing-library/react's optional peer pulls @types/react@19
into a resolvable location; tsc mixed it with the toolkit's React 18
types (React 19 dropped Component.refs), breaking 452 JSX usages. Pin
react/react-dom type resolution to webview-ui's React 18 copy via
tsconfig paths.
build:webview (tsc -b && vite build) and ci:check-all now pass.
* fix(vscode): restore @types/mocha for integration build + add bun:test types
The @vscode/test-cli integration runner still uses mocha, and
tsconfig.test.json compiles all src/**/*.test.ts (including bun-migrated
files) to out/. So:
- restore @types/mocha (integration compile needs the mocha ambient types)
- add `bun` to tsconfig.test.json types + root @types to both tsconfig
typeRoots so `bun:test` resolves under tsc for the migrated tests
* fix(vscode): declare glob — phantom dep used by package-standalone.mjs
scripts/package-standalone.mjs imports `glob` but it was never declared
(resolved transitively under npm's flat hoist). Under the bun workspace
store it's unresolvable, failing postcompile-standalone with
ERR_MODULE_NOT_FOUND. Declare glob ^11 (modern named-export API).
compile-standalone now produces dist-standalone/standalone.zip.
* fix(ci): strip ANSI before vitest zero-test guard grep
The vitest summary line colorizes the count ("Tests <ansi>582 passed"),
so the count isn't adjacent to the "Tests" label in raw bytes and the
guard regex failed even though 582 tests passed. Strip ANSI escapes
before matching.
* fix(vscode): declare minimist — phantom dep in testing-platform-orchestrator
scripts/testing-platform-orchestrator.ts imports `minimist` (undeclared,
resolved transitively under npm hoist). Declare it so the testing-platform
integration job runs under the bun workspace store.
* fix(vscode): restore tsconfig-paths for integration runner; tp-orchestrator uses bun
Phase 6 over-removed tsconfig-paths: test-setup.js (loaded by the
@vscode/test-cli mocha integration runner) requires it to resolve @/
aliases in the compiled out/ tree — the extension host test runner failed
with "Cannot find module 'tsconfig-paths'". Restore it. Also switch the
testing-platform spawn from `npx ts-node index.ts` to `bun index.ts`
(bun runs TS natively; avoids the removed ts-node).
* fix(vscode): route tests by bun:test import marker; integration runner stays mocha
The mocha->bun codemod swept up tests that the Node-based @vscode/test-cli
integration runner compiles/runs, which cannot load the `bun:test` builtin
(and some need the real VSCode host). Establish a single source of truth:
a *.test.ts is bun-runner-owned IFF it imports "bun:test".
- run-bun-unit-tests.ts: discover files by the bun:test import marker
(not fixed globs), so every migrated file runs under bun.
- build-tests.js: generate a tsconfig that excludes all bun:test files
from the integration compile (json5-parsed), so out/ never contains
bun:test; gitignore the generated config.
- .vscode-test.mjs: exclude the bun unit dirs from the runner globs.
- revert host-dependent tests (hostbridge/*, extension, terminal,
FileContextTracker host bits) and 3 files with sinon-on-ESM/behavioral
issues (ClineIgnoreController, mentions, TelemetryService) back to
mocha; they run on @vscode/test-cli as before.
Verified: check-types 0 errors; compile-tests 0 bun:test in out/;
bun unit 65 files/962 pass/0 fail; vitest 582/0.
* fix(vscode): declare mocha — phantom dep for @vscode/test-cli integration runner
The @vscode/test-cli extension host loads `mocha` at runtime to run the
integration suite, but only @types/mocha was declared (npm hoisted the
mocha package transitively; bun's store does not expose it). The host
failed with "Cannot find module 'mocha'". Declare mocha ^11.7.4 (matches
@vscode/test-cli's own range).
* fix(vscode): robust Windows protoc-gen-ts_proto plugin resolution under bun
build-proto.mjs hardcoded node_modules/.bin/protoc-gen-ts_proto.cmd for
Windows, but bun's workspace store places/extensions the bin shim
differently (hoist + .cmd/.bunx), so Windows protos failed with
"protoc-gen-ts_proto: The system cannot find the file specified". Probe
the local + root .bin with known shim extensions instead. Also update
the testing-platform usage string (ts-node -> bun).
* fix(vscode): generate node .cmd wrapper for ts-proto plugin on Windows
The previous probe found bun's `.bunx` shim, but protoc cannot exec it
("%1 is not a valid Win32 application"). Instead, on Windows generate a
small .cmd wrapper that runs the resolved protoc-gen-ts_proto JS via
`node`, which protoc can execute regardless of package manager. POSIX
path (direct JS bin) is unchanged.
* fix(vscode): package VSIX with --no-dependencies (bundled) to stop monorepo traversal
Under the bun workspace, @cline/* are workspace:* symlinks pointing to
../../../../sdk/packages/*. vsce, walking the dependency tree, followed
them out of apps/vscode and packaged the whole monorepo (../, ~84MB incl.
root node_modules and .env), which crashed vsce's secret scanner and
failed all e2e jobs.
The extension is fully esbuild-bundled into dist/extension.js, so vsce
should not walk node_modules at all. Add --no-dependencies to every
vsce/ovsx package/publish path (e2e build, marketplace, nightly), and
tighten .vscodeignore to drop nested node_modules and dev-only inputs
(scripts, proto, testing-platform, bunfig, esbuild.mjs, etc.).
Result: VSIX is 39 files / ~7 MB and the secret scan passes.
* docs(vscode): tighten bun/node comments and consolidate into a clinerule
- add .clinerules/bun-and-node.md (eternal-now: bun=tooling, node=runtime,
keep-list, and the bun:test-vs-mocha test routing rule); remove the
apps/vscode/docs/bun-migration-notes.md migration doc and point
.clinerules/general.md at the rule (single-line bullet matching the file).
- fix the hotfix-release note: there is no infra step that regenerates the
lockfile; a CHANGELOG+version bump leaves bun.lock consistent (workspace
versions aren't pinned) and publish runs --frozen-lockfile.
- reframe runner/preload comments to describe the code as-is (drop
"migrated off mocha"/codemod history); add a TODO on the bun-test preload
to migrate suites off the vitest `vi` shim to native bun:test and delete it.
- remove the one-shot mocha->bun codemod scripts.
* fix(debug-harness): pin debugee VSCode version so bundled Playwright can drive it
The harness downloaded "stable" VSCode (currently 1.125 / Electron 42),
which the bundled Playwright cannot drive — `_electron.launch()` hangs
until its 60s timeout (Electron started and a window appeared, but the
launch handshake never completed). Default to a known-good version
(1.103.0, matching the e2e CI matrix) and allow override via
VSCODE_TEST_VERSION.
* fix(webview): render under bun workspace — dedupe React, drop stale codicons link
The webview mounted but crashed before rendering (blank sidebar; e2e
"Login to Cline" never visible) with "Cannot read properties of null
(reading 'useRef')" — the classic two-React-copies / null hook dispatcher.
Under the bun workspace, sibling packages pull react@19 into the shared
store and a transitive webview dep resolved a second React instance into
the vite bundle. Add resolve.dedupe + pin react/react-dom to webview-ui's
own React 18 copy.
Also drop the separate `<link>` to node_modules/@vscode/codicons in the
webview HTML: the webview's index.css already @imports codicons, so the
font is bundled into the build assets. Under bun that node_modules path
is a symlink to the root store (outside the webview localResourceRoots)
and isn't packaged with --no-dependencies, so the link 404'd; the bundle
covers it. Re-scope the .vscodeignore nested-node_modules exclude so it
no longer shadows the codicons re-include.
* fix(debug-harness): disable GPU so the debugee renders in headless/VM envs
On headless/VM GPU stacks the debugee Electron's GPU process crash-loops
("Exiting GPU process during initialization" / CreateCommandBuffer
kTransientFailure), killing the window before Playwright finishes
attaching and tripping the 60s launch timeout. Force software rendering
(--disable-gpu and friends) for a stable harness launch.
* fix(debug-harness): survive launch failures; configurable, longer launch timeout
The harness crashed (whole bun process exited) whenever VSCode launch
failed/timed out: Playwright emits a late unhandled rejection on the dead
CDP transport after we've already handled the launch error, and the
default behavior takes the HTTP server down with it — forcing a full
restart just to retry.
- Add process-level unhandledRejection/uncaughtException guards so stray
async errors are logged and the server keeps serving (retry via `launch`).
- On launch failure, close the orphaned Electron so a retry isn't blocked.
- Make the _electron.launch timeout configurable (--launch-timeout) and
raise the default to 120s for cold launches; document VSCODE_TEST_VERSION.
* fix(ci): address review feedback — vsix --no-dependencies, drop stale coverage path, Windows shell
- ext-vscode-publish-stable.yml: add --no-dependencies to the release-artifact
`vsce package` (Max's catch). Without it, vsce follows the @cline/* workspace
symlinks out of the package and bloats the .vsix with the whole monorepo.
- ext-vscode-test.yml: drop the stale apps/vscode/coverage-unit/lcov.info upload
path (Max's catch). That file was produced by the removed nyc unit-coverage
step (.nycrc.unit.json); nothing generates it now.
- ext-vscode-test-e2e.yml: the better-sqlite3 assert step ran under the Windows
runner's default pwsh and failed to parse the POSIX test. Pin it to `shell: bash`
(Git Bash ships on windows-latest); the non-e2e job already defaults to bash.
---------
Co-authored-by: Cline Agent <cline-agent@users.noreply.github.com>
This commit is contained in:
co-authored by
Cline Agent
parent
d29c5ceb88
commit
2946698eee
@@ -0,0 +1,55 @@
|
||||
# Bun (tooling) and Node (runtime)
|
||||
|
||||
This repo uses **bun** for package management and task running, and **Node** as
|
||||
the execution runtime. Both are correct at the same time; the distinction is the
|
||||
source of most confusion, so keep it straight before editing scripts, configs,
|
||||
docs, or comments.
|
||||
|
||||
## Use bun for tooling
|
||||
|
||||
- `bun install` (never `npm install` / `npm ci`)
|
||||
- `bun run <script>` (never `npm run <script>`)
|
||||
- `bunx <bin>` (never `npx <bin>`)
|
||||
- `bun <file>.ts` to run a TS entrypoint directly (no `ts-node` / `tsx`)
|
||||
- `bun esbuild.mjs` to drive the build (esbuild/vite are still the bundlers)
|
||||
- `bun run --parallel ...` for parallel tasks
|
||||
|
||||
The root `bun.lock` is the single lockfile for the whole workspace, including
|
||||
`apps/vscode`, `webview-ui`, and `testing-platform`. There are no per-package npm
|
||||
lockfiles.
|
||||
|
||||
## Node is the runtime — do NOT rewrite these to bun
|
||||
|
||||
The build product runs on Node: the VS Code extension host loads
|
||||
`dist/extension.js` as CommonJS under Node, and the standalone `cline-core` is a
|
||||
Node process. The following are Node runtime/ABI references and are correct as-is:
|
||||
|
||||
| Reference | Why it is Node |
|
||||
|-----------|----------------|
|
||||
| esbuild `platform: "node"` / `target: "node..."` | The bundle targets the Node runtime (extension host, standalone core). |
|
||||
| `TARGET_NODE_VERSION` (`scripts/package-standalone.mjs`) | Pins the Node ABI of the bundled standalone runtime (matches the JetBrains-packaged Node). |
|
||||
| `prebuild-install --target=<node version>` | Downloads native `.node` binaries for that Node ABI. |
|
||||
| `NODE_PATH=... node cline-core.js` | The standalone core is launched by Node, not bun. |
|
||||
| `node:` import specifiers (e.g. `node:fs`) | Node builtin module scheme; unrelated to tooling. |
|
||||
| `process.versions.node`, `engines.node`, `@types/node` | Runtime version probe / declared runtime / its types. |
|
||||
| `ELECTRON_RUN_AS_NODE` | VS Code/Electron runs the extension host as Node. |
|
||||
|
||||
When a file legitimately uses both bun and node (e.g. `package-standalone.mjs`
|
||||
does `bun install` but `prebuild-install --target=<node>`), the `node` token is
|
||||
the runtime/ABI target, not tooling. If unsure, leave it.
|
||||
|
||||
## Tests: bun vs the VS Code host
|
||||
|
||||
A test file's runner is decided by its import:
|
||||
|
||||
- **`import ... from "bun:test"`** → runs under `bun test` (the node-side unit
|
||||
suites + the SDK/model-catalog suites). `scripts/run-bun-unit-tests.ts`
|
||||
discovers these by the `bun:test` import and runs one isolated bun process per
|
||||
file. `build-tests.js` excludes them from the integration compile so the
|
||||
`bun:test` builtin never reaches Node.
|
||||
- **`import ... from "mocha"`** → runs under `@vscode/test-cli` in a real VS Code
|
||||
extension host (Node). These exercise the live `vscode` API and cannot run
|
||||
under bun.
|
||||
|
||||
So a file imports `bun:test` XOR `mocha`. Don't add `bun:test` to a test that
|
||||
needs the real extension host.
|
||||
@@ -6,10 +6,10 @@ HTTP-controlled debugger for the VSCode extension at `src/dev/debug-harness/serv
|
||||
|
||||
```bash
|
||||
# Build extension first if needed (protos + esbuild):
|
||||
npm run protos && IS_DEV=true node esbuild.mjs
|
||||
bun run protos && IS_DEV=true bun esbuild.mjs
|
||||
|
||||
# Launch (skip-build if already built):
|
||||
npx tsx src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
bun src/dev/debug-harness/server.ts --skip-build --auto-launch
|
||||
|
||||
# In another terminal:
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
@@ -51,7 +51,7 @@ debug-only hook via `ext.evaluate` (with `awaitPromise: true`):
|
||||
It runs the same `SharedUriHandler.handleUri` as VSCode's real URI handler and exists only
|
||||
when `CLINE_CAPTURE_BROWSER` is set (the harness always sets it; never ships in prod).
|
||||
For end-to-end MCP OAuth, get a real `code` from the local MCP OAuth test server
|
||||
(`npm run dev:mcp-oauth-test-server`).
|
||||
(`bun run dev:mcp-oauth-test-server`).
|
||||
|
||||
## Navigating Views — Use Commands, Not Clicks
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ This file is the secret sauce for working effectively in this codebase. It captu
|
||||
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
|
||||
|
||||
## Miscellaneous
|
||||
- The whole repo (including `apps/vscode`) uses **bun** for package management and task running. Emit `bun run X` / `bun install` / `bunx <bin>` / `bun file.ts`, never npm/npx. Node remains the *runtime* (VS Code's extension host and the standalone cline-core are Node), so Node-runtime tokens are legitimate and must not be "fixed" to bun — see @.clinerules/bun-and-node.md for the keep-list vs rewrite-list.
|
||||
- Avoid provider-specific string matching / hardcoded provider branches when fixing provider/config plumbing. Prefer provider metadata, shared catalog/defaults, explicit protocol/client capabilities, or centralized normalization utilities that apply by data shape rather than `providerId === "..."`. If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `bun run compile`, not `bun run build`).
|
||||
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
|
||||
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
|
||||
- Additional instructions about making requests: @.clinerules/network.md
|
||||
@@ -72,7 +73,7 @@ The extension and webview communicate via gRPC-like protocol over VS Code messag
|
||||
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
|
||||
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
|
||||
|
||||
**Run `npm run protos`** after any proto changes—generates types in:
|
||||
**Run `bun run protos`** after any proto changes—generates types in:
|
||||
- `src/shared/proto/` - Shared type definitions
|
||||
- `src/generated/grpc-js/` - Service implementations
|
||||
- `src/generated/nice-grpc/` - Promise-based clients
|
||||
@@ -108,7 +109,7 @@ Settings plumbing gotcha: if a key is user-toggleable from settings, wire both c
|
||||
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
|
||||
|
||||
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
|
||||
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
|
||||
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `bun run protos`
|
||||
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
|
||||
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
|
||||
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
|
||||
@@ -181,7 +182,7 @@ env -u ELECTRON_RUN_AS_NODE -u ELECTRON_NO_ATTACH_CONSOLE \
|
||||
-u VSCODE_CLI -u VSCODE_CODE_CACHE_PATH -u VSCODE_CRASH_REPORTER_PROCESS_TYPE \
|
||||
-u VSCODE_CWD -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_UNCAUGHT_ERRORS \
|
||||
-u VSCODE_IPC_HOOK -u VSCODE_NLS_CONFIG -u VSCODE_PID -u VSCODE_L10N_BUNDLE_LOCATION \
|
||||
npx tsx src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
```
|
||||
|
||||
Check your own env with `env | grep -iE 'electron|vscode_'` first; `ELECTRON_RUN_AS_NODE=1`
|
||||
|
||||
@@ -42,7 +42,7 @@ Here, we use the common `StringRequest` and `KeyValuePair` types.
|
||||
|
||||
After editing a `.proto` file, regenerate the TypeScript code. From the project root, run:
|
||||
```bash
|
||||
npm run protos
|
||||
bun run protos
|
||||
```
|
||||
This command compiles all `.proto` files and outputs the generated code to `src/generated/` and `src/shared/`. Do not edit these generated files manually.
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ On the main branch, create a commit that updates:
|
||||
|
||||
3. No changelog-entry file cleanup is needed. Contributors do not create changelog-entry files in this repo.
|
||||
|
||||
**Skip running `npm run install:all`** - release automation handles lockfile consistency as needed.
|
||||
**No dependency install is needed.** A CHANGELOG + `version` bump does not change any dependency, and `bun.lock` does not pin workspace-package versions, so the lockfile stays consistent. The publish workflow runs `bun install --frozen-lockfile`, which would *fail* on an out-of-sync lock — so only run `bun install` here if you actually change dependencies (then commit the updated `bun.lock`).
|
||||
|
||||
Commit with message format: `v{VERSION} Release Notes (hotfix)`
|
||||
|
||||
|
||||
@@ -53,25 +53,47 @@ jobs:
|
||||
echo "Building sha: $GITHUB_SHA"
|
||||
git --no-pager log -1 --oneline
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
# Node is still REQUIRED in the publish job (not just for install): the publish
|
||||
# scripts run as `node ./scripts/publish-*.mjs` and shell out to `npx ovsx`.
|
||||
# setup-bun does not provide a Node runtime, so keep setup-node here.
|
||||
# Pinned to Node 22 because newer LTS (Node 24 / npm 11) can make vsce's
|
||||
# `npm list` dependency detection fail with ELSPROBLEMS during packaging.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
|
||||
- name: Install extension dependencies
|
||||
# Single root install resolves the whole bun workspace at once (replaces the
|
||||
# per-package `npm ci` steps for apps/vscode + webview-ui).
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci --include=optional
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
# @cline/* are local workspace symlinks to source packages; build dist/ before
|
||||
# packaging/publishing the extension.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci --include=optional
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
run: |
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
|
||||
echo "(bun trustedDependencies postinstall likely did not run)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
# vsce is now a workspace devDependency (on node_modules/.bin), but ovsx is not
|
||||
# vendored and the publish script invokes it via `npx ovsx`, so install ovsx
|
||||
# globally here (npm is available via setup-node). vsce is installed globally
|
||||
# too to preserve the script's existing PATH expectations.
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
@@ -89,7 +111,9 @@ jobs:
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
# The script itself runs under `node ./scripts/publish-nightly.mjs`; bun run
|
||||
# just launches it. Node + npm (for `npx ovsx`) are provided by setup-node above.
|
||||
run: bun run publish:marketplace:nightly
|
||||
|
||||
- name: Tag published commit
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
@@ -109,19 +109,48 @@ jobs:
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "resolved_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
# Node is still REQUIRED in the publish job (not just for install): the
|
||||
# publish scripts run as `node scripts/publish-*.mjs`, the version step uses
|
||||
# `node -p`, and `npx ovsx` needs npm. setup-bun does not provide a Node
|
||||
# runtime, so keep setup-node. Pinned to Node 22 because newer LTS
|
||||
# (Node 24 / npm 11) can make vsce's `npm list` detection fail with
|
||||
# ELSPROBLEMS during packaging.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install extension dependencies
|
||||
# Single root install resolves the whole bun workspace at once (replaces the
|
||||
# per-package `npm install` steps for apps/vscode + webview-ui).
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode install --include=optional
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
# @cline/* are local workspace symlinks to source packages; build dist/ before
|
||||
# packaging/publishing the extension.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui install --include=optional
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
run: |
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
|
||||
echo "(bun trustedDependencies postinstall likely did not run)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
# vsce is a workspace devDependency (on node_modules/.bin), but ovsx is not
|
||||
# vendored and the publish script invokes it via `npx ovsx`, so install ovsx
|
||||
# globally (npm is available via setup-node). vsce is installed globally too
|
||||
# to preserve the script's existing PATH expectations.
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
@@ -164,14 +193,20 @@ jobs:
|
||||
node scripts/marketplace-readme.mjs swap-in
|
||||
trap 'node scripts/marketplace-readme.mjs restore' EXIT
|
||||
|
||||
# Required to generate the .vsix
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
# Required to generate the .vsix. --no-dependencies: the extension
|
||||
# is fully esbuild-bundled, and under the bun workspace the @cline/*
|
||||
# deps are symlinks pointing outside the package, so without this vsce
|
||||
# would walk them and pull the whole monorepo into the .vsix.
|
||||
vsce package --no-dependencies --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
# These scripts run under `node scripts/publish-marketplace.mjs`;
|
||||
# bun run just launches them. Node + npm (for `npx ovsx`) come from
|
||||
# setup-node above.
|
||||
if [ "$RELEASE_TYPE" = "pre-release" ]; then
|
||||
npm run publish:marketplace:prerelease
|
||||
bun run publish:marketplace:prerelease
|
||||
echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
else
|
||||
npm run publish:marketplace
|
||||
bun run publish:marketplace
|
||||
echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry"
|
||||
fi
|
||||
|
||||
|
||||
@@ -45,12 +45,16 @@ jobs:
|
||||
- 'apps/vscode/assets/**'
|
||||
- 'apps/vscode/walkthrough/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/webview-ui/package.json'
|
||||
# Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode).
|
||||
- 'bun.lock'
|
||||
# SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build.
|
||||
- 'sdk/packages/**'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/biome.jsonc'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.mocharc.json'
|
||||
- 'apps/vscode/bunfig.toml'
|
||||
- 'apps/vscode/.vscode-test.mjs'
|
||||
- 'apps/vscode/.vscodeignore'
|
||||
- 'apps/vscode/playwright*.ts'
|
||||
@@ -84,26 +88,20 @@ jobs:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
bun-version: 1.3.14
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
# Cache Bun's global install cache - keyed on the authoritative root bun.lock.
|
||||
- name: Cache Bun install cache
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
id: bun-cache
|
||||
with:
|
||||
path: apps/vscode/node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('apps/vscode/package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: apps/vscode/webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('apps/vscode/webview-ui/package-lock.json') }}
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# Cache VS Code installation
|
||||
- name: Cache VS Code
|
||||
@@ -124,22 +122,41 @@ jobs:
|
||||
~/.cache/ms-playwright
|
||||
~/Library/Caches/ms-playwright
|
||||
~/AppData/Local/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('apps/vscode/package-lock.json') }}
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install extension dependencies
|
||||
# Single root install resolves the entire bun workspace at once (replaces
|
||||
# the per-package `npm ci` steps for apps/vscode + webview-ui).
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
# @cline/* are local workspace symlinks to source packages; build dist/
|
||||
# before building/packaging the extension for E2E.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Install vsce
|
||||
run: npm install -g @vscode/vsce
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
# Force bash: the Windows runner defaults to pwsh, which can't parse this
|
||||
# POSIX test. Git Bash ships on GitHub's windows-latest images.
|
||||
shell: bash
|
||||
run: |
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
|
||||
echo "(bun trustedDependencies postinstall likely did not run)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
# NOTE: vsce is no longer installed globally. @vscode/vsce is a workspace
|
||||
# devDependency of apps/vscode (resolved into node_modules/.bin), and the
|
||||
# `test:e2e:build` script invokes `vsce` via `bun run`, which puts the local
|
||||
# .bin on PATH. No global install needed.
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
@@ -148,11 +165,11 @@ jobs:
|
||||
# Run optimized E2E tests (eliminates redundant builds)
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:e2e:optimal
|
||||
run: xvfb-run -a bun run test:e2e:optimal
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:e2e:optimal
|
||||
run: bun run test:e2e:optimal
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -45,13 +45,16 @@ jobs:
|
||||
- 'apps/vscode/assets/**'
|
||||
- 'apps/vscode/walkthrough/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/webview-ui/package.json'
|
||||
# Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode).
|
||||
- 'bun.lock'
|
||||
# SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build.
|
||||
- 'sdk/packages/**'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/biome.jsonc'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.mocharc.json'
|
||||
- 'apps/vscode/.nycrc*.json'
|
||||
- 'apps/vscode/bunfig.toml'
|
||||
- 'apps/vscode/.vscode-test.mjs'
|
||||
- 'apps/vscode/test-setup.js'
|
||||
- '.github/workflows/ext-vscode-test.yml'
|
||||
@@ -60,9 +63,13 @@ jobs:
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/testing-platform/**'
|
||||
- 'apps/vscode/testing-platform/package.json'
|
||||
- 'apps/vscode/tests/specs/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
# Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode).
|
||||
- 'bun.lock'
|
||||
# SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build.
|
||||
- 'sdk/packages/**'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
@@ -82,27 +89,38 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install extension dependencies
|
||||
# Single root install resolves the entire bun workspace (apps/vscode,
|
||||
# webview-ui, testing-platform and the @cline/* SDK symlinks) at once,
|
||||
# so the previous per-package `npm ci` steps collapse into one.
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci --include=optional
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
# @cline/* are local workspace symlinks to source packages; their dist/
|
||||
# output must be built before the extension can type-check/compile.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci --include=optional
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
run: |
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
|
||||
echo "(bun trustedDependencies postinstall likely did not run)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
run: npm run ci:check-all
|
||||
run: bun run ci:check-all
|
||||
|
||||
vscode-test:
|
||||
needs: [detect-changes, quality-checks]
|
||||
@@ -123,30 +141,43 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install extension dependencies
|
||||
# Single root install resolves the entire bun workspace at once (replaces
|
||||
# the per-package `npm ci` steps for apps/vscode + webview-ui).
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci --include=optional
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
# @cline/* are local workspace symlinks to source packages; build dist/
|
||||
# before compiling/testing the extension.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci --include=optional
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
|
||||
echo "(bun trustedDependencies postinstall likely did not run)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
# NOTE: The old `npm config set script-shell bash` step is intentionally
|
||||
# removed. Scripts are now launched with `bun run`, which uses Bun's own
|
||||
# built-in cross-platform shell rather than npm's configured script-shell,
|
||||
# so that npm-specific Windows workaround no longer applies. Bash-dependent
|
||||
# scripts (e.g. scripts/proto-lint.sh, standalone/runclinecore.sh) are
|
||||
# invoked explicitly via `bash ...` from within the package scripts, and
|
||||
# this job's `defaults.run.shell: bash` (Git Bash on Windows) still covers
|
||||
# the workflow `run:` blocks below.
|
||||
|
||||
- name: Cache VS Code test runtime
|
||||
if: runner.os == 'Windows'
|
||||
@@ -158,29 +189,51 @@ jobs:
|
||||
# Build the extension and tests (without redundant checks)
|
||||
- name: Build Tests and Extension
|
||||
id: build_step
|
||||
run: npm run ci:build
|
||||
run: bun run ci:build
|
||||
|
||||
- name: Vitest Suites (SDK adapter + model catalog)
|
||||
id: vitest_tests
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: npm run test:vitest
|
||||
# The vitest config sets passWithNoTests: true, so a broken glob/alias
|
||||
# would "pass" with zero tests. Capture output and assert a non-zero
|
||||
# test count to guard against silent skips.
|
||||
run: |
|
||||
set -o pipefail
|
||||
bun run test:vitest 2>&1 | tee vitest-output.log
|
||||
# Strip ANSI color codes before matching — vitest colorizes the
|
||||
# "Tests N passed" summary, so the count is not adjacent to the
|
||||
# "Tests" label in the raw bytes.
|
||||
if ! sed -r 's/\x1b\[[0-9;]*m//g' vitest-output.log | grep -Eq 'Tests[[:space:]]+[0-9]*[1-9][0-9]* (passed|failed)'; then
|
||||
echo "ERROR: vitest reported zero tests (possible silent skip)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Unit Tests with coverage - Linux
|
||||
- name: Unit Tests (bun) - Linux
|
||||
id: unit_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
# The node-side unit suite (.mocharc spec set) now runs under `bun test`
|
||||
# via scripts/run-bun-unit-tests.ts (one isolated bun process per file).
|
||||
# The runner exits non-zero on any failure and prints a final
|
||||
# "Files: N Pass: P Fail: F" summary; assert a non-zero pass count to
|
||||
# guard against an empty glob silently "passing".
|
||||
run: |
|
||||
npx nyc --nycrc-path .nycrc.unit.json --reporter=lcov npm run test:unit
|
||||
set -o pipefail
|
||||
bun run test:unit 2>&1 | tee unit-output.log
|
||||
if ! grep -Eq 'Pass:[[:space:]]+[0-9]*[1-9][0-9]*' unit-output.log; then
|
||||
echo "ERROR: bun unit runner reported zero passing tests (possible silent skip)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Unit Tests - Non-Linux
|
||||
- name: Unit Tests (bun) - Non-Linux
|
||||
id: unit_tests_non_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }}
|
||||
run: |
|
||||
npm run test:unit
|
||||
bun run test:unit
|
||||
|
||||
- name: Extension Integration Tests - Linux
|
||||
id: integration_tests_linux
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }}
|
||||
run: xvfb-run -a npm run test:coverage
|
||||
run: xvfb-run -a bun run test:coverage
|
||||
|
||||
- name: Extension Integration Tests - Non-Linux
|
||||
id: integration_tests_non_linux
|
||||
@@ -188,7 +241,7 @@ jobs:
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
echo "Running extension integration tests (attempt ${attempt}/3)"
|
||||
if npm run test:integration; then
|
||||
if bun run test:integration; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -206,7 +259,7 @@ jobs:
|
||||
if: ${{ !cancelled() && steps.build_step.outcome == 'success' }}
|
||||
run: |
|
||||
cd webview-ui
|
||||
npm run test:coverage
|
||||
bun run test:coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -215,7 +268,6 @@ jobs:
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
apps/vscode/coverage-unit/lcov.info
|
||||
apps/vscode/webview-ui/coverage/lcov.info
|
||||
|
||||
test-platform-integration:
|
||||
@@ -229,39 +281,45 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
apps/vscode/testing-platform/package-lock.json
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install extension dependencies
|
||||
# Single root install resolves the whole bun workspace, including the
|
||||
# testing-platform package, so the separate per-package `npm ci` steps
|
||||
# (extension + webview-ui + testing-platform) collapse into one.
|
||||
- name: Install workspace dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci --include=optional
|
||||
run: bun install --frozen-lockfile
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
# @cline/* are local workspace symlinks to source packages; build dist/
|
||||
# before compiling the standalone core.
|
||||
- name: Build SDK packages
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci --include=optional
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
run: |
|
||||
NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
if [ ! -f "$NODE_FILE" ]; then
|
||||
echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE"
|
||||
echo "(bun trustedDependencies postinstall likely did not run)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found better-sqlite3 native binary: $NODE_FILE"
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
run: bun run download-ripgrep
|
||||
|
||||
- name: Compile Standalone
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/testing-platform ci --include=optional
|
||||
run: bun run compile-standalone
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
timeout-minutes: 7
|
||||
run: npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
run: bun run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -84,3 +84,4 @@ apps/vscode/webview-ui/src/**/*.js.map
|
||||
*.db-wal
|
||||
.cline/**/managed.json
|
||||
.cline/**/bundle.json
|
||||
apps/vscode/tsconfig.test.generated.json
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ fi
|
||||
|
||||
gitleaks git --pre-commit --redact --staged --verbose || exit 1
|
||||
|
||||
cd apps/vscode && lint-staged
|
||||
cd apps/vscode && bunx lint-staged
|
||||
|
||||
|
||||
+14
-14
@@ -45,7 +45,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
3. Install [bun](https://bun.com)
|
||||
4. Install the necessary dependencies for the extension and webview-gui:
|
||||
```bash
|
||||
cd apps/vscode && npm run install:all && cd ../..
|
||||
cd apps/vscode && bun run install:all && cd ../..
|
||||
cd sdk && bun run build && cd ..
|
||||
```
|
||||
5. Generate Protocol Buffer files (required before first build):
|
||||
@@ -61,8 +61,8 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
2. Push your branch and create a PR on GitHub. Our CI will:
|
||||
- Run tests and checks
|
||||
3. Testing
|
||||
- Run `cd apps/vscode && npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
- Run `cd apps/vscode && bun run test` to run tests locally.
|
||||
- Before submitting PR, run `bun run format:fix` to format your code
|
||||
|
||||
### Extension
|
||||
|
||||
@@ -74,12 +74,12 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
|
||||
2. **Local Development**
|
||||
- cd into the vscode extension, `cd apps/vscode`
|
||||
- Run `npm run install:all` to install dependencies
|
||||
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
|
||||
- Run `npm run test` to run tests locally
|
||||
- Run `bun run install:all` to install dependencies
|
||||
- Run `bun run protos` to generate Protocol Buffer files (required before first build)
|
||||
- Run `bun run test` to run tests locally
|
||||
- Run → Start Debugging or `>Debug: Select and Start Debugging` and wait for a new VS Code instance to open
|
||||
- **Terminal Workflow**: Use `npm run dev` (generates protos + runs watch mode) or `npm run watch` (if protos already generated)
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
- **Terminal Workflow**: Use `bun run dev` (generates protos + runs watch mode) or `bun run watch` (if protos already generated)
|
||||
- Before submitting PR, run `bun run format:fix` to format your code
|
||||
|
||||
3. **Linux-specific Setup**
|
||||
VS Code extension tests on Linux require the following system libraries:
|
||||
@@ -135,8 +135,8 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
|
||||
2. **Code Quality**
|
||||
|
||||
- Run `npm run lint` to check code style
|
||||
- Run `npm run format` to automatically format code
|
||||
- Run `bun run lint` to check code style
|
||||
- Run `bun run format` to automatically format code
|
||||
- All PRs must pass CI checks which include both linting and formatting
|
||||
- Address any warnings or errors from linter before submitting
|
||||
- Follow TypeScript best practices and maintain type safety
|
||||
@@ -144,7 +144,7 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
3. **Testing**
|
||||
|
||||
- Add tests for new features
|
||||
- Run `npm test` to ensure all tests pass
|
||||
- Run `bun test` to ensure all tests pass
|
||||
- Update existing tests if your changes affect them
|
||||
- Include both unit tests and integration tests where appropriate
|
||||
|
||||
@@ -154,9 +154,9 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines
|
||||
|
||||
- **Running E2E tests:**
|
||||
```bash
|
||||
npm run test:e2e # Build and run all E2E tests
|
||||
npm run e2e # Run tests without rebuilding
|
||||
npm run test:e2e -- --debug # Run with interactive debugger
|
||||
bun run test:e2e # Build and run all E2E tests
|
||||
bun run e2e # Run tests without rebuilding
|
||||
bun run test:e2e -- --debug # Run with interactive debugger
|
||||
```
|
||||
|
||||
- **Writing E2E tests:**
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"extension": [
|
||||
"ts"
|
||||
],
|
||||
"spec": [
|
||||
"src/**/__tests__/*.ts",
|
||||
"src/test/services/**/*.test.ts"
|
||||
],
|
||||
"ignore": [
|
||||
"src/core/controller/models/__tests__/providerCatalogHandlers.test.ts",
|
||||
"src/core/controller/models/__tests__/providerCatalogSmoke.test.ts",
|
||||
"src/core/controller/models/__tests__/providerSwitchNormalization.test.ts",
|
||||
"src/core/controller/models/__tests__/resolveModelInfo.test.ts",
|
||||
"src/core/controller/models/__tests__/refreshClineRecommendedModels.test.ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
"source-map-support/register",
|
||||
"./src/test/requires.ts"
|
||||
],
|
||||
"recursive": true,
|
||||
"exit": true
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"all": true,
|
||||
"check-coverage": false,
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.d.ts",
|
||||
|
||||
"**/*.{test,spec}.{js,jsx,ts,tsx,mjs,cjs}",
|
||||
"**/__tests__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/.nyc_output/**",
|
||||
"**/.vscode-test/**",
|
||||
"**/tests-results/**",
|
||||
"src/test/**",
|
||||
|
||||
"src/generated/**",
|
||||
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/build/**",
|
||||
"**/coverage/**",
|
||||
"**/coverage-unit/**",
|
||||
"**/proto/**",
|
||||
|
||||
"**/*.{config,setup}.{js,ts,mjs,cjs}",
|
||||
"**/vite-env.d.ts",
|
||||
|
||||
"**/*.{css,scss,sass,less,styl}",
|
||||
"**/*.{svg,png,jpg,jpeg,gif,ico}",
|
||||
"**/*.{json,yaml,yml}"
|
||||
],
|
||||
"extension": [
|
||||
".ts",
|
||||
".js"
|
||||
],
|
||||
"cache": true,
|
||||
"sourceMap": true,
|
||||
"instrument": true,
|
||||
"report-dir": "./coverage-unit"
|
||||
}
|
||||
@@ -7,6 +7,13 @@ export default defineConfig({
|
||||
files: [
|
||||
"out/src/{core,test,utils,shared,integrations,hosts,services}/**/*.test.js",
|
||||
"src/{core,test,utils,shared,integrations,hosts,services}/**/*.test.js",
|
||||
// The bun unit suite (src/**/__tests__/* and src/test/services/**) runs under
|
||||
// `bun test` (run-bun-unit-tests.ts) and imports `bun:test`, which this
|
||||
// Node-based runner cannot load. Exclude it here.
|
||||
"!out/src/**/__tests__/**/*.test.js",
|
||||
"!out/src/test/services/**/*.test.js",
|
||||
"!src/**/__tests__/**/*.test.js",
|
||||
"!src/test/services/**/*.test.js",
|
||||
],
|
||||
mocha: {
|
||||
ui: "bdd",
|
||||
|
||||
@@ -10,8 +10,25 @@ CLAUDE.local.md
|
||||
out/
|
||||
dist-standalone/
|
||||
node_modules/
|
||||
# Nested workspace-member node_modules (bun links these under each package).
|
||||
# Scoped to the sub-package dirs so it doesn't shadow the top-level
|
||||
# node_modules/@vscode/codicons re-include below.
|
||||
webview-ui/node_modules/**
|
||||
testing-platform/node_modules/**
|
||||
standalone/**/node_modules/**
|
||||
src/**
|
||||
standalone/**
|
||||
# Build/dev tooling and inputs — bundled into dist/extension.js, not needed in the VSIX.
|
||||
bunfig.toml
|
||||
esbuild.mjs
|
||||
knip.json
|
||||
biome.jsonc
|
||||
test-setup.js
|
||||
.env.example
|
||||
scripts/**
|
||||
proto/**
|
||||
testing-platform/**
|
||||
tests/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
esbuild.js
|
||||
@@ -46,7 +63,6 @@ eslint-rules/
|
||||
old_docs/
|
||||
evals/
|
||||
.codespellrc
|
||||
.mocharc.json
|
||||
buf.yaml
|
||||
.clinerules/
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[test]
|
||||
# Module-substitution aliases for `bun test`. bun resolves tsconfig `paths`
|
||||
# (@/*, @core/*, @shared/*, …) and the real @cline/llms + @cline/shared dist
|
||||
# builds on its own; the preload only shadows `vscode` and `@cline/core` with
|
||||
# their unit-test stubs (mirrors vitest.config.ts resolve.alias). See
|
||||
# src/test/bun-test-preload.ts for details.
|
||||
preload = ["./src/test/bun-test-preload.ts"]
|
||||
Generated
-22071
File diff suppressed because it is too large
Load Diff
+46
-56
@@ -342,65 +342,66 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"vscode:prepublish": "bun run package",
|
||||
"compile": "bun run check-types && bun run lint && bun esbuild.mjs",
|
||||
"compile-standalone": "bun run check-types && bun run lint && bun esbuild.mjs --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"dev": "npm run protos && npm run watch",
|
||||
"watch": "npx npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
"dev": "bun run protos && bun run watch",
|
||||
"watch": "bun run --parallel watch:esbuild watch:tsc",
|
||||
"watch:esbuild": "bun esbuild.mjs --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
"package": "bun run check-types && bun run build:webview && bun run lint && bun esbuild.mjs --production",
|
||||
"protos": "node scripts/build-proto.mjs",
|
||||
"protos-python": "node scripts/build-python-proto.mjs",
|
||||
"download-ripgrep": "node scripts/download-ripgrep.mjs",
|
||||
"postprotos": "biome format --config-path ./biome.jsonc src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched",
|
||||
"clean:build": "rimraf dist dist-standalone webview-ui/build src/generated out/",
|
||||
"clean:deps": "rimraf node_modules webview-ui/node_modules",
|
||||
"clean:all": "npm run clean:build && npm run clean:deps",
|
||||
"clean:all": "bun run clean:build && bun run clean:deps",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
|
||||
"lint": "biome lint --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"check-types": "bun run protos && bunx tsc --noEmit && cd webview-ui && bunx tsc --noEmit",
|
||||
"lint": "biome lint --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && bun run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"analyze:unused": "npx --yes knip --config knip.json --include files,exports,types,enumMembers,duplicates",
|
||||
"analyze:unused:prod": "npx --yes knip --config knip.json --production --include files,exports,types,enumMembers,dependencies",
|
||||
"analyze:unused": "bunx --yes knip --config knip.json --include files,exports,types,enumMembers,duplicates",
|
||||
"analyze:unused:prod": "bunx --yes knip --config knip.json --production --include files,exports,types,enumMembers,dependencies",
|
||||
"analyze:unused:fix-exports": "node scripts/remove-unused-exports.mjs --apply",
|
||||
"analyze:unused:fix-exports:dry": "node scripts/remove-unused-exports.mjs",
|
||||
"format": "biome format --config-path ./biome.jsonc --changed --since main --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error --semicolons=as-needed",
|
||||
"format:fix": "biome check --config-path ./biome.jsonc --changed --since main --no-errors-on-unmatched --files-ignore-unknown=true --write --semicolons=as-needed",
|
||||
"fix:all": "biome check --config-path ./biome.jsonc --no-errors-on-unmatched --files-ignore-unknown=true --write --diagnostic-level=error --unsafe --semicolons=as-needed",
|
||||
"ci:check-all": "npx npm-run-all -p check-types lint format",
|
||||
"ci:build": "npm run protos && npm run build:webview && node esbuild.mjs && npm run compile-tests",
|
||||
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
|
||||
"test": "npx npm-run-all test:unit test:integration",
|
||||
"test:integration": "npm run compile-tests && vscode-test",
|
||||
"test:unit": "cross-env TS_NODE_PROJECT=./tsconfig.unit-test.json mocha",
|
||||
"ci:check-all": "bun run --parallel check-types lint format",
|
||||
"ci:build": "bun run protos && bun run build:webview && bun esbuild.mjs && bun run compile-tests",
|
||||
"pretest": "bun run compile && bun run compile-tests && bun run compile-standalone && bun run lint",
|
||||
"test": "bun run test:unit && bun run test:integration",
|
||||
"test:integration": "bun run compile-tests && vscode-test",
|
||||
"test:unit": "bun scripts/run-bun-unit-tests.ts",
|
||||
"test:vitest": "vitest run --config vitest.config.ts",
|
||||
"test:vitest:watch": "vitest --config vitest.config.ts",
|
||||
"test:coverage": "npm run compile-tests && vscode-test --coverage",
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "npx tsx scripts/testing-platform-orchestrator.ts",
|
||||
"dev:mcp-oauth-test-server": "npx tsx src/dev/mcp-oauth-test-server/server.ts",
|
||||
"test:bun": "bun scripts/run-bun-tests.ts",
|
||||
"test:bun:unit": "bun scripts/run-bun-unit-tests.ts",
|
||||
"test:coverage": "bun run compile-tests && vscode-test --coverage",
|
||||
"test:sca-server": "bun --watch scripts/test-standalone-core-api-server.ts",
|
||||
"test:tp-orchestrator": "bun scripts/testing-platform-orchestrator.ts",
|
||||
"dev:mcp-oauth-test-server": "bun src/dev/mcp-oauth-test-server/server.ts",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e:build": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix",
|
||||
"test:e2e": "playwright install && npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "npx tsx scripts/interactive-playwright.ts",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
"test:e2e:build": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix",
|
||||
"test:e2e": "playwright install && bun run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "bun run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:ui": "bun scripts/interactive-playwright.ts",
|
||||
"install:all": "bun install",
|
||||
"dev:webview": "cd webview-ui && bun run dev",
|
||||
"build:webview": "cd webview-ui && bun run build",
|
||||
"test:webview": "cd webview-ui && bun run test",
|
||||
"publish:marketplace": "node scripts/publish-marketplace.mjs",
|
||||
"publish:marketplace:prerelease": "node scripts/publish-marketplace.mjs --pre-release",
|
||||
"publish:marketplace:nightly": "node ./scripts/publish-nightly.mjs",
|
||||
"prepare": "npx husky",
|
||||
"docs": "cd docs && npm run dev",
|
||||
"docs:check-links": "cd docs && npm run check",
|
||||
"docs:rename-file": "cd docs && npm run rename",
|
||||
"docs": "cd docs && bun run dev",
|
||||
"docs:check-links": "cd docs && bun run check",
|
||||
"docs:rename-file": "cd docs && bun run rename",
|
||||
"report-issue": "node scripts/report-issue.js",
|
||||
"storybook": "cd webview-ui && npm run storybook",
|
||||
"eval:smoke:run": "npx tsx evals/smoke-tests/run-smoke-tests.ts"
|
||||
"storybook": "cd webview-ui && bun run storybook",
|
||||
"eval:smoke:run": "bun evals/smoke-tests/run-smoke-tests.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/shared/storage/state-keys.ts": [
|
||||
@@ -424,7 +425,6 @@
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/picomatch": "^4.0.2",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^21.0.0",
|
||||
@@ -436,24 +436,22 @@
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "5.6.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"glob": "^11.0.0",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^17.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^11.7.4",
|
||||
"playwright": "^1.55.1",
|
||||
"prebuild-install": "^7.1.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
"proxyquire": "^2.1.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^21.0.3",
|
||||
"tar": "^7.5.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^2.6.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
@@ -462,10 +460,10 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cline/agents": "0.0.47",
|
||||
"@cline/core": "0.0.47",
|
||||
"@cline/llms": "0.0.47",
|
||||
"@cline/shared": "0.0.47",
|
||||
"@cline/agents": "workspace:*",
|
||||
"@cline/core": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@grpc/grpc-js": "^1.9.15",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
@@ -524,6 +522,7 @@
|
||||
"mammoth": "^1.11.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"nice-grpc": "^2.1.12",
|
||||
"nice-grpc-common": "^2.0.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"open": "^10.1.2",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
@@ -551,15 +550,6 @@
|
||||
"vscode-uri": "^3.1.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"overrides": {
|
||||
"tar-fs": ">=3.1.1",
|
||||
"tar": "^7.5.2",
|
||||
"vite": "^7.1.11",
|
||||
"js-yaml": "^4.1.1",
|
||||
"serialize-javascript": ">=7.0.3",
|
||||
"protobufjs": "7.5.8",
|
||||
"diff": "8.0.4"
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"lcov",
|
||||
|
||||
@@ -34,9 +34,26 @@ const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js")
|
||||
const NICE_JS_OUT_DIR = path.resolve("src/generated/nice-grpc")
|
||||
const DESCRIPTOR_OUT_DIR = path.resolve("dist-standalone/proto")
|
||||
|
||||
const TS_PROTO_PLUGIN = isWindows
|
||||
? path.resolve("node_modules/.bin/protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
|
||||
: require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
// protoc invokes the ts-proto plugin as a child process, so it needs a path it can
|
||||
// directly execute. On POSIX the package's JS bin (with its shebang) works. On
|
||||
// Windows protoc cannot exec a bare .js or bun's `.bunx` shim ("%1 is not a valid
|
||||
// Win32 application"), and the package manager's `.cmd` shim location/name varies
|
||||
// (npm vs bun's hoisted store). To be package-manager-agnostic, generate a tiny
|
||||
// .cmd wrapper that runs the resolved plugin JS via `node`.
|
||||
function resolveTsProtoPlugin() {
|
||||
const pluginJs = require.resolve("ts-proto/protoc-gen-ts_proto")
|
||||
if (!isWindows) {
|
||||
return pluginJs
|
||||
}
|
||||
const wrapperDir = path.resolve("dist-standalone")
|
||||
fsSync.mkdirSync(wrapperDir, { recursive: true })
|
||||
const wrapperPath = path.join(wrapperDir, "protoc-gen-ts_proto.cmd")
|
||||
// %* forwards protoc's plugin args/stdio to the JS entry run under node.
|
||||
fsSync.writeFileSync(wrapperPath, `@echo off\r\nnode "${pluginJs}" %*\r\n`)
|
||||
return wrapperPath
|
||||
}
|
||||
|
||||
const TS_PROTO_PLUGIN = resolveTsProtoPlugin()
|
||||
|
||||
const TS_PROTO_OPTIONS = [
|
||||
"env=both",
|
||||
|
||||
@@ -61,7 +61,37 @@ async function main() {
|
||||
fs.rmSync(path.join(__dirname, "..", "out", "src"), { recursive: true, force: true })
|
||||
fs.rmSync(path.join(__dirname, "..", "out", "packages"), { recursive: true, force: true })
|
||||
|
||||
execSync("tsc -p ./tsconfig.test.json --outDir out", { encoding: "utf-8" })
|
||||
// Single source of truth for the bun-vs-integration test split: any *.test.ts that
|
||||
// imports from "bun:test" is owned by the bun runner (scripts/run-bun-unit-tests.ts)
|
||||
// and must NOT be compiled into the Node-based @vscode/test-cli `out/` tree (Node
|
||||
// cannot load the `bun:test` builtin, and these files use bun-only APIs like
|
||||
// `mock.module` / 3-arg `it`). Generate a tsconfig that excludes them so the
|
||||
// integration compile only ever sees mocha-owned tests.
|
||||
const projectRoot = path.join(__dirname, "..")
|
||||
const bunTestImport = /from\s+["']bun:test["']/
|
||||
function collectBunTestFiles(dir, acc) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === "node_modules") continue
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
collectBunTestFiles(full, acc)
|
||||
} else if (entry.isFile() && entry.name.endsWith(".test.ts")) {
|
||||
if (bunTestImport.test(fs.readFileSync(full, "utf-8"))) {
|
||||
acc.push(path.relative(projectRoot, full).split(path.sep).join("/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
const bunOwnedTests = collectBunTestFiles(path.join(projectRoot, "src"), [])
|
||||
// tsconfig.test.json is JSONC (contains comments); parse with json5 (a project dep).
|
||||
const JSON5 = require("json5")
|
||||
const baseTestConfig = JSON5.parse(fs.readFileSync(path.join(projectRoot, "tsconfig.test.json"), "utf-8"))
|
||||
baseTestConfig.exclude = [...(baseTestConfig.exclude ?? []), ...bunOwnedTests]
|
||||
const generatedConfigPath = path.join(projectRoot, "tsconfig.test.generated.json")
|
||||
fs.writeFileSync(generatedConfigPath, JSON.stringify(baseTestConfig, null, "\t"))
|
||||
|
||||
execSync(`tsc -p ${JSON.stringify(generatedConfigPath)} --outDir out`, { encoding: "utf-8" })
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
|
||||
@@ -47,8 +47,13 @@ async function installNodeDependencies() {
|
||||
|
||||
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
|
||||
|
||||
console.log("Running npm install in distribution directory...")
|
||||
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
|
||||
// This is an ISOLATED runtime install inside the standalone distribution
|
||||
// directory (dist-standalone), driven by the "cline-core" runtime-files
|
||||
// manifest — it is NOT part of the monorepo workspace install. TARGET_NODE_VERSION
|
||||
// and the prebuild-install calls below target the Node ABI of the bundled
|
||||
// runtime (matching the JetBrains-packaged Node), not the build tooling.
|
||||
console.log("Running bun install in distribution directory...")
|
||||
execSync("bun install", { stdio: "inherit", cwd: BUILD_DIR })
|
||||
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
|
||||
@@ -92,7 +97,10 @@ async function packageAllBinaryDeps() {
|
||||
const dest = path.join(binaryDir, module)
|
||||
await cpr(src, dest)
|
||||
|
||||
// Download the binary libs
|
||||
// Download the binary libs.
|
||||
// `--target=${TARGET_NODE_VERSION}` selects the Node ABI of the bundled
|
||||
// standalone runtime (NOT the bun/build tooling) so the prebuilt native
|
||||
// `.node` binaries load in the Node that runs cline-core.
|
||||
const v = IS_VERBOSE ? "--verbose" : ""
|
||||
const cmd = `npx prebuild-install --platform=${platform} --arch=${arch} --target=${TARGET_NODE_VERSION} ${v}`
|
||||
log_verbose(`${module}: ${cmd}`)
|
||||
|
||||
@@ -37,13 +37,16 @@ process.on("SIGINT", cleanupOnSignal(130))
|
||||
process.on("SIGTERM", cleanupOnSignal(143))
|
||||
|
||||
try {
|
||||
const vsceArgs = ["publish", "--allow-package-secrets", "sendgrid"]
|
||||
// --no-dependencies: the extension is fully esbuild-bundled into dist/extension.js,
|
||||
// so vsce must not walk node_modules (the @cline/* workspace symlinks point out of
|
||||
// the package and would drag the whole monorepo into the VSIX).
|
||||
const vsceArgs = ["publish", "--no-dependencies", "--allow-package-secrets", "sendgrid"]
|
||||
if (isPrerelease) {
|
||||
vsceArgs.push("--pre-release")
|
||||
}
|
||||
execFileSync("vsce", vsceArgs, { stdio: "inherit" })
|
||||
|
||||
const ovsxArgs = ["ovsx", "publish"]
|
||||
const ovsxArgs = ["ovsx", "publish", "--no-dependencies"]
|
||||
if (isPrerelease) {
|
||||
ovsxArgs.push("--pre-release")
|
||||
}
|
||||
|
||||
@@ -388,6 +388,9 @@ class NightlyPublisher {
|
||||
const args = [
|
||||
"package",
|
||||
...(isPreRelease ? ["--pre-release"] : []),
|
||||
// The extension is fully esbuild-bundled, so vsce must not walk node_modules
|
||||
// (the @cline/* workspace symlinks point outside the package).
|
||||
"--no-dependencies",
|
||||
"--no-update-package-json",
|
||||
"--no-git-tag-version",
|
||||
"--allow-package-secrets",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawn } from "node:child_process"
|
||||
import path from "node:path"
|
||||
/**
|
||||
* Runner for the SDK-adapter + model-catalog `bun test` suites (the same set
|
||||
* `vitest.config.ts` covers; `test:vitest` runs them under vitest).
|
||||
*
|
||||
* Why a script instead of a bare `bun test <globs>`:
|
||||
*
|
||||
* 1. Curated include set. These suites are an explicit list (see
|
||||
* INCLUDE_PATTERNS, kept in sync with `vitest.config.ts` `test.include`),
|
||||
* not the whole tree, so the node-side unit and @vscode/test-cli suites are
|
||||
* not pulled in. `bun test`'s positional args don't expand `**` the way we
|
||||
* need, so we resolve the globs ourselves with bun's `Glob`.
|
||||
*
|
||||
* 2. One process per file. `bun test` runs all files in a single process by
|
||||
* default, so `mock.module(...)` registrations leak between files — suites
|
||||
* mocking the same specifier with different shapes (e.g.
|
||||
* `@/core/storage/StateManager`, `@cline/core`) clobber each other.
|
||||
* `--parallel` runs each file in its own worker process, giving each a fresh
|
||||
* module registry.
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/run-bun-tests.ts # run the curated set, isolated
|
||||
* bun scripts/run-bun-tests.ts --list # print the resolved file list only
|
||||
*/
|
||||
import { Glob } from "bun"
|
||||
|
||||
// Mirror of vitest.config.ts `test.include`. Keep these in sync.
|
||||
const INCLUDE_PATTERNS = [
|
||||
"src/sdk/**/*.test.ts",
|
||||
"src/shared/vsCodeSelectorUtils.test.ts",
|
||||
"src/core/storage/remote-config/**/*.test.ts",
|
||||
"src/shared/model-catalog/provider-helpers.test.ts",
|
||||
"src/core/controller/models/__tests__/providerCatalogHandlers.test.ts",
|
||||
"src/core/controller/models/__tests__/providerSwitchNormalization.test.ts",
|
||||
"src/core/controller/models/__tests__/resolveModelInfo.test.ts",
|
||||
"src/core/controller/models/__tests__/providerCatalogSmoke.test.ts",
|
||||
"src/core/controller/models/__tests__/refreshClineRecommendedModels.test.ts",
|
||||
]
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dir, "..")
|
||||
|
||||
async function resolveFiles(): Promise<string[]> {
|
||||
const seen = new Set<string>()
|
||||
for (const pattern of INCLUDE_PATTERNS) {
|
||||
const glob = new Glob(pattern)
|
||||
for await (const match of glob.scan({ cwd: projectRoot, onlyFiles: true })) {
|
||||
seen.add(match)
|
||||
}
|
||||
}
|
||||
return [...seen].sort()
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const files = await resolveFiles()
|
||||
if (files.length === 0) {
|
||||
console.error("run-bun-tests: no test files matched the include patterns")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const passthrough = process.argv.slice(2)
|
||||
if (passthrough.includes("--list")) {
|
||||
console.log(files.join("\n"))
|
||||
return
|
||||
}
|
||||
|
||||
const args = ["test", "--parallel", ...passthrough.filter((arg) => arg !== "--list"), ...files]
|
||||
const child = spawn("bun", args, { cwd: projectRoot, stdio: "inherit" })
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal)
|
||||
return
|
||||
}
|
||||
process.exit(code ?? 1)
|
||||
})
|
||||
}
|
||||
|
||||
void main()
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import path from "node:path"
|
||||
/**
|
||||
* Runner for the node-side `bun test` unit suites.
|
||||
*
|
||||
* A test file belongs to this runner iff it imports from "bun:test" (files that
|
||||
* need the real VS Code extension host import from "mocha" and run under
|
||||
* @vscode/test-cli instead). We glob all `*.test.ts` and keep only the
|
||||
* bun:test ones; the SDK/model-catalog suites listed in IGNORED run through
|
||||
* `run-bun-tests.ts`, so they're skipped here to avoid double-running.
|
||||
*
|
||||
* Why one process per file: `bun test --parallel <allFiles>` reuses a pool of
|
||||
* worker processes, and `mock.module(...)` registrations accumulate across files
|
||||
* sharing a worker. Suites that mock the same specifier with different shapes
|
||||
* (e.g. `@core/storage/disk`, `@cline/core`, `fs/promises`, `os`) then clobber
|
||||
* each other and fail only at scale. Spawning one `bun test` process per file
|
||||
* (bounded by a small concurrency pool, via `Bun.spawn`) gives each file a fresh
|
||||
* module registry.
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/run-bun-unit-tests.ts # run the suite, isolated
|
||||
* bun scripts/run-bun-unit-tests.ts --list # print the resolved file list
|
||||
* bun scripts/run-bun-unit-tests.ts --all # include ELECTRON_HOST_ONLY
|
||||
* bun scripts/run-bun-unit-tests.ts -c 6 # concurrency (default 4)
|
||||
*/
|
||||
import { Glob } from "bun"
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dir, "..")
|
||||
|
||||
// A file runs under `bun test` iff it imports "bun:test"; mocha-owned files are
|
||||
// skipped by the import filter in resolveFiles().
|
||||
const INCLUDE_PATTERNS = ["src/**/*.test.ts"]
|
||||
const BUN_TEST_IMPORT = /from\s+["']bun:test["']/
|
||||
|
||||
// SDK + model-catalog suites run through `run-bun-tests.ts`; skip them here so
|
||||
// they aren't run twice.
|
||||
const IGNORED = new Set<string>([
|
||||
"src/core/controller/models/__tests__/providerCatalogHandlers.test.ts",
|
||||
"src/core/controller/models/__tests__/providerCatalogSmoke.test.ts",
|
||||
"src/core/controller/models/__tests__/providerSwitchNormalization.test.ts",
|
||||
"src/core/controller/models/__tests__/resolveModelInfo.test.ts",
|
||||
"src/core/controller/models/__tests__/refreshClineRecommendedModels.test.ts",
|
||||
])
|
||||
|
||||
// Files that require the real VSCode Electron host (@vscode/test-cli). Excluded
|
||||
// by default; they continue to run under @vscode/test-cli.
|
||||
const ELECTRON_HOST_ONLY = new Set<string>([])
|
||||
|
||||
async function resolveFiles(includeHostOnly: boolean): Promise<string[]> {
|
||||
const seen = new Set<string>()
|
||||
for (const pattern of INCLUDE_PATTERNS) {
|
||||
const glob = new Glob(pattern)
|
||||
for await (const match of glob.scan({ cwd: projectRoot, onlyFiles: true })) {
|
||||
const normalized = match.split(path.sep).join("/")
|
||||
if (IGNORED.has(normalized)) {
|
||||
continue
|
||||
}
|
||||
if (!includeHostOnly && ELECTRON_HOST_ONLY.has(normalized)) {
|
||||
continue
|
||||
}
|
||||
// Only bun-runner-owned files (those importing "bun:test"). Files still on
|
||||
// the @vscode/test-cli Electron host import from "mocha" and are skipped.
|
||||
const source = await Bun.file(path.join(projectRoot, normalized)).text()
|
||||
if (!BUN_TEST_IMPORT.test(source)) {
|
||||
continue
|
||||
}
|
||||
seen.add(normalized)
|
||||
}
|
||||
}
|
||||
return [...seen].sort()
|
||||
}
|
||||
|
||||
type FileResult = {
|
||||
file: string
|
||||
code: number
|
||||
pass: number
|
||||
fail: number
|
||||
output: string
|
||||
}
|
||||
|
||||
// `bun test` prints its summary as e.g. " 12 pass\n 0 fail".
|
||||
function parseCounts(output: string): { pass: number; fail: number } {
|
||||
let pass = 0
|
||||
let fail = 0
|
||||
for (const m of output.matchAll(/^\s*(\d+)\s+pass\b/gm)) {
|
||||
pass += Number(m[1])
|
||||
}
|
||||
for (const m of output.matchAll(/^\s*(\d+)\s+fail\b/gm)) {
|
||||
fail += Number(m[1])
|
||||
}
|
||||
return { pass, fail }
|
||||
}
|
||||
|
||||
const PER_FILE_TIMEOUT_MS = 120_000
|
||||
|
||||
async function runOne(file: string): Promise<FileResult> {
|
||||
const proc = Bun.spawn(["bun", "test", file], {
|
||||
cwd: projectRoot,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: { ...process.env, FORCE_COLOR: "0" },
|
||||
})
|
||||
// Guard against a single hung file stalling the whole pool: kill it after a
|
||||
// generous per-file budget and surface it as a failure.
|
||||
let timedOut = false
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true
|
||||
proc.kill()
|
||||
}, PER_FILE_TIMEOUT_MS)
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
clearTimeout(timer)
|
||||
const output = stdout + stderr + (timedOut ? `\n[runner] TIMEOUT after ${PER_FILE_TIMEOUT_MS}ms — killed\n` : "")
|
||||
const { pass, fail } = parseCounts(output)
|
||||
// A nonzero exit with no parsed counts (load/parse error, timeout) must count
|
||||
// as a failure so the gate cannot pass silently.
|
||||
const effectiveFail = timedOut && fail === 0 ? Math.max(fail, 1) : fail
|
||||
return { file, code, pass, fail: effectiveFail, output }
|
||||
}
|
||||
|
||||
async function runPool(files: string[], concurrency: number): Promise<FileResult[]> {
|
||||
const results: FileResult[] = []
|
||||
let next = 0
|
||||
const launch = async (): Promise<void> => {
|
||||
while (next < files.length) {
|
||||
const file = files[next++]
|
||||
const result = await runOne(file)
|
||||
results.push(result)
|
||||
const failed = result.fail > 0 || result.code !== 0
|
||||
const status = failed ? "FAIL" : "ok"
|
||||
const counts = `${result.pass} pass / ${result.fail} fail`
|
||||
process.stdout.write(`[${results.length}/${files.length}] ${status.padEnd(4)} ${counts.padEnd(20)} ${file}\n`)
|
||||
if (failed) {
|
||||
process.stdout.write(result.output.trimEnd() + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
const workers: Promise<void>[] = []
|
||||
for (let i = 0; i < Math.min(concurrency, files.length); i++) {
|
||||
workers.push(launch())
|
||||
}
|
||||
await Promise.all(workers)
|
||||
return results
|
||||
}
|
||||
|
||||
function parseConcurrency(argv: string[]): number {
|
||||
const flagIdx = argv.findIndex((a) => a === "-c" || a === "--concurrency")
|
||||
if (flagIdx !== -1 && argv[flagIdx + 1]) {
|
||||
const n = Number(argv[flagIdx + 1])
|
||||
if (Number.isFinite(n) && n > 0) {
|
||||
return Math.floor(n)
|
||||
}
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const passthrough = process.argv.slice(2)
|
||||
const includeHostOnly = passthrough.includes("--all")
|
||||
const files = await resolveFiles(includeHostOnly)
|
||||
if (files.length === 0) {
|
||||
console.error("run-bun-unit-tests: no test files matched")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (passthrough.includes("--list")) {
|
||||
console.log(files.join("\n"))
|
||||
return
|
||||
}
|
||||
|
||||
const concurrency = parseConcurrency(passthrough)
|
||||
const started = Date.now()
|
||||
console.log(`Running ${files.length} unit test files, isolated (concurrency ${concurrency})…\n`)
|
||||
|
||||
const results = await runPool(files, concurrency)
|
||||
|
||||
const totalPass = results.reduce((sum, r) => sum + r.pass, 0)
|
||||
const totalFail = results.reduce((sum, r) => sum + r.fail, 0)
|
||||
const failedFiles = results.filter((r) => r.fail > 0 || r.code !== 0).sort((a, b) => a.file.localeCompare(b.file))
|
||||
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
||||
|
||||
console.log("\n──────────────────────────────────────────────")
|
||||
console.log(`Files: ${results.length} Pass: ${totalPass} Fail: ${totalFail} Time: ${elapsed}s`)
|
||||
if (failedFiles.length > 0) {
|
||||
console.log(`\nFailing files (${failedFiles.length}):`)
|
||||
for (const r of failedFiles) {
|
||||
console.log(` ${r.file} (${r.pass} pass / ${r.fail} fail, exit ${r.code})`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
console.log("All unit test files passed.")
|
||||
}
|
||||
|
||||
void main()
|
||||
@@ -126,7 +126,7 @@ function stopServer(server: ChildProcess): Promise<void> {
|
||||
|
||||
function runTestingPlatform(specFile: string, grpcPort: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const testProcess = spawn("npx", ["ts-node", "index.ts", specFile, ...(fix ? ["--fix"] : [])], {
|
||||
const testProcess = spawn("bun", ["index.ts", specFile, ...(fix ? ["--fix"] : [])], {
|
||||
cwd: path.join(process.cwd(), "testing-platform"),
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import * as actualOs from "os"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
|
||||
// The SUT does `import * as os from "os"; os.homedir()`. Under bun, sinon's
|
||||
// `stub(os, "homedir")` on the test's own `os` binding does NOT propagate to the
|
||||
// SUT's namespace import, so inject a module-level homedir stub via mock.module
|
||||
// (the rest of `os` — tmpdir() etc. — keeps its real behavior).
|
||||
const homedirStub = sinon.stub()
|
||||
const osMockNamespace = { ...actualOs, homedir: homedirStub }
|
||||
const osMock = () => ({ ...osMockNamespace, default: osMockNamespace })
|
||||
mock.module("os", osMock)
|
||||
mock.module("node:os", osMock)
|
||||
|
||||
import os from "os"
|
||||
import { ClineConfigurationError, ClineEndpoint, ClineEnv, Environment } from "../config"
|
||||
|
||||
describe("ClineEndpoint configuration", () => {
|
||||
@@ -19,9 +31,10 @@ describe("ClineEndpoint configuration", () => {
|
||||
// Create .cline directory
|
||||
await fs.mkdir(path.join(tempDir, ".cline"), { recursive: true })
|
||||
|
||||
// Stub os.homedir to return our temp directory
|
||||
// Stub os.homedir to return our temp directory (via mock.module homedirStub)
|
||||
originalHomedir = os.homedir
|
||||
sandbox.stub(os, "homedir").returns(tempDir)
|
||||
homedirStub.reset()
|
||||
homedirStub.returns(tempDir)
|
||||
|
||||
// Reset the singleton state using internal method
|
||||
;(ClineEndpoint as any)._instance = null
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import * as actualDiskModule from "@core/storage/disk"
|
||||
import { expect } from "chai"
|
||||
import chokidar from "chokidar"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as actualChokidar from "chokidar"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
|
||||
import type { FileMetadataEntry, TaskMetadata } from "./ContextTrackerTypes"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@core/storage/disk` and
|
||||
// `chokidar` namespace exports ("ES Modules cannot be stubbed"). Inject
|
||||
// module-level sinon stubs via mock.module so the full sinon stub API keeps
|
||||
// working. (`vscode` is the writable unit-test stub, still sinon-stubbed below.)
|
||||
const getTaskMetadataStub: sinon.SinonStub = sinon.stub()
|
||||
const saveTaskMetadataStub: sinon.SinonStub = sinon.stub()
|
||||
const chokidarWatchStub: sinon.SinonStub = sinon.stub()
|
||||
const diskMock = () => ({
|
||||
...actualDiskModule,
|
||||
getTaskMetadata: getTaskMetadataStub,
|
||||
saveTaskMetadata: saveTaskMetadataStub,
|
||||
})
|
||||
const chokidarNamespace = { ...actualChokidar, watch: chokidarWatchStub }
|
||||
const chokidarMock = () => ({ ...chokidarNamespace, default: chokidarNamespace })
|
||||
mock.module("@core/storage/disk", diskMock)
|
||||
mock.module("@/core/storage/disk", diskMock)
|
||||
mock.module("chokidar", chokidarMock)
|
||||
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
@@ -17,11 +36,8 @@ describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let _mockWorkspace: sinon.SinonStub
|
||||
let mockFileSystemWatcher: any
|
||||
let chokidarWatchStub: sinon.SinonStub
|
||||
let tracker: FileContextTracker
|
||||
let mockTaskMetadata: TaskMetadata
|
||||
let getTaskMetadataStub: sinon.SinonStub
|
||||
let saveTaskMetadataStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
@@ -43,13 +59,16 @@ describe("FileContextTracker", () => {
|
||||
// Return the watcher itself for chaining
|
||||
mockFileSystemWatcher.on.returns(mockFileSystemWatcher)
|
||||
|
||||
// Stub chokidar.watch to return our mock watcher
|
||||
chokidarWatchStub = sandbox.stub(chokidar, "watch").returns(mockFileSystemWatcher as any)
|
||||
// Reset the module-level chokidar.watch stub to return our mock watcher
|
||||
chokidarWatchStub.reset()
|
||||
chokidarWatchStub.returns(mockFileSystemWatcher as any)
|
||||
|
||||
// Mock disk module functions
|
||||
// Reset the module-level disk stubs
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [], environment_history: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
getTaskMetadataStub.reset()
|
||||
getTaskMetadataStub.resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub.reset()
|
||||
saveTaskMetadataStub.resolves()
|
||||
|
||||
setVscodeHostProviderMock()
|
||||
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { parseYamlFrontmatter } from "../frontmatter"
|
||||
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { evaluateRuleConditionals, extractPathLikeStrings } from "../rule-conditionals"
|
||||
|
||||
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
|
||||
+69
-13
@@ -3,15 +3,57 @@
|
||||
* Tests skill discovery, override resolution, toggle filtering, and content loading
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import * as fs from "fs"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as actualFsPromises from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
|
||||
import * as skillDirectories from "@/core/storage/skill-directories"
|
||||
import * as actualSkillDirectories from "@/core/storage/skill-directories"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import * as fsUtils from "@/utils/fs"
|
||||
import * as actualFsUtils from "@/utils/fs"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@utils/fs`,
|
||||
// `@core/storage/skill-directories`, or the `fs/promises` namespace exports
|
||||
// ("ES Modules cannot be stubbed"). Crucially, under bun `fs.promises` and the
|
||||
// `fs/promises` module are NOT the same object, so stubbing `fs.promises.X`
|
||||
// (which worked under mocha/ts-node) does not affect the SUT's
|
||||
// `import * as fs from "fs/promises"` bindings. Inject module-level sinon stubs
|
||||
// via bun's mock.module so the full sinon stub API (.withArgs/.resolves/etc.)
|
||||
// keeps working through the exact specifiers the SUT imports.
|
||||
const fileExistsAtPathStub = sinon.stub()
|
||||
const isDirectoryStub_ = sinon.stub()
|
||||
const getSkillsDirectoriesForScanStub = sinon.stub()
|
||||
const readdirStub_ = sinon.stub()
|
||||
const statStub_ = sinon.stub()
|
||||
const readFileStub_ = sinon.stub()
|
||||
const writeFileStub_ = sinon.stub()
|
||||
const fsUtilsMock = () => ({
|
||||
...actualFsUtils,
|
||||
fileExistsAtPath: fileExistsAtPathStub,
|
||||
isDirectory: isDirectoryStub_,
|
||||
})
|
||||
const skillDirsMock = () => ({
|
||||
...actualSkillDirectories,
|
||||
getSkillsDirectoriesForScan: getSkillsDirectoriesForScanStub,
|
||||
})
|
||||
const fsPromisesMockNamespace = {
|
||||
...actualFsPromises,
|
||||
readdir: readdirStub_,
|
||||
stat: statStub_,
|
||||
readFile: readFileStub_,
|
||||
writeFile: writeFileStub_,
|
||||
}
|
||||
const fsPromisesMock = () => ({ ...fsPromisesMockNamespace, default: fsPromisesMockNamespace })
|
||||
// The SUT imports via the `@utils/*` and `@core/*` tsconfig path aliases;
|
||||
// register both the `@/`-prefixed and bare-alias forms to be safe.
|
||||
mock.module("@utils/fs", fsUtilsMock)
|
||||
mock.module("@/utils/fs", fsUtilsMock)
|
||||
mock.module("@core/storage/skill-directories", skillDirsMock)
|
||||
mock.module("@/core/storage/skill-directories", skillDirsMock)
|
||||
mock.module("fs/promises", fsPromisesMock)
|
||||
mock.module("node:fs/promises", fsPromisesMock)
|
||||
|
||||
import { parseYamlFrontmatter } from "../frontmatter"
|
||||
import {
|
||||
discoverSkills,
|
||||
@@ -40,13 +82,22 @@ describe("Skills Utility Functions", () => {
|
||||
// Stub Logger.warn to avoid noise in test output
|
||||
sandbox.stub(Logger, "warn")
|
||||
|
||||
// Stub filesystem utilities
|
||||
fileExistsStub = sandbox.stub(fsUtils, "fileExistsAtPath")
|
||||
isDirectoryStub = sandbox.stub(fsUtils, "isDirectory")
|
||||
readdirStub = sandbox.stub(fs.promises, "readdir")
|
||||
statStub = sandbox.stub(fs.promises, "stat")
|
||||
readFileStub = sandbox.stub(fs.promises, "readFile")
|
||||
sandbox.stub(skillDirectories, "getSkillsDirectoriesForScan").returns([
|
||||
// Reset the module-level sinon stubs (injected via mock.module above) and
|
||||
// re-point the per-test handles at them.
|
||||
fileExistsAtPathStub.reset()
|
||||
isDirectoryStub_.reset()
|
||||
getSkillsDirectoriesForScanStub.reset()
|
||||
readdirStub_.reset()
|
||||
statStub_.reset()
|
||||
readFileStub_.reset()
|
||||
writeFileStub_.reset()
|
||||
fileExistsStub = fileExistsAtPathStub
|
||||
isDirectoryStub = isDirectoryStub_
|
||||
readdirStub = readdirStub_
|
||||
statStub = statStub_
|
||||
readFileStub = readFileStub_
|
||||
|
||||
getSkillsDirectoriesForScanStub.returns([
|
||||
{ path: path.join(TEST_CWD, ".clinerules", "skills"), source: "project" },
|
||||
{ path: path.join(TEST_CWD, ".cline", "skills"), source: "project" },
|
||||
{ path: path.join(TEST_CWD, ".claude", "skills"), source: "project" },
|
||||
@@ -743,8 +794,13 @@ describe("setSkillDisabledInFrontmatter", () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
sandbox.stub(Logger, "warn")
|
||||
readFileStub = sandbox.stub(fs.promises, "readFile")
|
||||
writeFileStub = sandbox.stub(fs.promises, "writeFile").resolves()
|
||||
// Use the module-level fs/promises stubs (mock.module above) since under
|
||||
// bun fs.promises !== the `fs/promises` module the SUT imports.
|
||||
readFileStub_.reset()
|
||||
writeFileStub_.reset()
|
||||
readFileStub = readFileStub_
|
||||
writeFileStub = writeFileStub_
|
||||
writeFileStub.resolves()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { Controller } from "@core/controller"
|
||||
import { BooleanResponse, StringRequest } from "@shared/proto/cline/common"
|
||||
import * as pathUtils from "@utils/path"
|
||||
import * as actualPathUtils from "@utils/path"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as sinon from "sinon"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@utils/path` namespace export
|
||||
// ("ES Modules cannot be stubbed"). Inject a module-level sinon stub via
|
||||
// mock.module so the full sinon stub API keeps working.
|
||||
const getWorkspacePathStub: sinon.SinonStub = sinon.stub()
|
||||
const pathUtilsMock = () => ({ ...actualPathUtils, getWorkspacePath: getWorkspacePathStub })
|
||||
mock.module("@utils/path", pathUtilsMock)
|
||||
mock.module("@/utils/path", pathUtilsMock)
|
||||
|
||||
import { ifFileExistsRelativePath } from "../ifFileExistsRelativePath"
|
||||
|
||||
describe("ifFileExistsRelativePath", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: Controller
|
||||
let getWorkspacePathStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create a mock controller
|
||||
mockController = {} as any
|
||||
|
||||
// Stub getWorkspacePath utility
|
||||
getWorkspacePathStub = sandbox.stub(pathUtils, "getWorkspacePath")
|
||||
// Reset the module-level getWorkspacePath stub
|
||||
getWorkspacePathStub.reset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
getWorkspacePathStub.reset()
|
||||
})
|
||||
|
||||
it("should return BooleanResponse with boolean value", async () => {
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { Controller } from "@core/controller"
|
||||
import * as openFileIntegration from "@integrations/misc/open-file"
|
||||
import * as actualOpenFileIntegration from "@integrations/misc/open-file"
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import * as pathUtils from "@utils/path"
|
||||
import * as actualPathUtils from "@utils/path"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@integrations/misc/open-file`
|
||||
// and `@utils/path` namespace exports ("ES Modules cannot be stubbed"). Inject
|
||||
// module-level sinon stubs via mock.module so the full sinon stub API keeps
|
||||
// working. (`Logger` is a class with static methods and is still sinon-stubbed
|
||||
// directly below.)
|
||||
const openFileIntegrationStub: sinon.SinonStub = sinon.stub()
|
||||
const getWorkspacePathStub: sinon.SinonStub = sinon.stub()
|
||||
const openFileMock = () => ({ ...actualOpenFileIntegration, openFile: openFileIntegrationStub })
|
||||
const pathUtilsMock = () => ({ ...actualPathUtils, getWorkspacePath: getWorkspacePathStub })
|
||||
mock.module("@integrations/misc/open-file", openFileMock)
|
||||
mock.module("@utils/path", pathUtilsMock)
|
||||
mock.module("@/utils/path", pathUtilsMock)
|
||||
|
||||
import { openFileRelativePath } from "../openFileRelativePath"
|
||||
|
||||
describe("openFileRelativePath", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockController: Controller
|
||||
let openFileIntegrationStub: sinon.SinonStub
|
||||
let getWorkspacePathStub: sinon.SinonStub
|
||||
let consoleErrorStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -22,11 +34,9 @@ describe("openFileRelativePath", () => {
|
||||
// Create a mock controller
|
||||
mockController = {} as any
|
||||
|
||||
// Stub the openFileIntegration function
|
||||
openFileIntegrationStub = sandbox.stub(openFileIntegration, "openFile")
|
||||
|
||||
// Stub getWorkspacePath utility
|
||||
getWorkspacePathStub = sandbox.stub(pathUtils, "getWorkspacePath")
|
||||
// Reset the module-level sinon stubs (injected via mock.module above)
|
||||
openFileIntegrationStub.reset()
|
||||
getWorkspacePathStub.reset()
|
||||
|
||||
// Stub console.error to prevent test output pollution
|
||||
consoleErrorStub = sandbox.stub(Logger, "error")
|
||||
@@ -34,6 +44,8 @@ describe("openFileRelativePath", () => {
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
openFileIntegrationStub.reset()
|
||||
getWorkspacePathStub.reset()
|
||||
})
|
||||
|
||||
it("should return Empty response on successful execution", async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { Controller } from "@core/controller"
|
||||
import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
|
||||
import { GrpcCancel, GrpcRequest } from "@shared/WebviewMessage"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as sinon from "sinon"
|
||||
import { getRequestRegistry, handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { GrpcRecorderNoops } from "@/core/controller/grpc-recorder/grpc-recorder"
|
||||
import { GrpcRecorderBuilder } from "@/core/controller/grpc-recorder/grpc-recorder.builder"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { beforeAll, describe, it } from "bun:test"
|
||||
import { GrpcRecorder, IRecorder } from "@core/controller/grpc-recorder/grpc-recorder"
|
||||
import { expect } from "chai"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
@@ -6,7 +7,7 @@ import { GrpcRequest } from "@/shared/WebviewMessage"
|
||||
describe("grpc-recorder", () => {
|
||||
let recorder: IRecorder
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
recorder = GrpcRecorder.builder()
|
||||
.withFilters((req: GrpcRequest) => req.service === "the-unwanted-service")
|
||||
.enableIf(true)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { beforeAll, describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { before, describe, it } from "mocha"
|
||||
import { LogFileHandler } from "@/core/controller/grpc-recorder/log-file-handler"
|
||||
|
||||
describe("log-file-handler", () => {
|
||||
let logHandler: LogFileHandler
|
||||
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
logHandler = new LogFileHandler()
|
||||
expect(logHandler.getFilePath()).not.empty
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { Controller } from "@core/controller"
|
||||
import { IRecorder } from "@core/controller/grpc-recorder/grpc-recorder"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
@@ -19,9 +19,8 @@ describe("Hook System", () => {
|
||||
await writeHookScriptForPlatform(hookPath, nodeScript)
|
||||
}
|
||||
|
||||
beforeEach(async function () {
|
||||
beforeEach(async () => {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_TEST_TIMEOUT_MS)
|
||||
}
|
||||
setDistinctId("test-id")
|
||||
hookTestEnv = await createHookTestEnv()
|
||||
@@ -52,14 +51,15 @@ describe("Hook System", () => {
|
||||
})
|
||||
|
||||
describe("StdioHookRunner", () => {
|
||||
it("should execute workspace hook from its respective workspace root directory", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
it(
|
||||
"should execute workspace hook from its respective workspace root directory",
|
||||
async () => {
|
||||
if (process.platform === "win32") {
|
||||
}
|
||||
|
||||
// Create a test hook script that outputs the current working directory
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
// Create a test hook script that outputs the current working directory
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "PreToolUse")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = require('fs').readFileSync(0, 'utf-8');
|
||||
// Output the current working directory
|
||||
console.log(JSON.stringify({
|
||||
@@ -67,28 +67,30 @@ console.log(JSON.stringify({
|
||||
contextModification: "CWD: " + process.cwd()
|
||||
}))`
|
||||
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
|
||||
// Test execution
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("PreToolUse")
|
||||
// Test execution
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("PreToolUse")
|
||||
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
preToolUse: {
|
||||
toolName: "test_tool",
|
||||
parameters: {},
|
||||
},
|
||||
})
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
preToolUse: {
|
||||
toolName: "test_tool",
|
||||
parameters: {},
|
||||
},
|
||||
})
|
||||
|
||||
result.cancel.should.be.false()
|
||||
// The hook should execute from its workspace root (tempDir)
|
||||
// Use fs.realpath to normalize paths (handles macOS /private prefix)
|
||||
const cwdFromHook = result.contextModification?.replace("CWD: ", "")
|
||||
const normalizedCwd = await fs.realpath(cwdFromHook)
|
||||
const normalizedTempDir = await fs.realpath(tempDir)
|
||||
normalizedCwd.should.equal(normalizedTempDir)
|
||||
})
|
||||
result.cancel.should.be.false()
|
||||
// The hook should execute from its workspace root (tempDir)
|
||||
// Use fs.realpath to normalize paths (handles macOS /private prefix)
|
||||
const cwdFromHook = result.contextModification?.replace("CWD: ", "")
|
||||
const normalizedCwd = await fs.realpath(cwdFromHook)
|
||||
const normalizedTempDir = await fs.realpath(tempDir)
|
||||
normalizedCwd.should.equal(normalizedTempDir)
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should execute hook script and parse output", async () => {
|
||||
// Create a test hook script
|
||||
@@ -617,42 +619,45 @@ console.log(JSON.stringify({
|
||||
result.errorMessage?.should.match(/Workspace error/)
|
||||
})
|
||||
|
||||
it("should execute global hook from primary workspace root directory", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
it(
|
||||
"should execute global hook from primary workspace root directory",
|
||||
async () => {
|
||||
if (process.platform === "win32") {
|
||||
}
|
||||
|
||||
// Create a global hook script that outputs the current working directory
|
||||
const globalHookPath = path.join(globalHooksDir, "PreToolUse")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
// Create a global hook script that outputs the current working directory
|
||||
const globalHookPath = path.join(globalHooksDir, "PreToolUse")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
const input = require('fs').readFileSync(0, 'utf-8');
|
||||
// Output the current working directory
|
||||
console.log(JSON.stringify({
|
||||
cancel: false,
|
||||
contextModification: "CWD: " + process.cwd()
|
||||
}))`
|
||||
await writeHookScript(globalHookPath, globalHookScript)
|
||||
await writeHookScript(globalHookPath, globalHookScript)
|
||||
|
||||
// Test execution
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("PreToolUse")
|
||||
// Test execution
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("PreToolUse")
|
||||
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
preToolUse: {
|
||||
toolName: "test_tool",
|
||||
parameters: {},
|
||||
},
|
||||
})
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
preToolUse: {
|
||||
toolName: "test_tool",
|
||||
parameters: {},
|
||||
},
|
||||
})
|
||||
|
||||
result.cancel.should.be.false()
|
||||
// Global hooks should execute from the primary workspace root (tempDir)
|
||||
// Use fs.realpath to normalize paths (handles macOS /private prefix)
|
||||
const cwdFromHook = result.contextModification?.replace("CWD: ", "")
|
||||
const normalizedCwd = await fs.realpath(cwdFromHook)
|
||||
const normalizedTempDir = await fs.realpath(tempDir)
|
||||
normalizedCwd.should.equal(normalizedTempDir)
|
||||
})
|
||||
result.cancel.should.be.false()
|
||||
// Global hooks should execute from the primary workspace root (tempDir)
|
||||
// Use fs.realpath to normalize paths (handles macOS /private prefix)
|
||||
const cwdFromHook = result.contextModification?.replace("CWD: ", "")
|
||||
const normalizedCwd = await fs.realpath(cwdFromHook)
|
||||
const normalizedTempDir = await fs.realpath(tempDir)
|
||||
normalizedCwd.should.equal(normalizedTempDir)
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should work with global PostToolUse hooks", async () => {
|
||||
// Create global PostToolUse hook
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { getHookLaunchConfig, resetHookLaunchConfigCacheForTesting } from "../HookProcess"
|
||||
import { withPlatform } from "./test-utils"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { getHooksEnabledSafe } from "../hooks-utils"
|
||||
import { withPlatform } from "./test-utils"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { afterEach, beforeEach } from "bun:test"
|
||||
import * as fs from "fs/promises"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { afterAll, beforeAll, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { escapeShellPath } from "../shell-escape"
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("Shell Path Escaping", () => {
|
||||
}
|
||||
|
||||
// Restore platform after tests
|
||||
after(() => {
|
||||
afterAll(() => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
@@ -24,7 +24,7 @@ describe("Shell Path Escaping", () => {
|
||||
})
|
||||
|
||||
describe("Unix/Linux/macOS path escaping", () => {
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
setPlatform("darwin") // macOS, but same escaping as Linux
|
||||
})
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("Shell Path Escaping", () => {
|
||||
})
|
||||
|
||||
describe("Windows path escaping", () => {
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
setPlatform("win32")
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
@@ -111,13 +111,14 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Time-Based Calculations", () => {
|
||||
it("should correctly calculate minutes ago for recent resumes", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
it(
|
||||
"should correctly calculate minutes ago for recent resumes",
|
||||
async () => {
|
||||
if (process.platform === "win32") {
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const lastTs = parseInt(input.taskResume.previousState.lastMessageTs);
|
||||
const now = Date.now();
|
||||
@@ -127,43 +128,46 @@ console.log(JSON.stringify({
|
||||
contextModification: "Minutes ago: " + minutesAgo
|
||||
}))`
|
||||
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskResume")
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskResume")
|
||||
|
||||
// Test various time intervals
|
||||
const testCases = [
|
||||
{ offset: 2 * 60 * 1000, expected: 2 }, // 2 minutes
|
||||
{ offset: 30 * 60 * 1000, expected: 30 }, // 30 minutes
|
||||
{ offset: 90 * 60 * 1000, expected: 90 }, // 90 minutes
|
||||
]
|
||||
// Test various time intervals
|
||||
const testCases = [
|
||||
{ offset: 2 * 60 * 1000, expected: 2 }, // 2 minutes
|
||||
{ offset: 30 * 60 * 1000, expected: 30 }, // 30 minutes
|
||||
{ offset: 90 * 60 * 1000, expected: 90 }, // 90 minutes
|
||||
]
|
||||
|
||||
for (const { offset, expected } of testCases) {
|
||||
const timestamp = Date.now() - offset
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: timestamp.toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
for (const { offset, expected } of testCases) {
|
||||
const timestamp = Date.now() - offset
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: timestamp.toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
result.contextModification?.should.equal(`Minutes ago: ${expected}`)
|
||||
}
|
||||
})
|
||||
result.contextModification?.should.equal(`Minutes ago: ${expected}`)
|
||||
}
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should handle very old timestamps (days ago)", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
it(
|
||||
"should handle very old timestamps (days ago)",
|
||||
async () => {
|
||||
if (process.platform === "win32") {
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const lastTs = parseInt(input.taskResume.previousState.lastMessageTs);
|
||||
const now = Date.now();
|
||||
@@ -173,27 +177,29 @@ console.log(JSON.stringify({
|
||||
contextModification: daysAgo > 0 ? "Days ago: " + daysAgo : "Recent"
|
||||
}))`
|
||||
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskResume")
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskResume")
|
||||
|
||||
// Test 7 days ago
|
||||
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: sevenDaysAgo.toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
// Test 7 days ago
|
||||
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: sevenDaysAgo.toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
result.contextModification?.should.equal("Days ago: 7")
|
||||
})
|
||||
result.contextModification?.should.equal("Days ago: 7")
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should handle edge case: future timestamp", async () => {
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
@@ -230,13 +236,14 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Message Count Analysis", () => {
|
||||
it("should analyze message count thresholds", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
it(
|
||||
"should analyze message count thresholds",
|
||||
async () => {
|
||||
if (process.platform === "win32") {
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
const count = parseInt(input.taskResume.previousState.messageCount);
|
||||
let category;
|
||||
@@ -248,33 +255,35 @@ console.log(JSON.stringify({
|
||||
contextModification: "Conversation length: " + category + " (" + count + " messages)"
|
||||
}))`
|
||||
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
await writeHookScript(hookPath, hookScript)
|
||||
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskResume")
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskResume")
|
||||
|
||||
const testCases = [
|
||||
{ count: "2", expected: "short (2 messages)" },
|
||||
{ count: "10", expected: "medium (10 messages)" },
|
||||
{ count: "50", expected: "long (50 messages)" },
|
||||
]
|
||||
const testCases = [
|
||||
{ count: "2", expected: "short (2 messages)" },
|
||||
{ count: "10", expected: "medium (10 messages)" },
|
||||
{ count: "50", expected: "long (50 messages)" },
|
||||
]
|
||||
|
||||
for (const { count, expected } of testCases) {
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: count,
|
||||
conversationHistoryDeleted: "false",
|
||||
for (const { count, expected } of testCases) {
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: count,
|
||||
conversationHistoryDeleted: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
result.contextModification?.should.equal(`Conversation length: ${expected}`)
|
||||
}
|
||||
})
|
||||
result.contextModification?.should.equal(`Conversation length: ${expected}`)
|
||||
}
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should handle zero message count", async () => {
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskResume")
|
||||
@@ -552,94 +561,101 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Fixture-Based Tests", () => {
|
||||
it("should validate representative fixtures end-to-end", async function () {
|
||||
// Multiple fixture scenarios spawn child processes sequentially,
|
||||
// which can easily exceed the default 2 s Mocha timeout.
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
|
||||
const scenarios: FixtureScenario[] = [
|
||||
{
|
||||
fixtureName: "success",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("TaskResume hook executed successfully")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "recent-resume",
|
||||
lastMessageTs: (Date.now() - 2 * 60 * 1000).toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.match(/Recently paused task/)
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "long-pause",
|
||||
lastMessageTs: (Date.now() - 48 * 60 * 60 * 1000).toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.match(/paused 48 hours ago/)
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "context-deleted",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "50",
|
||||
conversationHistoryDeleted: "true",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.match(/truncated/)
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "message-count",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "25",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("TASK_CONTEXT: Resuming task with 25 previous messages")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "context-injection",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal(
|
||||
"WORKSPACE_RULES: Task test-task resumed - review previous context",
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
await withFixtureRunner("TaskResume", `hooks/taskresume/${scenario.fixtureName}`, hookTestEnv, async (runner) => {
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: scenario.lastMessageTs,
|
||||
messageCount: scenario.messageCount,
|
||||
conversationHistoryDeleted: scenario.conversationHistoryDeleted,
|
||||
},
|
||||
it(
|
||||
"should validate representative fixtures end-to-end",
|
||||
async () => {
|
||||
// Multiple fixture scenarios spawn child processes sequentially,
|
||||
// which can easily exceed the default 2 s Mocha timeout.
|
||||
const scenarios: FixtureScenario[] = [
|
||||
{
|
||||
fixtureName: "success",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("TaskResume hook executed successfully")
|
||||
},
|
||||
})
|
||||
},
|
||||
{
|
||||
fixtureName: "recent-resume",
|
||||
lastMessageTs: (Date.now() - 2 * 60 * 1000).toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.match(/Recently paused task/)
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "long-pause",
|
||||
lastMessageTs: (Date.now() - 48 * 60 * 60 * 1000).toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.match(/paused 48 hours ago/)
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "context-deleted",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "50",
|
||||
conversationHistoryDeleted: "true",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.match(/truncated/)
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "message-count",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "25",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("TASK_CONTEXT: Resuming task with 25 previous messages")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "context-injection",
|
||||
lastMessageTs: Date.now().toString(),
|
||||
messageCount: "5",
|
||||
conversationHistoryDeleted: "false",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal(
|
||||
"WORKSPACE_RULES: Task test-task resumed - review previous context",
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
scenario.assert(result)
|
||||
})
|
||||
}
|
||||
})
|
||||
for (const scenario of scenarios) {
|
||||
await withFixtureRunner(
|
||||
"TaskResume",
|
||||
`hooks/taskresume/${scenario.fixtureName}`,
|
||||
hookTestEnv,
|
||||
async (runner) => {
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
taskResume: {
|
||||
taskMetadata: { taskId: "test-task", ulid: "test-ulid" },
|
||||
previousState: {
|
||||
lastMessageTs: scenario.lastMessageTs,
|
||||
messageCount: scenario.messageCount,
|
||||
conversationHistoryDeleted: scenario.conversationHistoryDeleted,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
scenario.assert(result)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should preserve fixture-based failure behavior", async () => {
|
||||
await withFixtureRunner("TaskResume", "hooks/taskresume/error", hookTestEnv, async (runner) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { spyOn } from "bun:test"
|
||||
import * as fs from "fs/promises"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import should from "should"
|
||||
import sinon from "sinon"
|
||||
import { HostProvider } from "../../../hosts/host-provider"
|
||||
import { HookOutput } from "../../../shared/proto/cline/hooks"
|
||||
import { setVscodeHostProviderMock } from "../../../test/host-provider-test-utils"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import { StateManager } from "../../storage/StateManager"
|
||||
import { HookDiscoveryCache } from "../HookDiscoveryCache"
|
||||
@@ -60,17 +63,36 @@ export function hookPath(hooksDir: string, hookName: string, platform: NodeJS.Pl
|
||||
return path.join(hooksDir, hookFileName(hookName, platform))
|
||||
}
|
||||
|
||||
export function stubHookDirs(sandbox: sinon.SinonSandbox, dirs: string[]): sinon.SinonStub {
|
||||
const existing = diskModule.getAllHooksDirs as unknown as sinon.SinonStub
|
||||
if (existing && typeof existing.getCall === "function") {
|
||||
existing.resolves(dirs)
|
||||
return existing
|
||||
}
|
||||
// bun loads real ESM, so sinon cannot stub the `getAllHooksDirs` namespace
|
||||
// export ("ES Modules cannot be stubbed"). Use bun's spyOn, which can replace
|
||||
// ESM namespace bindings in place. The spy is tracked module-locally so the
|
||||
// per-env cleanup can restore it (the sandbox arg is retained for call-site
|
||||
// compatibility but is unused for this export).
|
||||
let hooksDirsSpy: ReturnType<typeof spyOn> | undefined
|
||||
|
||||
return sandbox.stub(diskModule, "getAllHooksDirs").resolves(dirs)
|
||||
export function stubHookDirs(_sandbox: sinon.SinonSandbox, dirs: string[]): ReturnType<typeof spyOn> {
|
||||
if (!hooksDirsSpy) {
|
||||
hooksDirsSpy = spyOn(diskModule, "getAllHooksDirs")
|
||||
}
|
||||
hooksDirsSpy.mockImplementation(async () => dirs)
|
||||
return hooksDirsSpy
|
||||
}
|
||||
|
||||
function restoreHookDirsSpy(): void {
|
||||
hooksDirsSpy?.mockRestore()
|
||||
hooksDirsSpy = undefined
|
||||
}
|
||||
|
||||
export async function createHookTestEnv(): Promise<HookTestEnv> {
|
||||
// Hook execution emits telemetry, which lazily constructs TelemetryService
|
||||
// via HostProvider.env.getHostVersion(). Under mocha's single-process run an
|
||||
// earlier suite left HostProvider initialized; bun's per-file isolation does
|
||||
// not, so initialize it here (idempotent) to keep the telemetry path from
|
||||
// throwing "HostProvider not setup".
|
||||
if (!HostProvider.isInitialized()) {
|
||||
setVscodeHostProviderMock()
|
||||
}
|
||||
|
||||
const sandbox = sinon.createSandbox()
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "hook-test-"))
|
||||
const hooksDir = await createHooksDirectory(tempDir)
|
||||
@@ -96,6 +118,7 @@ export async function createHookTestEnv(): Promise<HookTestEnv> {
|
||||
sandbox,
|
||||
cleanup: async () => {
|
||||
sandbox.restore()
|
||||
restoreHookDirsSpy()
|
||||
resetHookCache()
|
||||
await removeTempDirWithRetry(tempDir)
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
@@ -36,9 +36,7 @@ describe("UserPromptSubmit Hook", () => {
|
||||
})
|
||||
|
||||
describe("Hook Input Format", () => {
|
||||
it("should receive prompt text from user content", async function () {
|
||||
this.timeout(5000)
|
||||
|
||||
it("should receive prompt text from user content", async () => {
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "UserPromptSubmit")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -63,7 +61,7 @@ console.log(JSON.stringify({
|
||||
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Received prompt")
|
||||
})
|
||||
}, 5000)
|
||||
|
||||
it("should handle multiline prompts", async () => {
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "UserPromptSubmit")
|
||||
@@ -370,92 +368,94 @@ console.log(JSON.stringify({
|
||||
// Fixtures serve as both test data and examples for manual testing
|
||||
const isWindows = process.platform === "win32"
|
||||
|
||||
it("should validate representative fixtures end-to-end", async function () {
|
||||
// Multiple fixture scenarios spawn child processes sequentially,
|
||||
// which can easily exceed the default 2 s Mocha timeout.
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
it(
|
||||
"should validate representative fixtures end-to-end",
|
||||
async () => {
|
||||
// Multiple fixture scenarios spawn child processes sequentially,
|
||||
// which can easily exceed the default 2 s Mocha timeout.
|
||||
const scenarios: FixtureScenario[] = [
|
||||
{
|
||||
fixtureName: "success",
|
||||
prompt: "Create a feature",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Prompt approved")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "blocking",
|
||||
prompt: "Do something forbidden",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.true()
|
||||
result.errorMessage?.should.equal("Prompt violates policy")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "context-injection",
|
||||
prompt: "Build something",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("CONTEXT_INJECTION: User is in plan mode")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "multiline",
|
||||
prompt: "Line 1\nLine 2\nLine 3",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Line count: 3")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "special-chars",
|
||||
prompt: "Test @user #feature $cost",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Special chars preserved")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "empty-prompt",
|
||||
prompt: "",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Prompt length: 0")
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const scenarios: FixtureScenario[] = [
|
||||
{
|
||||
fixtureName: "success",
|
||||
prompt: "Create a feature",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Prompt approved")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "blocking",
|
||||
prompt: "Do something forbidden",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.true()
|
||||
result.errorMessage?.should.equal("Prompt violates policy")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "context-injection",
|
||||
prompt: "Build something",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("CONTEXT_INJECTION: User is in plan mode")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "multiline",
|
||||
prompt: "Line 1\nLine 2\nLine 3",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Line count: 3")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "special-chars",
|
||||
prompt: "Test @user #feature $cost",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Special chars preserved")
|
||||
},
|
||||
},
|
||||
{
|
||||
fixtureName: "empty-prompt",
|
||||
prompt: "",
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Prompt length: 0")
|
||||
},
|
||||
},
|
||||
]
|
||||
if (!isWindows) {
|
||||
scenarios.push({
|
||||
fixtureName: "large-prompt",
|
||||
prompt: "x".repeat(10000),
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Prompt size: 10000")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!isWindows) {
|
||||
scenarios.push({
|
||||
fixtureName: "large-prompt",
|
||||
prompt: "x".repeat(10000),
|
||||
assert: (result: HookOutput) => {
|
||||
result.cancel.should.be.false()
|
||||
result.contextModification?.should.equal("Prompt size: 10000")
|
||||
},
|
||||
})
|
||||
}
|
||||
for (const scenario of scenarios) {
|
||||
await withFixtureRunner(
|
||||
"UserPromptSubmit",
|
||||
`hooks/userpromptsubmit/${scenario.fixtureName}`,
|
||||
hookTestEnv,
|
||||
async (runner) => {
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
userPromptSubmit: {
|
||||
prompt: scenario.prompt,
|
||||
attachments: [],
|
||||
},
|
||||
})
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
await withFixtureRunner(
|
||||
"UserPromptSubmit",
|
||||
`hooks/userpromptsubmit/${scenario.fixtureName}`,
|
||||
hookTestEnv,
|
||||
async (runner) => {
|
||||
const result = await runner.run({
|
||||
taskId: "test-task",
|
||||
userPromptSubmit: {
|
||||
prompt: scenario.prompt,
|
||||
attachments: [],
|
||||
},
|
||||
})
|
||||
|
||||
scenario.assert(result)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
scenario.assert(result)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
WINDOWS_HOOK_TEST_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
it("should cover malformed-json fixture path", async () => {
|
||||
await withFixtureRunner("UserPromptSubmit", "hooks/userpromptsubmit/malformed-json", hookTestEnv, async (runner) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "fs/promises"
|
||||
import { after, beforeEach, describe, it } from "mocha"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { ClineIgnoreController } from "./ClineIgnoreController"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
|
||||
import * as extractTextModule from "@integrations/misc/extract-text"
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { formatResponse } from "../responses"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { formatResponse } from "../responses"
|
||||
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, it, mock } from "bun:test"
|
||||
import "should"
|
||||
import * as fsUtils from "@utils/fs"
|
||||
import * as actualFsUtils from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@utils/fs` namespace export
|
||||
// ("ES Modules cannot be stubbed"). Inject a module-level sinon stub for
|
||||
// `isDirectory` via mock.module so the full sinon stub API keeps working. It
|
||||
// defaults to the real implementation; only the error-propagation test overrides
|
||||
// it. Register both the alias form and the SUT's relative form.
|
||||
const realIsDirectory = actualFsUtils.isDirectory
|
||||
const isDirectoryStub: sinon.SinonStub = sinon.stub()
|
||||
const fsUtilsMock = () => ({ ...actualFsUtils, isDirectory: isDirectoryStub })
|
||||
mock.module("@utils/fs", fsUtilsMock)
|
||||
mock.module("@/utils/fs", fsUtilsMock)
|
||||
|
||||
import { getAllHooksDirs, getWorkspaceHooksDirs, setRuntimeHooksDir } from "../disk"
|
||||
import { StateManager } from "../StateManager"
|
||||
|
||||
@@ -16,6 +28,10 @@ describe("disk - hooks functionality", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
// Default the module-level isDirectory stub to the real implementation;
|
||||
// individual tests override it as needed.
|
||||
isDirectoryStub.reset()
|
||||
isDirectoryStub.callsFake((...args: unknown[]) => (realIsDirectory as (...a: unknown[]) => Promise<boolean>)(...args))
|
||||
tempDir = path.join(os.tmpdir(), `disk-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
})
|
||||
@@ -151,7 +167,7 @@ describe("disk - hooks functionality", () => {
|
||||
} as any)
|
||||
|
||||
// Stub isDirectory to throw an error
|
||||
sandbox.stub(fsUtils, "isDirectory").rejects(new Error("Permission denied"))
|
||||
isDirectoryStub.rejects(new Error("Permission denied"))
|
||||
|
||||
// Should propagate the error
|
||||
try {
|
||||
@@ -204,7 +220,7 @@ describe("disk - hooks functionality", () => {
|
||||
getGlobalStateKey: () => [],
|
||||
} as any)
|
||||
|
||||
sandbox.stub(fsUtils, "isDirectory").callsFake(async (targetPath: string) => targetPath === runtimeHooksDir)
|
||||
isDirectoryStub.callsFake(async (targetPath: string) => targetPath === runtimeHooksDir)
|
||||
|
||||
setRuntimeHooksDir(runtimeHooksDir)
|
||||
|
||||
@@ -220,7 +236,7 @@ describe("disk - hooks functionality", () => {
|
||||
getGlobalStateKey: () => [],
|
||||
} as any)
|
||||
|
||||
sandbox.stub(fsUtils, "isDirectory").resolves(false)
|
||||
isDirectoryStub.resolves(false)
|
||||
|
||||
setRuntimeHooksDir(runtimeHooksDir)
|
||||
|
||||
@@ -235,7 +251,7 @@ describe("disk - atomic writes", () => {
|
||||
let testGlobalStorageDir: string
|
||||
|
||||
// Setup HostProvider for tests with real temp directory
|
||||
before(async () => {
|
||||
beforeAll(async () => {
|
||||
// Create a real temp directory for the tests
|
||||
testGlobalStorageDir = path.join(os.tmpdir(), `cline-test-storage-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(testGlobalStorageDir, { recursive: true })
|
||||
@@ -246,7 +262,7 @@ describe("disk - atomic writes", () => {
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
HostProvider.reset()
|
||||
|
||||
// Clean up temp directory
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
import * as diskStorage from "@core/storage/disk"
|
||||
import * as remoteConfigFetch from "@core/storage/remote-config/fetch"
|
||||
import * as remoteConfigUtils from "@core/storage/remote-config/utils"
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import * as actualDiskStorage from "@core/storage/disk"
|
||||
import * as actualRemoteConfigUtils from "@core/storage/remote-config/utils"
|
||||
import * as assert from "assert"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@core/storage/disk` and
|
||||
// `@core/storage/remote-config/utils` namespace exports ("ES Modules cannot be
|
||||
// stubbed"). Inject module-level sinon stubs via mock.module so the full sinon
|
||||
// stub API keeps working. `AuthService`/`ClineAccountService` statics and the
|
||||
// `accountService` instance method are still sinon-stubbed directly below.
|
||||
const isRemoteConfigEnabledStub: sinon.SinonStub = sinon.stub()
|
||||
const applyRemoteConfigStub: sinon.SinonStub = sinon.stub()
|
||||
const clearRemoteConfigStub: sinon.SinonStub = sinon.stub()
|
||||
const writeRemoteConfigToCacheStub: sinon.SinonStub = sinon.stub()
|
||||
const readRemoteConfigFromCacheStub: sinon.SinonStub = sinon.stub()
|
||||
const deleteRemoteConfigFromCacheStub: sinon.SinonStub = sinon.stub()
|
||||
const diskMock = () => ({
|
||||
...actualDiskStorage,
|
||||
writeRemoteConfigToCache: writeRemoteConfigToCacheStub,
|
||||
readRemoteConfigFromCache: readRemoteConfigFromCacheStub,
|
||||
deleteRemoteConfigFromCache: deleteRemoteConfigFromCacheStub,
|
||||
})
|
||||
const utilsMock = () => ({
|
||||
...actualRemoteConfigUtils,
|
||||
isRemoteConfigEnabled: isRemoteConfigEnabledStub,
|
||||
applyRemoteConfig: applyRemoteConfigStub,
|
||||
clearRemoteConfig: clearRemoteConfigStub,
|
||||
})
|
||||
// Register both the alias form (this test's imports) and the relative form the
|
||||
// SUT (remote-config/fetch.ts) uses, since bun's mock.module matches specifiers.
|
||||
mock.module("@core/storage/disk", diskMock)
|
||||
mock.module("@/core/storage/disk", diskMock)
|
||||
mock.module("../disk", diskMock)
|
||||
mock.module("@core/storage/remote-config/utils", utilsMock)
|
||||
mock.module("@/core/storage/remote-config/utils", utilsMock)
|
||||
mock.module("./utils", utilsMock)
|
||||
|
||||
import * as remoteConfigFetch from "@core/storage/remote-config/fetch"
|
||||
|
||||
describe("fetchRemoteConfig", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let accountService: ClineAccountService
|
||||
let authServiceStub: Partial<AuthService>
|
||||
let fetchUserRemoteConfigStub: sinon.SinonStub
|
||||
let isRemoteConfigEnabledStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
@@ -21,12 +53,19 @@ describe("fetchRemoteConfig", () => {
|
||||
accountService = new ClineAccountService()
|
||||
sandbox.stub(ClineAccountService, "getInstance").returns(accountService)
|
||||
fetchUserRemoteConfigStub = sandbox.stub(accountService, "fetchUserRemoteConfig")
|
||||
isRemoteConfigEnabledStub = sandbox.stub(remoteConfigUtils, "isRemoteConfigEnabled").returns(true)
|
||||
sandbox.stub(remoteConfigUtils, "applyRemoteConfig").resolves()
|
||||
sandbox.stub(remoteConfigUtils, "clearRemoteConfig")
|
||||
sandbox.stub(diskStorage, "writeRemoteConfigToCache").resolves()
|
||||
sandbox.stub(diskStorage, "readRemoteConfigFromCache").resolves({ version: "v1" })
|
||||
sandbox.stub(diskStorage, "deleteRemoteConfigFromCache").resolves()
|
||||
|
||||
// Reset and (re)configure the module-level sinon stubs injected above.
|
||||
isRemoteConfigEnabledStub.reset()
|
||||
applyRemoteConfigStub.reset()
|
||||
clearRemoteConfigStub.reset()
|
||||
writeRemoteConfigToCacheStub.reset()
|
||||
readRemoteConfigFromCacheStub.reset()
|
||||
deleteRemoteConfigFromCacheStub.reset()
|
||||
isRemoteConfigEnabledStub.returns(true)
|
||||
applyRemoteConfigStub.resolves()
|
||||
writeRemoteConfigToCacheStub.resolves()
|
||||
readRemoteConfigFromCacheStub.resolves({ version: "v1" })
|
||||
deleteRemoteConfigFromCacheStub.resolves()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -55,7 +94,7 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
assert.strictEqual(controller.accountService.switchAccount.callCount, 1)
|
||||
assert.strictEqual(controller.accountService.switchAccount.firstCall.args[0], "org-target")
|
||||
assert.ok((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).calledOnce)
|
||||
assert.ok(applyRemoteConfigStub.calledOnce)
|
||||
})
|
||||
|
||||
it("skips switchAccount when already in the chosen org", async () => {
|
||||
@@ -79,7 +118,7 @@ describe("fetchRemoteConfig", () => {
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
assert.strictEqual(controller.accountService.switchAccount.callCount, 0)
|
||||
assert.ok((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).calledOnce)
|
||||
assert.ok(applyRemoteConfigStub.calledOnce)
|
||||
})
|
||||
|
||||
it("uses discoveredValue inline and skips org-level config fetch", async () => {
|
||||
@@ -103,11 +142,11 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
assert.ok((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).calledOnce)
|
||||
assert.ok(applyRemoteConfigStub.calledOnce)
|
||||
// writeRemoteConfigToCache is called with the parsed config, proving inline parse succeeded.
|
||||
// If it had fallen through to fetchRemoteConfigForOrganization, it would need getAuthToken
|
||||
// and make an HTTP call — but no axios stub is set up, so the test would fail.
|
||||
assert.ok((diskStorage.writeRemoteConfigToCache as sinon.SinonStub).calledOnce)
|
||||
assert.ok(writeRemoteConfigToCacheStub.calledOnce)
|
||||
})
|
||||
|
||||
it("falls back to org-level fetch when discoveredValue fails to parse", async () => {
|
||||
@@ -132,8 +171,8 @@ describe("fetchRemoteConfig", () => {
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
// Parse failed → fetchRemoteConfigForOrganization → no auth → cache fallback
|
||||
assert.ok((diskStorage.readRemoteConfigFromCache as sinon.SinonStub).called)
|
||||
assert.ok((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).calledOnce)
|
||||
assert.ok(readRemoteConfigFromCacheStub.called)
|
||||
assert.ok(applyRemoteConfigStub.calledOnce)
|
||||
})
|
||||
|
||||
it("does not switch org when resolve fails", async () => {
|
||||
@@ -149,7 +188,7 @@ describe("fetchRemoteConfig", () => {
|
||||
})
|
||||
|
||||
// Both inline parse and org-level fetch fail (no auth → no fetch), cache is empty
|
||||
;(diskStorage.readRemoteConfigFromCache as sinon.SinonStub).resolves(undefined)
|
||||
readRemoteConfigFromCacheStub.resolves(undefined)
|
||||
|
||||
const controller = {
|
||||
accountService: { switchAccount: sandbox.stub() },
|
||||
@@ -162,8 +201,8 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
// Config resolution failed — user should stay in their current org
|
||||
assert.strictEqual(controller.accountService.switchAccount.callCount, 0)
|
||||
assert.ok((remoteConfigUtils.clearRemoteConfig as sinon.SinonStub).called)
|
||||
assert.strictEqual((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.ok(clearRemoteConfigStub.called)
|
||||
assert.strictEqual(applyRemoteConfigStub.callCount, 0)
|
||||
})
|
||||
|
||||
it("falls back to next locally-allowed org when backend org is opted-out", async () => {
|
||||
@@ -187,7 +226,7 @@ describe("fetchRemoteConfig", () => {
|
||||
isRemoteConfigEnabledStub.withArgs("org-3").returns(true)
|
||||
// Fallback org has no discoveredValue, so it will go through fetchRemoteConfigForOrganization
|
||||
// which needs auth → will fall back to cache
|
||||
;(diskStorage.readRemoteConfigFromCache as sinon.SinonStub).resolves({ version: "v1" })
|
||||
readRemoteConfigFromCacheStub.resolves({ version: "v1" })
|
||||
|
||||
const controller = {
|
||||
accountService: { switchAccount: sandbox.stub().resolves() },
|
||||
@@ -198,7 +237,7 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
assert.ok((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).calledOnce)
|
||||
assert.ok(applyRemoteConfigStub.calledOnce)
|
||||
})
|
||||
|
||||
it("clears remote config when all orgs are locally opted-out", async () => {
|
||||
@@ -222,9 +261,9 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
assert.ok((remoteConfigUtils.clearRemoteConfig as sinon.SinonStub).called)
|
||||
assert.ok(clearRemoteConfigStub.called)
|
||||
assert.strictEqual(controller.accountService.switchAccount.callCount, 0)
|
||||
assert.strictEqual((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.strictEqual(applyRemoteConfigStub.callCount, 0)
|
||||
})
|
||||
|
||||
it("calls clearRemoteConfig when discovery returns no qualifying org", async () => {
|
||||
@@ -239,9 +278,9 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
assert.ok((remoteConfigUtils.clearRemoteConfig as sinon.SinonStub).called)
|
||||
assert.ok(clearRemoteConfigStub.called)
|
||||
assert.strictEqual(controller.accountService.switchAccount.callCount, 0)
|
||||
assert.strictEqual((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.strictEqual(applyRemoteConfigStub.callCount, 0)
|
||||
})
|
||||
|
||||
it("clears remote config when isRemoteConfigEnabled toggled off mid-flight", async () => {
|
||||
@@ -268,9 +307,9 @@ describe("fetchRemoteConfig", () => {
|
||||
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
assert.ok((diskStorage.writeRemoteConfigToCache as sinon.SinonStub).calledOnce)
|
||||
assert.ok((remoteConfigUtils.clearRemoteConfig as sinon.SinonStub).called)
|
||||
assert.strictEqual((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.ok(writeRemoteConfigToCacheStub.calledOnce)
|
||||
assert.ok(clearRemoteConfigStub.called)
|
||||
assert.strictEqual(applyRemoteConfigStub.callCount, 0)
|
||||
})
|
||||
|
||||
it("preserves existing config on unexpected network error", async () => {
|
||||
@@ -286,7 +325,7 @@ describe("fetchRemoteConfig", () => {
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
// Transient errors should NOT clear existing remote config
|
||||
assert.strictEqual((remoteConfigUtils.clearRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.strictEqual(clearRemoteConfigStub.callCount, 0)
|
||||
assert.strictEqual(controller.postStateToWebview.callCount, 0)
|
||||
})
|
||||
|
||||
@@ -311,7 +350,7 @@ describe("fetchRemoteConfig", () => {
|
||||
await remoteConfigFetch.fetchRemoteConfig(controller as any)
|
||||
|
||||
// switchAccount failure should NOT clear existing remote config
|
||||
assert.strictEqual((remoteConfigUtils.clearRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.strictEqual((remoteConfigUtils.applyRemoteConfig as sinon.SinonStub).callCount, 0)
|
||||
assert.strictEqual(clearRemoteConfigStub.callCount, 0)
|
||||
assert.strictEqual(applyRemoteConfigStub.callCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Covers: transformRemoteConfigToStateShape and toggle synchronisation logic.
|
||||
*/
|
||||
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
import { synchronizeRemoteRuleToggles } from "@/core/context/instructions/user-instructions/rule-helpers"
|
||||
import { parseRemoteSkillEntries } from "@/core/context/instructions/user-instructions/skills"
|
||||
import { transformRemoteConfigToStateShape } from "@/core/storage/remote-config/utils"
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import "should"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import * as actualDiskModule from "@core/storage/disk"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@core/storage/disk` namespace
|
||||
// export ("ES Modules cannot be stubbed"). Inject a module-level sinon stub for
|
||||
// `getMcpSettingsFilePath` via mock.module so the full sinon stub API keeps
|
||||
// working. Register both the alias form and the relative form the SUT uses.
|
||||
const getMcpSettingsFilePathStub: sinon.SinonStub = sinon.stub()
|
||||
const diskMock = () => ({ ...actualDiskModule, getMcpSettingsFilePath: getMcpSettingsFilePathStub })
|
||||
mock.module("@core/storage/disk", diskMock)
|
||||
mock.module("@/core/storage/disk", diskMock)
|
||||
mock.module("../../disk", diskMock)
|
||||
|
||||
import { syncRemoteMcpServersToSettings } from "../remote-config/syncRemoteMcpServers"
|
||||
|
||||
describe("syncRemoteMcpServersToSettings", () => {
|
||||
@@ -18,7 +29,8 @@ describe("syncRemoteMcpServersToSettings", () => {
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
settingsPath = path.join(tempDir, "cline_mcp_settings.json")
|
||||
|
||||
sandbox.stub(diskModule, "getMcpSettingsFilePath").callsFake(async () => {
|
||||
getMcpSettingsFilePathStub.reset()
|
||||
getMcpSettingsFilePathStub.callsFake(async () => {
|
||||
try {
|
||||
await fs.access(settingsPath)
|
||||
} catch {
|
||||
@@ -30,6 +42,7 @@ describe("syncRemoteMcpServersToSettings", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
sandbox.restore()
|
||||
getMcpSettingsFilePathStub.reset()
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
} catch {
|
||||
|
||||
@@ -78,15 +78,11 @@ export abstract class WebviewProvider {
|
||||
// The JS file from the React build output
|
||||
const scriptUrl = this.getExtensionUrl("webview-ui", "build", "assets", "index.js")
|
||||
|
||||
// The CSS file from the React build output
|
||||
// The CSS file from the React build output. The webview's own index.css
|
||||
// @imports @vscode/codicons, so the codicon @font-face + codicon.ttf are
|
||||
// bundled into these build assets — no separate codicons <link> needed.
|
||||
const stylesUrl = this.getExtensionUrl("webview-ui", "build", "assets", "index.css")
|
||||
|
||||
// The codicon font from the React build output
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
|
||||
// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
|
||||
// don't forget to add font-src ${webview.cspSource};
|
||||
const codiconsUrl = this.getExtensionUrl("node_modules", "@vscode", "codicons", "dist", "codicon.css")
|
||||
|
||||
// Use a nonce to only allow a specific script to be run.
|
||||
/*
|
||||
content security policy of your webview to only allow scripts that have a specific nonce
|
||||
@@ -109,7 +105,6 @@ export abstract class WebviewProvider {
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUrl}">
|
||||
<link href="${codiconsUrl}" rel="stylesheet" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
|
||||
connect-src https://*.posthog.com https://*.cline.bot;
|
||||
font-src ${this.getCspSource()} data:;
|
||||
@@ -182,7 +177,6 @@ export abstract class WebviewProvider {
|
||||
|
||||
const nonce = getNonce()
|
||||
const stylesUrl = this.getExtensionUrl("webview-ui", "build", "assets", "index.css")
|
||||
const codiconsUrl = this.getExtensionUrl("node_modules", "@vscode", "codicons", "dist", "codicon.css")
|
||||
|
||||
const scriptEntrypoint = "src/main.tsx"
|
||||
const scriptUrl = `http://${localServerUrl}/${scriptEntrypoint}`
|
||||
@@ -215,7 +209,6 @@ export abstract class WebviewProvider {
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
<link rel="stylesheet" type="text/css" href="${stylesUrl}">
|
||||
<link href="${codiconsUrl}" rel="stylesheet" />
|
||||
<title>Cline</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Tests the core functionality of path resolution in single and multi-root workspaces
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import { VcsType, WorkspaceRoot } from "@shared/multi-root/types"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { createWorkspacePathAdapter, WorkspacePathAdapter } from "../WorkspacePathAdapter"
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* These tests ensure behavior preservation during refactoring
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import { VcsType, WorkspaceRoot } from "@shared/multi-root/types"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
import {
|
||||
addWorkspaceHint,
|
||||
hasWorkspaceHint,
|
||||
|
||||
@@ -16,7 +16,7 @@ Designed to be driven from an agentic loop via `curl` commands.
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start the debug harness server
|
||||
npx tsx src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
bun src/dev/debug-harness/server.ts --auto-launch --skip-build
|
||||
|
||||
# Terminal 2: Interact via curl
|
||||
curl localhost:19229/api -d '{"method":"status"}'
|
||||
@@ -27,7 +27,7 @@ curl localhost:19229/api -d '{"method":"ui.screenshot"}'
|
||||
## Server Options
|
||||
|
||||
```
|
||||
npx tsx src/dev/debug-harness/server.ts [options]
|
||||
bun src/dev/debug-harness/server.ts [options]
|
||||
|
||||
Options:
|
||||
--skip-build Skip building extension/webview (use existing dist/)
|
||||
@@ -42,7 +42,7 @@ Options:
|
||||
```bash
|
||||
# This builds protos, extension (unminified+sourcemaps), webview (unminified+sourcemaps),
|
||||
# downloads VSCode, launches it, and connects CDP to the extension host.
|
||||
npx tsx src/dev/debug-harness/server.ts --auto-launch
|
||||
bun src/dev/debug-harness/server.ts --auto-launch
|
||||
```
|
||||
|
||||
## Data Isolation
|
||||
@@ -126,7 +126,7 @@ curl localhost:19229/api -d '{"method":"ui.screenshot"}'
|
||||
MCP servers that require OAuth use a different flow: the browser redirects
|
||||
to a `vscode://` URI handled by the extension's URI handler. The auth provider
|
||||
(e.g. Linear) decides the `code`; for end-to-end testing, pair this with the
|
||||
local MCP OAuth test server (`npm run dev:mcp-oauth-test-server`, see
|
||||
local MCP OAuth test server (`bun run dev:mcp-oauth-test-server`, see
|
||||
`src/dev/mcp-oauth-test-server/README.md`), which mints real codes/tokens.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -10,15 +10,21 @@
|
||||
* - UI automation (click, type, screenshot) via Playwright
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx src/dev/debug-harness/server.ts [options]
|
||||
* bun src/dev/debug-harness/server.ts [options]
|
||||
*
|
||||
* Options:
|
||||
* --skip-build Skip building extension/webview
|
||||
* --auto-launch Automatically launch VSCode on startup
|
||||
* --workspace PATH Workspace directory to open
|
||||
* --port PORT Server port (default: 19229)
|
||||
* --launch-timeout MS Playwright _electron.launch timeout (default: 120000)
|
||||
* --no-browser-capture Let openExternal() open real browser windows (for interactive OAuth)
|
||||
*
|
||||
* Env:
|
||||
* VSCODE_TEST_VERSION Debugee VSCode version to download (default: 1.103.0; the
|
||||
* bundled Playwright cannot drive the Electron in the latest
|
||||
* "stable"). Set to "stable" or a pinned x.y.z to override.
|
||||
*
|
||||
* Then send commands:
|
||||
* curl localhost:19229/api -d '{"method":"launch"}'
|
||||
* curl localhost:19229/api -d '{"method":"ui.screenshot"}'
|
||||
@@ -49,6 +55,10 @@ function getArg(name: string): string | undefined {
|
||||
|
||||
const PORT = Number.parseInt(getArg("--port") || "19229", 10)
|
||||
const EXT_INSPECT_PORT = 9230
|
||||
// Playwright's _electron.launch() resolves once it controls the Electron main
|
||||
// process. A cold launch (first-run profile init, slow CI/VM) can exceed the old
|
||||
// 60s default, so allow more headroom and let it be overridden.
|
||||
const LAUNCH_TIMEOUT_MS = Number.parseInt(getArg("--launch-timeout") || "120000", 10)
|
||||
const PROJECT_ROOT = path.resolve(__script_dir, "..", "..", "..")
|
||||
const SCREENSHOT_DIR = path.join(os.tmpdir(), "cline-debug")
|
||||
const DEFAULT_WORKSPACE = path.join(os.tmpdir(), "cline-debug-workspace")
|
||||
@@ -344,9 +354,13 @@ class DebugHarness {
|
||||
}
|
||||
}
|
||||
|
||||
// Download VSCode binary
|
||||
// Download VSCode binary. Default to a version whose Electron the bundled
|
||||
// Playwright can drive (the very latest "stable" can ship an Electron newer
|
||||
// than playwright supports, making _electron.launch() hang). Override with
|
||||
// VSCODE_TEST_VERSION (e.g. "stable" or a pinned x.y.z).
|
||||
log("Ensuring VSCode binary is available...")
|
||||
const executablePath = await downloadAndUnzipVSCode("stable", undefined, new SilentReporter())
|
||||
const vscodeVersion = process.env.VSCODE_TEST_VERSION || "1.103.0"
|
||||
const executablePath = await downloadAndUnzipVSCode(vscodeVersion, undefined, new SilentReporter())
|
||||
log(`VSCode binary: ${executablePath}`)
|
||||
|
||||
// Resolve the CLINE_DIR for the debugee (separate from debugger's ~/.cline)
|
||||
@@ -381,6 +395,15 @@ class DebugHarness {
|
||||
"--disable-updates",
|
||||
"--skip-welcome",
|
||||
"--skip-release-notes",
|
||||
// Headless/VM GPU stacks crash the renderer's GPU process
|
||||
// ("Exiting GPU process during initialization" /
|
||||
// "CreateCommandBuffer kTransientFailure"), which can kill the window
|
||||
// before Playwright finishes attaching and trip the launch timeout.
|
||||
// Force software rendering for a stable debugee.
|
||||
"--disable-gpu",
|
||||
"--disable-gpu-compositing",
|
||||
"--disable-software-rasterizer",
|
||||
"--disable-dev-shm-usage",
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
workspace,
|
||||
],
|
||||
@@ -396,9 +419,13 @@ class DebugHarness {
|
||||
CLINE_CAPTURE_BROWSER: BROWSER_CAPTURE ? "1" : "0",
|
||||
CLINE_DEBUG_HARNESS_PORT: String(PORT),
|
||||
},
|
||||
timeout: 60000,
|
||||
timeout: LAUNCH_TIMEOUT_MS,
|
||||
})
|
||||
} catch (e: any) {
|
||||
// Best-effort: kill the orphaned Electron so a retry isn't blocked by a
|
||||
// half-launched instance, and so Playwright stops emitting late rejections
|
||||
// on the dead CDP transport.
|
||||
await this.app?.close().catch(() => {})
|
||||
this.app = null
|
||||
throw new Error(`Failed to launch VSCode: ${e.message}`)
|
||||
}
|
||||
@@ -1572,3 +1599,15 @@ async function cleanShutdown() {
|
||||
}
|
||||
process.on("SIGINT", cleanShutdown)
|
||||
process.on("SIGTERM", cleanShutdown)
|
||||
|
||||
// Keep the harness alive on stray async errors. A failed/aborted VSCode launch
|
||||
// (e.g. Playwright's _electron.launch timing out) can emit a late rejection on
|
||||
// the dead CDP transport AFTER we've already handled the launch error; without
|
||||
// this, the default behavior would crash the whole server and you'd have to
|
||||
// restart it just to retry. Log and keep serving so `launch` can be retried.
|
||||
process.on("unhandledRejection", (reason: unknown) => {
|
||||
log("Unhandled rejection (ignored, server stays up):", reason instanceof Error ? reason.message : String(reason))
|
||||
})
|
||||
process.on("uncaughtException", (err: Error) => {
|
||||
log("Uncaught exception (ignored, server stays up):", err.message)
|
||||
})
|
||||
|
||||
@@ -32,9 +32,9 @@ Exercises MCP OAuth failure modes without a real remote server:
|
||||
|
||||
```bash
|
||||
cd apps/vscode
|
||||
npm run dev:mcp-oauth-test-server -- --verbose
|
||||
bun run dev:mcp-oauth-test-server -- --verbose
|
||||
# or directly:
|
||||
npx tsx src/dev/mcp-oauth-test-server/server.ts --verbose
|
||||
bun src/dev/mcp-oauth-test-server/server.ts --verbose
|
||||
```
|
||||
|
||||
Then in Cline, add an MCP server (StreamableHTTP) pointing at:
|
||||
@@ -64,13 +64,13 @@ can click **Approve** or **Deny**.
|
||||
10-minute state window:
|
||||
|
||||
```bash
|
||||
npx tsx src/dev/mcp-oauth-test-server/server.ts --slow-authorize 605000 --verbose
|
||||
bun src/dev/mcp-oauth-test-server/server.ts --slow-authorize 605000 --verbose
|
||||
```
|
||||
|
||||
**Denied redirect** — always deny so every redirect carries `access_denied`:
|
||||
|
||||
```bash
|
||||
npx tsx src/dev/mcp-oauth-test-server/server.ts --auto-deny --verbose
|
||||
bun src/dev/mcp-oauth-test-server/server.ts --auto-deny --verbose
|
||||
```
|
||||
|
||||
## Debug-harness integration
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
import * as gitUtils from "@/utils/git"
|
||||
import * as actualGitUtils from "@/utils/git"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@/utils/git` namespace export
|
||||
// ("ES Modules cannot be stubbed"). Inject a module-level sinon stub for
|
||||
// `getGitDiff` via mock.module so the full sinon stub API keeps working.
|
||||
const getGitDiffStub: sinon.SinonStub = sinon.stub()
|
||||
const gitUtilsMock = () => ({ ...actualGitUtils, getGitDiff: getGitDiffStub })
|
||||
mock.module("@/utils/git", gitUtilsMock)
|
||||
mock.module("@utils/git", gitUtilsMock)
|
||||
|
||||
import { getGitDiffStagedFirst } from "../commit-message-generator"
|
||||
|
||||
describe("commit-message-generator", () => {
|
||||
describe("getGitDiffStagedFirst", () => {
|
||||
beforeEach(() => {
|
||||
getGitDiffStub.reset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
getGitDiffStub.reset()
|
||||
})
|
||||
|
||||
it("should return staged changes when they exist", async () => {
|
||||
const stub = sinon.stub(gitUtils, "getGitDiff")
|
||||
const stub = getGitDiffStub
|
||||
stub.withArgs("/repo", true).resolves("staged diff content")
|
||||
|
||||
const result = await getGitDiffStagedFirst("/repo")
|
||||
@@ -20,7 +34,7 @@ describe("commit-message-generator", () => {
|
||||
})
|
||||
|
||||
it("should fall back to all changes when no staged changes exist", async () => {
|
||||
const stub = sinon.stub(gitUtils, "getGitDiff")
|
||||
const stub = getGitDiffStub
|
||||
stub.withArgs("/repo", true).rejects(new Error("No changes in workspace for commit message"))
|
||||
stub.withArgs("/repo", false).resolves("all diff content")
|
||||
|
||||
@@ -32,7 +46,7 @@ describe("commit-message-generator", () => {
|
||||
})
|
||||
|
||||
it("should propagate error when both staged and all changes fail", async () => {
|
||||
const stub = sinon.stub(gitUtils, "getGitDiff")
|
||||
const stub = getGitDiffStub
|
||||
stub.withArgs("/repo", true).rejects(new Error("No changes"))
|
||||
stub.withArgs("/repo", false).rejects(new Error("No changes in workspace for commit message"))
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { ClineFileStorage } from "@shared/storage/ClineFileStorage"
|
||||
import { createStorageContext, type StorageContext } from "@shared/storage/storage-context"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { strict as assert } from "assert"
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import { strict as assert } from "assert"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { strict as assert } from "assert"
|
||||
import * as fs from "fs/promises"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as os from "os"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { strict as assert } from "assert"
|
||||
import * as fs from "fs/promises"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as vscode from "vscode"
|
||||
import { DiagnosticSeverity } from "@/shared/proto/index.cline"
|
||||
import { convertToFileDiagnostics, convertVscodeDiagnostics } from "./getDiagnostics"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { after, before, beforeEach, describe, it } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as fs from "fs/promises"
|
||||
import { after, before, beforeEach, describe, it } from "mocha"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { DiagnosticSeverity, FileDiagnostics } from "@shared/proto/index.cline"
|
||||
import { expect } from "chai"
|
||||
import { beforeEach, describe, it } from "mocha"
|
||||
import * as sinon from "sinon"
|
||||
import * as pathUtils from "@/utils/path"
|
||||
import * as actualPathUtils from "@/utils/path"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@/utils/path` namespace export
|
||||
// ("ES Modules cannot be stubbed"). Inject a module-level sinon stub for
|
||||
// `getCwd` via mock.module so the full sinon stub API keeps working.
|
||||
const getCwdStub: sinon.SinonStub = sinon.stub()
|
||||
const pathUtilsMock = () => ({ ...actualPathUtils, getCwd: getCwdStub })
|
||||
mock.module("@/utils/path", pathUtilsMock)
|
||||
mock.module("@utils/path", pathUtilsMock)
|
||||
|
||||
import { diagnosticsToProblemsString, getNewDiagnostics } from "../"
|
||||
|
||||
describe("Diagnostics Tests", () => {
|
||||
@@ -187,14 +196,14 @@ describe("Diagnostics Tests", () => {
|
||||
})
|
||||
|
||||
describe("diagnosticsToProblemsString", () => {
|
||||
let _getCwdStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
_getCwdStub = sinon.stub(pathUtils, "getCwd").resolves("/workspace")
|
||||
getCwdStub.reset()
|
||||
getCwdStub.resolves("/workspace")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
getCwdStub.reset()
|
||||
})
|
||||
|
||||
it("should return empty string when diagnostics array is empty", async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import * as assert from "assert"
|
||||
import { describe, it } from "mocha"
|
||||
import { DiffViewProvider } from "../DiffViewProvider"
|
||||
|
||||
class TestBoundaryDiffViewProvider extends DiffViewProvider {
|
||||
@@ -26,7 +26,7 @@ class TestBoundaryDiffViewProvider extends DiffViewProvider {
|
||||
return this.documentText
|
||||
}
|
||||
|
||||
async saveDocument(): Promise<Boolean> {
|
||||
async saveDocument(): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
async closeAllDiffViews(): Promise<void> {}
|
||||
@@ -215,7 +215,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
return this.documentText
|
||||
}
|
||||
|
||||
async saveDocument(): Promise<Boolean> {
|
||||
async saveDocument(): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
async closeAllDiffViews(): Promise<void> {}
|
||||
@@ -277,9 +277,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
assert.strictEqual(provider.replaceTextCallCount, 1, "Rapid updates should be throttled")
|
||||
})
|
||||
|
||||
it("should allow update after throttle period", async function () {
|
||||
this.timeout(500) // Allow time for the delay
|
||||
|
||||
it("should allow update after throttle period", async () => {
|
||||
const provider = new ThrottleTestDiffViewProvider()
|
||||
provider.setup("initial\n")
|
||||
|
||||
@@ -293,7 +291,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
// Next update should go through
|
||||
await provider.update("line1\nline2\n", false)
|
||||
assert.strictEqual(provider.replaceTextCallCount, 2, "Update after throttle period should go through")
|
||||
})
|
||||
}, 500)
|
||||
|
||||
it("should always process final updates regardless of throttling", async () => {
|
||||
const provider = new ThrottleTestDiffViewProvider()
|
||||
@@ -333,9 +331,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
assert.strictEqual(provider.replaceTextCallCount, 2, "Final update should bypass length check")
|
||||
})
|
||||
|
||||
it("should reset throttle state on reset()", async function () {
|
||||
this.timeout(500)
|
||||
|
||||
it("should reset throttle state on reset()", async () => {
|
||||
const provider = new ThrottleTestDiffViewProvider()
|
||||
provider.setup("initial\n")
|
||||
|
||||
@@ -356,7 +352,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
// Update immediately after reset should go through (throttle state cleared)
|
||||
await provider.update("newcontent\n", false)
|
||||
assert.strictEqual(provider.replaceTextCallCount, 1, "Update after reset should go through immediately")
|
||||
})
|
||||
}, 500)
|
||||
|
||||
it("should allow first update to go through immediately", async () => {
|
||||
const provider = new ThrottleTestDiffViewProvider()
|
||||
@@ -367,9 +363,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
assert.strictEqual(provider.replaceTextCallCount, 1, "First update should not be throttled")
|
||||
})
|
||||
|
||||
it("should handle streaming simulation with many rapid updates", async function () {
|
||||
this.timeout(500)
|
||||
|
||||
it("should handle streaming simulation with many rapid updates", async () => {
|
||||
const provider = new ThrottleTestDiffViewProvider()
|
||||
provider.setup("")
|
||||
|
||||
@@ -395,11 +389,9 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
// Final update always goes through
|
||||
await provider.update(contentAfterWait + "end", true)
|
||||
assert.strictEqual(provider.replaceTextCallCount, 3, "Final update should go through")
|
||||
})
|
||||
|
||||
it("should throttle by time regardless of content length changes", async function () {
|
||||
this.timeout(500)
|
||||
}, 500)
|
||||
|
||||
it("should throttle by time regardless of content length changes", async () => {
|
||||
const provider = new ThrottleTestDiffViewProvider()
|
||||
provider.setup("")
|
||||
|
||||
@@ -417,7 +409,7 @@ describe("DiffViewProvider Update Throttling", () => {
|
||||
// Now should go through
|
||||
await provider.update("line1\nline2\nline3\nline4\n", false)
|
||||
assert.strictEqual(provider.replaceTextCallCount, 2, "Should update after throttle period")
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
|
||||
describe("DiffViewProvider Newline Preservation", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it } from "mocha"
|
||||
import assert from "node:assert/strict"
|
||||
import { EventEmitter } from "events"
|
||||
import { describe, it } from "mocha"
|
||||
import { orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
import type {
|
||||
CommandExecutorCallbacks,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { EventEmitter } from "events"
|
||||
import { describe, it } from "mocha"
|
||||
import { orchestrateCommandExecution } from "../CommandOrchestrator"
|
||||
import { CHUNK_DEBOUNCE_MS } from "../constants"
|
||||
import type {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "mocha"
|
||||
import { getShellArgs, unwrapPowerShell } from "../shellArgs"
|
||||
|
||||
describe("unwrapPowerShell", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { afterEach, describe, it } from "bun:test"
|
||||
/**
|
||||
* Tests for selfHosted mode behavior across PostHog-based services.
|
||||
* When ClineEndpoint.isSelfHosted() returns true, all PostHog functionality should be disabled.
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Tests API fetching, caching, auth updates, and rate limit backoff
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import type { BannerRules } from "@shared/ClineBanner"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as sinon from "sinon"
|
||||
import { ClineEnv, Environment } from "@/config"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { ClineError, ClineErrorType } from "../ClineError"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterAll, describe, it } from "bun:test"
|
||||
import * as fs from "fs/promises"
|
||||
import { after, describe, it } from "mocha"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import "should"
|
||||
@@ -12,7 +12,7 @@ function normalizeForComparison(filePath: string): string {
|
||||
describe("listFiles", () => {
|
||||
const tmpDir = path.join(os.tmpdir(), `cline-list-files-test-${Math.random().toString(36).slice(2)}`)
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined)
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("listFiles gitignore handling", () => {
|
||||
// overwrite earlier .gitignore files and pass for the wrong reasons.
|
||||
const baseDir = path.join(os.tmpdir(), `cline-gitignore-test-${Math.random().toString(36).slice(2)}`)
|
||||
|
||||
after(async () => {
|
||||
afterAll(async () => {
|
||||
await fs.rm(baseDir, { recursive: true, force: true }).catch(() => undefined)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as nodeMachineId from "node-machine-id"
|
||||
import * as actualNodeMachineId from "node-machine-id"
|
||||
import * as sinon from "sinon"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `node-machine-id` namespace
|
||||
// export ("ES Modules cannot be stubbed"). Inject a module-level sinon stub for
|
||||
// `machineId` via mock.module so the full sinon stub API keeps working.
|
||||
const machineIdStub: sinon.SinonStub = sinon.stub()
|
||||
mock.module("node-machine-id", () => ({ ...actualNodeMachineId, machineId: machineIdStub }))
|
||||
|
||||
import { _GENERATED_MACHINE_ID_KEY, getDistinctId, initializeDistinctId, setDistinctId } from "@/services/logging/distinctId"
|
||||
import { StorageContext } from "@/shared/storage"
|
||||
|
||||
@@ -58,6 +65,9 @@ describe("distinctId", () => {
|
||||
// Mock extension storage
|
||||
mockStorage = { globalState: mockGlobalState } as unknown as StorageContext
|
||||
|
||||
// Reset the module-level node-machine-id stub
|
||||
machineIdStub.reset()
|
||||
|
||||
// Reset the distinctId module state
|
||||
setDistinctId("")
|
||||
})
|
||||
@@ -74,7 +84,7 @@ describe("distinctId", () => {
|
||||
|
||||
it("should use id from extension globalstate if it exists", async () => {
|
||||
mockGlobalState.get.withArgs(_GENERATED_MACHINE_ID_KEY).returns(MOCK_GLOBAL_STATE_ID)
|
||||
const machineIdStub = sandbox.stub(nodeMachineId, "machineId")
|
||||
// machineIdStub is the module-level stub (left unconfigured -> resolves undefined)
|
||||
|
||||
await initializeDistinctId(mockStorage, mockUuidGenerator)
|
||||
|
||||
@@ -85,7 +95,7 @@ describe("distinctId", () => {
|
||||
|
||||
it("should use the machine ID from node-machine-id", async () => {
|
||||
// Mock node-machine-id to return a machine ID
|
||||
const machineIdStub = sandbox.stub(nodeMachineId, "machineId").resolves(MOCK_MACHINE_ID)
|
||||
machineIdStub.resolves(MOCK_MACHINE_ID)
|
||||
|
||||
await initializeDistinctId(mockStorage, mockUuidGenerator)
|
||||
|
||||
@@ -97,7 +107,7 @@ describe("distinctId", () => {
|
||||
it("distinct ID should be stable", async () => {
|
||||
mockGlobalState.get.withArgs(_GENERATED_MACHINE_ID_KEY).returns(undefined)
|
||||
// Mock node-machine-id to return a machine ID
|
||||
sandbox.stub(nodeMachineId, "machineId").resolves(MOCK_MACHINE_ID)
|
||||
machineIdStub.resolves(MOCK_MACHINE_ID)
|
||||
|
||||
await initializeDistinctId(mockStorage, mockUuidGenerator)
|
||||
expect(getDistinctId()).to.equal(MOCK_MACHINE_ID)
|
||||
@@ -111,7 +121,7 @@ describe("distinctId", () => {
|
||||
it("should generate and store UUID if node-machine-id returns empty string", async () => {
|
||||
mockGlobalState.get.withArgs(_GENERATED_MACHINE_ID_KEY).returns(undefined)
|
||||
// Mock node-machine-id to return empty string
|
||||
const machineIdStub = sandbox.stub(nodeMachineId, "machineId").resolves("")
|
||||
machineIdStub.resolves("")
|
||||
|
||||
await initializeDistinctId(mockStorage, mockUuidGenerator)
|
||||
|
||||
@@ -123,7 +133,7 @@ describe("distinctId", () => {
|
||||
it("should handle node-machine-id errors gracefully", async () => {
|
||||
mockGlobalState.get.withArgs(_GENERATED_MACHINE_ID_KEY).returns(undefined)
|
||||
// Mock node-machine-id to throw an error
|
||||
const machineIdStub = sandbox.stub(nodeMachineId, "machineId").rejects(new Error("Failed to get machine ID"))
|
||||
machineIdStub.rejects(new Error("Failed to get machine ID"))
|
||||
|
||||
await initializeDistinctId(mockStorage, mockUuidGenerator)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
import { McpHub } from "../McpHub"
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import "should"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import fs from "fs/promises"
|
||||
import * as actualDiskModule from "@core/storage/disk"
|
||||
import fs, * as actualFsPromises from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@core/storage/disk` namespace
|
||||
// export, and `fs.promises` (the default import below) is NOT the same object as
|
||||
// the SUT's `import * as fs from "fs/promises"`. Inject module-level sinon stubs
|
||||
// via mock.module so the full sinon stub API keeps working on the exact
|
||||
// specifiers the SUT imports. `writeFile` defaults to the real implementation so
|
||||
// the test's own settings-file writes still hit disk.
|
||||
// Capture the genuine writeFile before mock.module overrides `fs/promises`, so
|
||||
// the test's pass-through stub does not recurse into itself.
|
||||
const realWriteFile = actualFsPromises.writeFile
|
||||
const getMcpSettingsFilePathStub: sinon.SinonStub = sinon.stub()
|
||||
const writeFileStub: sinon.SinonStub = sinon.stub()
|
||||
const diskMock = () => ({ ...actualDiskModule, getMcpSettingsFilePath: getMcpSettingsFilePathStub })
|
||||
const fsPromisesNamespace = { ...actualFsPromises, writeFile: writeFileStub }
|
||||
const fsPromisesMock = () => ({ ...fsPromisesNamespace, default: fsPromisesNamespace })
|
||||
mock.module("@core/storage/disk", diskMock)
|
||||
mock.module("@/core/storage/disk", diskMock)
|
||||
mock.module("fs/promises", fsPromisesMock)
|
||||
mock.module("node:fs/promises", fsPromisesMock)
|
||||
|
||||
import { McpHub } from "../McpHub"
|
||||
|
||||
// Regression tests for McpHub.deleteServerRPC(): deleting one server must not
|
||||
@@ -45,7 +65,12 @@ describe("McpHub.deleteServerRPC", () => {
|
||||
tempDir = path.join(os.tmpdir(), `mcp-delete-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
settingsPath = path.join(tempDir, "cline_mcp_settings.json")
|
||||
sandbox.stub(diskModule, "getMcpSettingsFilePath").resolves(settingsPath)
|
||||
getMcpSettingsFilePathStub.reset()
|
||||
getMcpSettingsFilePathStub.resolves(settingsPath)
|
||||
// Default writeFile to the real implementation; individual tests can wrap
|
||||
// it to observe behavior.
|
||||
writeFileStub.reset()
|
||||
writeFileStub.callsFake((...args: unknown[]) => (realWriteFile as (...a: unknown[]) => Promise<void>)(...args))
|
||||
|
||||
hub = Object.create(McpHub.prototype) as McpHub
|
||||
;(hub as any).getSettingsDirectoryPath = async () => tempDir
|
||||
@@ -95,10 +120,11 @@ describe("McpHub.deleteServerRPC", () => {
|
||||
const clock = sandbox.useFakeTimers()
|
||||
await writeSettings({ alpha: { type: "stdio", command: "a" }, beta: { type: "stdio", command: "b" } })
|
||||
|
||||
// Capture the flag at the moment the settings file is written.
|
||||
// Capture the flag at the moment the settings file is written. Wrap the
|
||||
// module-level writeFile stub (mock.module) rather than sinon-stubbing the
|
||||
// ESM `fs/promises` namespace, which bun forbids.
|
||||
let flagDuringWrite: boolean | undefined
|
||||
const realWriteFile = fs.writeFile.bind(fs)
|
||||
sandbox.stub(fs, "writeFile").callsFake((...args: unknown[]) => {
|
||||
writeFileStub.callsFake((...args: unknown[]) => {
|
||||
flagDuringWrite = (hub as any).isUpdatingClineSettings
|
||||
return (realWriteFile as (...a: unknown[]) => Promise<void>)(...args)
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* We avoid importing McpHub directly (too many transitive deps for unit tests).
|
||||
* Instead we extract the pure logic and test it in isolation.
|
||||
*/
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { type GetCallbackUrlFn, McpOAuthRedirectResolver } from "../McpOAuthRedirectResolver"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, it } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "bun:test"
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { shouldStartNewOAuthFlow } from "../mcpOAuthFlow"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it } from "mocha"
|
||||
import { describe, it } from "bun:test"
|
||||
import "should"
|
||||
import { McpSettingsSchema, ServerConfigSchema } from "../schemas"
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* validates provider switching capabilities, and ensures NoOpTelemetryProvider functionality
|
||||
*/
|
||||
|
||||
import { after, before, describe, it } from "mocha"
|
||||
import * as assert from "assert"
|
||||
import * as sinon from "sinon"
|
||||
import { ClineEndpoint } from "@/config"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { ApiFormat } from "@shared/proto/cline/models"
|
||||
import * as assert from "assert"
|
||||
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "../providers/ITelemetryProvider"
|
||||
|
||||
+21
-5
@@ -1,8 +1,23 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { InMemoryLogRecordExporter, LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs"
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import type { ClineAccountUserInfo } from "@/services/auth/AuthService"
|
||||
import * as distinctIdModule from "@/services/logging/distinctId"
|
||||
import * as actualDistinctIdModule from "@/services/logging/distinctId"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the `@/services/logging/distinctId`
|
||||
// namespace exports ("ES Modules cannot be stubbed"). Inject module-level sinon
|
||||
// stubs via mock.module so the full sinon stub API keeps working.
|
||||
const getDistinctIdStub: sinon.SinonStub = sinon.stub()
|
||||
const setDistinctIdStub: sinon.SinonStub = sinon.stub()
|
||||
const distinctIdMock = () => ({
|
||||
...actualDistinctIdModule,
|
||||
getDistinctId: getDistinctIdStub,
|
||||
setDistinctId: setDistinctIdStub,
|
||||
})
|
||||
mock.module("@/services/logging/distinctId", distinctIdMock)
|
||||
mock.module("@services/logging/distinctId", distinctIdMock)
|
||||
|
||||
import { OpenTelemetryTelemetryProvider } from "../OpenTelemetryTelemetryProvider"
|
||||
|
||||
function makeUserInfo(
|
||||
@@ -32,8 +47,6 @@ describe("OpenTelemetryTelemetryProvider.identifyUser", () => {
|
||||
let logExporter: InMemoryLogRecordExporter
|
||||
let loggerProvider: LoggerProvider
|
||||
let provider: OpenTelemetryTelemetryProvider
|
||||
let getDistinctIdStub: sinon.SinonStub
|
||||
let setDistinctIdStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
logExporter = new InMemoryLogRecordExporter()
|
||||
@@ -45,12 +58,15 @@ describe("OpenTelemetryTelemetryProvider.identifyUser", () => {
|
||||
bypassUserSettings: true,
|
||||
})
|
||||
|
||||
getDistinctIdStub = sinon.stub(distinctIdModule, "getDistinctId")
|
||||
setDistinctIdStub = sinon.stub(distinctIdModule, "setDistinctId")
|
||||
// Reset the module-level sinon stubs (injected via mock.module above).
|
||||
getDistinctIdStub.reset()
|
||||
setDistinctIdStub.reset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
getDistinctIdStub.reset()
|
||||
setDistinctIdStub.reset()
|
||||
})
|
||||
|
||||
it("should emit user_identified log and update distinct ID when ID changes", () => {
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { afterEach, beforeEach, describe, it, mock } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import * as fs from "fs/promises"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import * as webhookHooks from "@/services/lg-cns-integration/webhook-hooks"
|
||||
import * as actualWebhookHooks from "@/services/lg-cns-integration/webhook-hooks"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// bun loads real ESM, so sinon cannot stub the
|
||||
// `@/services/lg-cns-integration/webhook-hooks` namespace exports ("ES Modules
|
||||
// cannot be stubbed"). Inject module-level sinon stubs via mock.module so the
|
||||
// full sinon stub API keeps working.
|
||||
const writeLgWebhookConfigStub: sinon.SinonStub = sinon.stub()
|
||||
const writeLgWebhookHooksStub: sinon.SinonStub = sinon.stub()
|
||||
const webhookHooksMock = () => ({
|
||||
...actualWebhookHooks,
|
||||
writeLgWebhookConfig: writeLgWebhookConfigStub,
|
||||
writeLgWebhookHooks: writeLgWebhookHooksStub,
|
||||
})
|
||||
mock.module("@/services/lg-cns-integration/webhook-hooks", webhookHooksMock)
|
||||
mock.module("@services/lg-cns-integration/webhook-hooks", webhookHooksMock)
|
||||
|
||||
import { ErrorService } from "../error"
|
||||
import { SharedUriHandler } from "./SharedUriHandler"
|
||||
|
||||
@@ -121,8 +136,12 @@ describe("SharedUriHandler", () => {
|
||||
const promptFilePath = path.join(tempDir, "lg-spec.md")
|
||||
await fs.writeFile(promptFilePath, "Implement user registration flow", "utf-8")
|
||||
|
||||
const writeConfigStub = sandbox.stub(webhookHooks, "writeLgWebhookConfig").resolves()
|
||||
const writeHooksStub = sandbox.stub(webhookHooks, "writeLgWebhookHooks").resolves()
|
||||
const writeConfigStub = writeLgWebhookConfigStub
|
||||
writeConfigStub.reset()
|
||||
writeConfigStub.resolves()
|
||||
const writeHooksStub = writeLgWebhookHooksStub
|
||||
writeHooksStub.reset()
|
||||
writeHooksStub.resolves()
|
||||
|
||||
const result = await SharedUriHandler.handleUri(
|
||||
`vscode://cline.cline/lg-task?prompt-file=${encodeURIComponent(
|
||||
@@ -144,8 +163,12 @@ describe("SharedUriHandler", () => {
|
||||
})
|
||||
|
||||
it("should return false when LG task parameters are missing", async () => {
|
||||
const writeConfigStub = sandbox.stub(webhookHooks, "writeLgWebhookConfig").resolves()
|
||||
const writeHooksStub = sandbox.stub(webhookHooks, "writeLgWebhookHooks").resolves()
|
||||
const writeConfigStub = writeLgWebhookConfigStub
|
||||
writeConfigStub.reset()
|
||||
writeConfigStub.resolves()
|
||||
const writeHooksStub = writeLgWebhookHooksStub
|
||||
writeHooksStub.reset()
|
||||
writeHooksStub.resolves()
|
||||
const result = await SharedUriHandler.handleUri(
|
||||
"vscode://cline.cline/lg-task?prompt-file=%2Ftmp%2Fspec.md&webhook-url=https%3A%2F%2Fexample.com",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
|
||||
import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { strict as assert } from "node:assert"
|
||||
import { describe, it } from "mocha"
|
||||
import type { ClineMessage } from "../ExtensionMessage"
|
||||
import { getApiMetrics, getLastApiReqTotalTokens } from "../getApiMetrics"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
import {
|
||||
AwsBedrockSettingsSchema,
|
||||
ClineSettingsSchema,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it } from "bun:test"
|
||||
import { getProviderCollectionSync } from "@cline/llms"
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
import { getProviderDefaultModelId, getProviderModelIdKey } from "../provider-keys"
|
||||
|
||||
describe("Provider key mapping", () => {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { describe, it } from "bun:test"
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
|
||||
import {
|
||||
applyTransform,
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Preload for `bun test` (registered via bunfig.toml [test] preload). It makes
|
||||
// the SDK-adapter and model-catalog unit tests — written against vitest's `vi`
|
||||
// API and module aliases — run under `bun test`.
|
||||
//
|
||||
// TODO: migrate these suites to native `bun:test` (use `mock.module` / `spyOn`
|
||||
// directly and stub `vscode`/`@cline/core` per-file) and delete this preload's
|
||||
// `vi` shim. Until then the detail below documents exactly why the shim is shaped
|
||||
// the way it is.
|
||||
//
|
||||
// Two specifiers have real on-disk implementations that must be shadowed with
|
||||
// lightweight stubs in unit tests:
|
||||
//
|
||||
// vscode -> src/test/vscode-vitest-stub.ts (no VS Code host under bun)
|
||||
// @cline/core -> src/test/cline-core-vitest-stub.ts (lightweight SDK stub)
|
||||
//
|
||||
// bun's runtime plugin `onResolve` hook does NOT intercept these (`vscode` is
|
||||
// host/builtin-like and `@cline/core` is a symlinked workspace package — both
|
||||
// resolve below the JS plugin resolver). `mock.module()` is what works: it
|
||||
// registers an in-memory override that takes precedence for the whole test
|
||||
// process. (Other workspace packages — @cline/llms, @cline/shared,
|
||||
// @cline/shared/storage — and the tsconfig `paths` aliases resolve on their own.)
|
||||
//
|
||||
// bun's ESM linker statically validates every named import against the names on
|
||||
// the mock namespace. The stub only implements the @cline/core exports these
|
||||
// tests exercise, but other modules in the import graph statically import
|
||||
// additional names (e.g. `prepareRemoteConfigCoreIntegration`, `ClineCore`,
|
||||
// `createMcpTools`); a missing name is a hard "Export named 'X' not found" link
|
||||
// error. So we seed the mock namespace with every name the real @cline/core
|
||||
// exports (value `undefined`) and overlay the stub on top: stub names keep stub
|
||||
// behavior, every other valid import links as `undefined`.
|
||||
//
|
||||
// Importing the real package here is safe: the preload runs before any test
|
||||
// file, so this is the only point the real module is linked, and we only read
|
||||
// its export *names*, never its behavior (the mock shadows it everywhere tests look).
|
||||
import { vi as bunVi, mock } from "bun:test"
|
||||
import * as realClineCore from "@cline/core"
|
||||
import * as clineCoreStub from "./cline-core-vitest-stub"
|
||||
import * as vscodeStub from "./vscode-vitest-stub"
|
||||
|
||||
const clineCoreNamespace: Record<string, unknown> = {}
|
||||
for (const name of Object.keys(realClineCore)) {
|
||||
clineCoreNamespace[name] = undefined
|
||||
}
|
||||
Object.assign(clineCoreNamespace, clineCoreStub)
|
||||
|
||||
mock.module("@cline/core", () => clineCoreNamespace)
|
||||
|
||||
// `vscode`: the stub provides both named exports (Position, Uri, …) and a
|
||||
// default export (the namespace object). Preserve both shapes so `import * as
|
||||
// vscode from "vscode"` and `import vscode from "vscode"` both work.
|
||||
mock.module("vscode", () => ({ ...vscodeStub, default: vscodeStub.default }))
|
||||
|
||||
// `vitest`: the SDK-adapter and model-catalog unit tests import test primitives
|
||||
// and the `vi` helper namespace from "vitest". bun test provides describe/it/
|
||||
// expect/etc. and a partial `vi` object under `bun:test`, but does NOT register
|
||||
// a `vitest` module, so we alias one here, backed by the real bun:test
|
||||
// implementations wherever a 1:1 mapping exists. The remaining vitest-only
|
||||
// helpers (`vi.hoisted`, `vi.mock`, `vi.waitFor`, `vi.importActual`,
|
||||
// `vi.mocked`, `vi.resetModules`) are shimmed on top:
|
||||
//
|
||||
// • vi.hoisted(fn) — vitest hoists these above imports; under bun's ESM
|
||||
// ordering all imports settle before top-level test
|
||||
// code runs, so simply executing the factory in place
|
||||
// is sufficient (the returned object is referenced by
|
||||
// later vi.mock factory closures, which are lazy).
|
||||
// • vi.mock(spec, fac) — mapped to bun's mock.module. bun applies module
|
||||
// mocks retroactively to already-linked imports, which
|
||||
// matches vitest's hoisted-mock observable behavior.
|
||||
// • vi.waitFor(fn,opts) — poll fn until it resolves/returns without throwing.
|
||||
// • vi.importActual(s) — the module as resolved by vitest's `resolve.alias`
|
||||
// BEFORE `vi.mock` is layered on. For specifiers we
|
||||
// substitute via `mock.module` in this preload
|
||||
// (@cline/core, vscode), calling bun's `import()` from
|
||||
// inside a `vi.mock(sameSpecifier)` factory re-enters
|
||||
// the in-flight mock and DEADLOCKS. So we serve those
|
||||
// from a registry of the pre-built "actual" namespaces
|
||||
// (the stub modules vitest's alias points at) and only
|
||||
// fall back to real `import()` for everything else.
|
||||
// • vi.mocked(v) — identity (a TypeScript typing helper at runtime).
|
||||
// • vi.resetModules() — no-op; bun has no module registry to reset and these
|
||||
// suites re-establish their mocks per-test.
|
||||
const viWaitFor = async <T>(predicate: () => T | Promise<T>, options?: { timeout?: number; interval?: number }): Promise<T> => {
|
||||
const timeout = options?.timeout ?? 1000
|
||||
const interval = options?.interval ?? 20
|
||||
const deadline = Date.now() + timeout
|
||||
let lastError: unknown
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
return await predicate()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
if (Date.now() >= deadline) {
|
||||
throw lastError
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: bun has a *built-in* `vitest` → `bun:test` compatibility shim that
|
||||
// takes precedence over `mock.module("vitest", …)` (the alias does not fire for
|
||||
// the `vitest` specifier — verified empirically). The `vi` object that bun
|
||||
// hands to `import { vi } from "vitest"` is the SAME singleton as
|
||||
// `import { vi } from "bun:test"`. So instead of registering a fake module, we
|
||||
// augment that shared `vi` singleton in place with the vitest-only helpers
|
||||
// bun's compat layer omits.
|
||||
// Registry of pre-built "actual" namespaces for the specifiers this preload
|
||||
// substitutes. `importActual` serves these directly to avoid the re-entrant
|
||||
// mock-factory deadlock described above.
|
||||
const actualNamespaceRegistry: Record<string, unknown> = {
|
||||
"@cline/core": clineCoreNamespace,
|
||||
vscode: { ...vscodeStub, default: vscodeStub.default },
|
||||
}
|
||||
|
||||
// `vi.mock` → `mock.module`, with two adaptations to match vitest semantics
|
||||
// that bun's `mock.module` lacks:
|
||||
//
|
||||
// (1) vitest passes an `importOriginal` helper to the factory; bun does not.
|
||||
// We pass one (resolving from the "actual" registry, see importActual).
|
||||
//
|
||||
// (2) bun's `mock.module` DEADLOCKS if the factory is an async function that
|
||||
// actually suspends on an `await` — bun blocks the importing thread on the
|
||||
// returned promise without pumping the microtask queue (verified
|
||||
// empirically; bun's docs only ever show async factories that return
|
||||
// synchronously). vitest, by contrast, awaits async `vi.mock` factories
|
||||
// (e.g. ones that `await importOriginal()` / `await vi.importActual(...)`).
|
||||
// To bridge this without rewriting test files we invoke the factory
|
||||
// OURSELVES:
|
||||
// • sync result → register it directly.
|
||||
// • promise result → register a synchronous placeholder FIRST (seeded
|
||||
// with the original module's export names so consumers' named imports
|
||||
// link), then re-register synchronously with the resolved namespace
|
||||
// once the promise settles. A plain deferred-only register does NOT
|
||||
// propagate to already-linked *named* imports — the up-front
|
||||
// placeholder is what makes the live-binding update stick. `vi.mock`
|
||||
// runs at module top level and tests only run after an await boundary,
|
||||
// so the resolved registration is always in place before the first
|
||||
// test executes.
|
||||
const resolveOriginalNamespace = (specifier: string): unknown => {
|
||||
if (specifier in actualNamespaceRegistry) {
|
||||
return actualNamespaceRegistry[specifier]
|
||||
}
|
||||
try {
|
||||
// bun resolves `require` synchronously for both ESM and CJS workspace deps.
|
||||
return (globalThis as { require?: (id: string) => unknown }).require?.(specifier)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const viMock = (specifier: string, factory?: (importOriginal?: () => unknown) => unknown) => {
|
||||
if (typeof factory !== "function") {
|
||||
return mock.module(specifier, () => ({}))
|
||||
}
|
||||
const importOriginal = () => resolveOriginalNamespace(specifier)
|
||||
const result = factory(importOriginal)
|
||||
if (result && typeof (result as { then?: unknown }).then === "function") {
|
||||
// Seed a synchronous placeholder carrying the original export names so
|
||||
// consumers' named imports link before the async factory resolves.
|
||||
const original = resolveOriginalNamespace(specifier)
|
||||
const placeholder: Record<string, unknown> = {}
|
||||
if (original && typeof original === "object") {
|
||||
for (const name of Object.keys(original as object)) {
|
||||
placeholder[name] = (original as Record<string, unknown>)[name]
|
||||
}
|
||||
}
|
||||
mock.module(specifier, () => placeholder)
|
||||
void (result as Promise<unknown>).then((resolved) => {
|
||||
// Mutate the SAME placeholder object the live ESM bindings already
|
||||
// reference, then re-register it. Copying onto the existing object
|
||||
// (rather than swapping in a fresh one) is what makes already-linked
|
||||
// *named* imports observe the resolved values — re-registering a brand
|
||||
// new object can race the first test before the binding updates.
|
||||
if (resolved && typeof resolved === "object") {
|
||||
for (const name of Object.keys(resolved as object)) {
|
||||
placeholder[name] = (resolved as Record<string, unknown>)[name]
|
||||
}
|
||||
}
|
||||
mock.module(specifier, () => placeholder)
|
||||
})
|
||||
return
|
||||
}
|
||||
return mock.module(specifier, () => result as object)
|
||||
}
|
||||
|
||||
const viExtensions: Record<string, unknown> = {
|
||||
mocked: (value: unknown) => value,
|
||||
mock: viMock,
|
||||
hoisted: <T>(factory: () => T): T => factory(),
|
||||
// NOTE: return the registry value *synchronously* (not via an async
|
||||
// function) for known specifiers. bun blocks synchronously while awaiting an
|
||||
// async `vi.mock` factory and does not pump the microtask queue, so an
|
||||
// `async` importActual would never settle its continuation from inside a
|
||||
// factory → deadlock. Returning the plain namespace lets the factory's
|
||||
// `await` resolve in the same tick. Only the (rare) real-import fallback
|
||||
// returns a Promise.
|
||||
importActual: <T = unknown>(specifier: string): T | Promise<T> => {
|
||||
if (specifier in actualNamespaceRegistry) {
|
||||
return actualNamespaceRegistry[specifier] as T
|
||||
}
|
||||
return import(specifier) as Promise<T>
|
||||
},
|
||||
importMock: <T = unknown>(specifier: string): Promise<T> => import(specifier) as Promise<T>,
|
||||
resetModules: () => bunVi,
|
||||
waitFor: viWaitFor,
|
||||
}
|
||||
// Force-install every entry: these are intentional replacements/additions.
|
||||
// In particular `vi.mock` ALREADY exists on bun's `vi` (and is the one that
|
||||
// deadlocks on async factories), so it must be overwritten, not skipped.
|
||||
for (const [name, impl] of Object.entries(viExtensions)) {
|
||||
;(bunVi as Record<string, unknown>)[name] = impl
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user