Skip to content

feat: add tegg vitest adapter workspace#401

Merged
killagu merged 7 commits intoeggjs:masterfrom
killagu-claw:feat/vitest-adapter
Feb 17, 2026
Merged

feat: add tegg vitest adapter workspace#401
killagu merged 7 commits intoeggjs:masterfrom
killagu-claw:feat/vitest-adapter

Conversation

@killagu-claw
Copy link
Contributor

@killagu-claw killagu-claw commented Feb 15, 2026

Adds new workspace package @eggjs/tegg-vitest-adapter to support Vitest runs with tegg/egg-mock-like context semantics.

Key points:

  • Provides setup function + one-liner register entry.
  • Implements suite ctx + per-test ctx switching via ctxStorage.getStore override.
  • Adds tests covering ctx semantics, getStore restore behavior, and getApp failure bailing.

How to run:

  • cd core/vitest-adapter && npm test
  • cd core/vitest-adapter && npm run tsc:pub (requires ut/utoo in env)

Summary by CodeRabbit

  • New Features

    • Added a Vitest adapter and test runner providing automatic test context injection and lifecycle management, with configurable options and mock restoration.
  • Documentation

    • Added comprehensive README covering installation, configuration, lifecycle behavior, usage, environment notes, and limitations.
  • Tests

    • Added extensive Vitest suites and fixture apps validating context scoping, restoration, and lifecycle across hooks, nested, and parallel runs.
  • Chores

    • Added package, build, and test runner configuration and setup for the adapter.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new @eggjs/tegg-vitest-adapter package: a Vitest runner and shared utilities that manage Egg app resolution and ctxStorage lifecycle across Vitest file/task phases, plus docs, tests, fixtures, and build/config files.

Changes

Cohort / File(s) Summary
Runner & Shared Core
core/vitest-adapter/src/runner.ts, core/vitest-adapter/src/shared.ts
New TeggVitestRunner, HeldScope helpers, file/task state tracking, app resolution/ready caching, store override/restore, debug utilities, default getApp and mock-restore helpers.
Public API & Entry Points
core/vitest-adapter/src/index.ts, core/vitest-adapter/index.ts, core/vitest-adapter/runner.ts
Added configureTeggRunner and type re-exports; barrel files to expose package entry points and runner.
Package, Build & Config
core/vitest-adapter/package.json, core/vitest-adapter/tsconfig.json, core/vitest-adapter/tsconfig.pub.json, core/vitest-adapter/vitest.config.ts
New package metadata, exports, deps/peerDeps, build/test scripts, TypeScript compile/publish configs, and Vitest config wiring custom runner and aliases.
Documentation
core/vitest-adapter/README.md
New README documenting installation, setup (getApp, restoreMocks, parallel), lifecycle and context-injection semantics, limitations, and environment notes.
Tests & Test Setup
core/vitest-adapter/test/setup.ts, core/vitest-adapter/test/*.test.ts
Added ts-node test setup and multiple Vitest tests validating lifecycle, store restoration, error handling, context semantics, and app integration.
Fixtures
core/vitest-adapter/test/fixtures/apps/demo-app/*, .../modules/demo-module/*
Added demo-app fixture, service implementations (JS/TS), module metadata and config used by tests.
Misc
.gitignore
Added codex-logs/ to .gitignore.

Sequence Diagram

sequenceDiagram
    participant Vitest as Vitest
    participant Runner as TeggVitestRunner
    participant App as EggApp
    participant Store as ctxStorage

    Vitest->>Runner: collect file
    Runner->>App: getApp()/resolve
    App-->>Runner: app ready (cached per file)

    Vitest->>Runner: onBeforeRunSuite
    Runner->>Store: capture suite store
    Runner->>Store: enterWith(suite store)

    Vitest->>Runner: onBeforeRunTask
    Runner->>Runner: createHeldScope(task)
    Runner->>Store: set task store / enterWith(task store)

    Vitest->>Store: test executes with task store

    Vitest->>Runner: onAfterRunTask
    Runner->>Runner: releaseHeldScope -> restore previous store

    Vitest->>Runner: onAfterRunSuite
    Runner->>Store: restore suite store / cleanup
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

enhancement

Suggested reviewers

  • fengmk2
  • killagu

Poem

🐰 I hop through suites and tasks with care,
Holding stores and contexts in my lair,
App wakes, tests run, then curtains close,
Mocks restored, neat trails of code I chose,
A tegg-rabbit dances—ta-da! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add tegg vitest adapter workspace' directly and accurately reflects the main change: introducing a new workspace package for the tegg vitest adapter.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @zhizhizhina, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new Vitest adapter for the Egg.js/Tegg ecosystem, enabling developers to write tests using Vitest while maintaining the familiar context injection and lifecycle management provided by egg-mock. This integration facilitates a smoother transition or coexistence with Vitest, ensuring that application contexts are correctly managed across test suites and individual tests, and providing a robust testing environment.

Highlights

  • Vitest Adapter Introduction: A new @eggjs/tegg-vitest-adapter package was added to support Vitest runs with tegg/egg-mock-like context semantics.
  • Flexible Setup: Provides both an explicit setup function and a one-liner register entry for easy integration into Vitest configurations.
  • Context Management: Implements suite context and per-test context switching by overriding ctxStorage.getStore.
  • Robust Testing: Includes tests to cover context semantics, getStore restoration behavior, and graceful handling of getApp failures.
Changelog
  • core/vitest-adapter/README.md
    • Documented the purpose, installation, and usage of the @eggjs/tegg-vitest-adapter.
    • Detailed explicit and one-liner setup methods for Vitest.
    • Explained available options (getApp, parallel, restoreMocks).
    • Described the lifecycle and context injection mechanism, including limitations compared to Mocha.
  • core/vitest-adapter/index.ts
    • Re-exported all modules from the src directory to provide a unified entry point.
  • core/vitest-adapter/package.json
    • Created a new package definition for @eggjs/tegg-vitest-adapter.
    • Defined package metadata, main entry points, and export maps for . and ./register.
    • Added egg-mock as a dependency and vitest, egg, egg-tracer as dev dependencies.
    • Included scripts for cleaning, publishing, and testing.
  • core/vitest-adapter/register.ts
    • Provided a simple entry point to automatically set up the TeggVitestAdapter with default options.
  • core/vitest-adapter/src/index.ts
    • Implemented the core setupTeggVitestAdapter function.
    • Defined EggLikeApp and TeggVitestAdapterOptions interfaces.
    • Managed global state for suite and test contexts.
    • Created StoreController to handle ctxStorage.getStore overrides.
    • Integrated Vitest beforeAll, beforeEach, afterEach, afterAll hooks to manage app lifecycle, context creation, and restoration.
    • Added logic for parallel test handling, mock restoration, and graceful getApp failure.
  • core/vitest-adapter/test/fake_app.ts
    • Implemented a FakeApp class to simulate an Egg.js application for testing the adapter.
    • Included methods for ready, backgroundTasksFinished, close, mockContext, and mockContextScope.
    • Provided ctxStorage and context management logic for testing purposes.
  • core/vitest-adapter/test/get_app_throw.test.ts
    • Added a test to verify that the adapter gracefully handles scenarios where the getApp function throws an error, preventing test suite crashes.
  • core/vitest-adapter/test/get_store_restore.test.ts
    • Added a test to confirm that the ctxStorage.getStore function is correctly restored to the suite context after each individual test.
  • core/vitest-adapter/test/hooks.test.ts
    • Added comprehensive tests to validate the Vitest adapter's context semantics across beforeAll, beforeEach, afterEach, and afterAll hooks, ensuring proper context isolation and flow.
  • core/vitest-adapter/tsconfig.json
    • Configured TypeScript compilation for the vitest-adapter package, extending the monorepo's base configuration.
  • core/vitest-adapter/tsconfig.pub.json
    • Configured TypeScript compilation specifically for publishing the vitest-adapter package.
  • core/vitest-adapter/vitest.config.ts
    • Defined the Vitest configuration for the new package, setting the test environment to node and specifying test file inclusion patterns.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new package, @eggjs/tegg-vitest-adapter, to integrate tegg with vitest. The implementation cleverly uses vitest hooks to manage tegg context by monkey-patching app.ctxStorage.getStore(), effectively emulating egg-mock's behavior for Mocha. The code is well-structured, and the inclusion of comprehensive tests for context semantics, error handling, and lifecycle is commendable. I have a few suggestions to improve the documentation, package configuration, and code clarity.

Comment on lines +1 to +12
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"baseUrl": "./"
},
"exclude": [
"dist",
"node_modules",
"test"
]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file, tsconfig.pub.json, has the exact same content as tsconfig.json. This duplication can lead to maintenance issues. Consider removing this file and updating the tsc:pub script in package.json to use tsconfig.json. If there's a reason for two separate files, one should probably extend the other to avoid repeating configuration.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@core/vitest-adapter/package.json`:
- Around line 45-47: The package.json currently sets the Node engine to "node":
">=14.0.0" which is too low for Vitest 1.6; update the "engines" entry so the
node requirement is ">=18.0.0" (modify the "engines" block in
core/vitest-adapter/package.json) to prevent installs on unsupported Node
versions and ensure compatibility with Vitest 1.6.

In `@core/vitest-adapter/README.md`:
- Around line 77-80: Remove the "Command Output (local)" section from
core/vitest-adapter/README.md since it contains local dev notes and stale info;
locate the section header "## Command Output (local)" and delete that header
plus its bullet list so the README contains only consumer-facing documentation.

In `@core/vitest-adapter/src/index.ts`:
- Around line 130-143: safeGetApp currently swallows the thrown error from
getApp and logs only a generic message; update the catch in safeGetApp to
include the caught error (err) in the warning so diagnostics are clearer—when
getApp() throws, set STATE.disabled and STATE.warned as before but change the
console.warn call inside the !STATE.warned block to include the error (e.g.,
include err or err.stack) in the log message so the failure details are visible
for debugging.
🧹 Nitpick comments (7)
core/vitest-adapter/tsconfig.json (1)

1-12: tsconfig.json and tsconfig.pub.json are identical.

Both files have the same content. Typically tsconfig.json includes test/ for IDE type-checking during development, while tsconfig.pub.json excludes it for publishing. Consider removing "test" from the exclude list here so that editors pick up test files for diagnostics.

Suggested diff
   "exclude": [
     "dist",
-    "node_modules",
-    "test"
+    "node_modules"
   ]
core/vitest-adapter/package.json (1)

48-56: vitest should be a peerDependency, not just a devDependency.

The adapter imports Vitest's beforeAll/beforeEach/afterEach/afterAll hooks, so consumers must have Vitest installed. Declaring it as a peerDependency makes this contract explicit and avoids version mismatches.

Suggested change
   "dependencies": {
     "egg-mock": "^5.5.0"
   },
+  "peerDependencies": {
+    "vitest": "^1.6.0 || ^2.0.0"
+  },
   "devDependencies": {
     "@types/node": "^20.2.4",
     "typescript": "^5.0.4",
     "vitest": "^1.6.0",
core/vitest-adapter/test/fake_app.ts (1)

36-45: mockContext mutates getStore in place — scope inference depends on call order.

After the first call, this.ctxStorage.getStore is replaced (line 42), so subsequent calls always see a truthy store and assign scope: 'test'. This is fine for the current test scenarios but could be confusing if tests are added that call mockContext multiple times expecting independent scope detection. A comment clarifying this would help future contributors.

core/vitest-adapter/test/get_store_restore.test.ts (1)

57-57: forEach callback should not implicitly return a value.

The concise arrow body causes the return value of assert.notStrictEqual to be returned from the callback. Use a block body to satisfy the linter and clarify intent.

Proposed fix
-    testCtxList.forEach(ctx => assert.notStrictEqual(ctx, suiteCtx));
+    testCtxList.forEach(ctx => { assert.notStrictEqual(ctx, suiteCtx); });
core/vitest-adapter/test/hooks.test.ts (1)

65-78: Variable itCtxList shadows the outer declaration on line 21.

The inner itCtxList (array) shadows the outer itCtxList (Record) declared on line 21. While the scoping works correctly, consider renaming to something like multiItCtxList to avoid confusion.

core/vitest-adapter/src/index.ts (2)

52-58: Module-level STATE is shared across all calls to setupTeggVitestAdapter.

If setupTeggVitestAdapter is invoked more than once in the same file (e.g. at module level and inside a describe), the shared STATE will cause both registrations to stomp on each other. Consider adding a guard to warn or no-op on duplicate calls.

Proposed guard
+let _installed = false;
+
 export function setupTeggVitestAdapter(options: TeggVitestAdapterOptions = {}) {
+  if (_installed) {
+    console.warn('[tegg-vitest-adapter] setupTeggVitestAdapter() called more than once; ignoring.');
+    return;
+  }
+  _installed = true;
+
   const parallel = options.parallel ?? defaultIsParallel();

180-194: afterEach calls safeGetApp() again — redundant for the common case.

Every afterEach invocation calls safeGetApp(), which re-invokes the user-provided getApp function. For the default require('egg-mock/bootstrap').app this is cheap (cached require), but a custom getApp with side effects or async work will be called 2N + 2 times across N tests. Consider caching the resolved app in STATE from beforeAll instead of re-resolving each time.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@core/vitest-adapter/src/runner.ts`:
- Around line 52-73: createHeldScope can hang if ctx.beginModuleScope rejects
before it invokes the inner callback because scopeReady() is never called;
modify createHeldScope to race scopePromise against readyPromise (e.g., await
Promise.race([scopePromise.catch(e => { throw e }), readyPromise])) so that if
beginModuleScope rejects the error propagates immediately instead of blocking,
and ensure scopePromise still refers to the original ctx.beginModuleScope result
(so gate/endScope behavior is preserved) and that any rejection is
awaited/handled to avoid unhandled rejections (reference createHeldScope,
ctx.beginModuleScope, scopePromise, readyPromise, scopeReady, gate, endScope).
- Around line 109-114: TeggVitestRunner's per-file state (fileAppMap,
disabledFiles, and warned) is accumulating across watch-mode reruns because
importFile repopulates them but they are never cleared; update the runner to
reset or selectively clear these collections at the start/end of a run: either
clear fileAppMap.clear(), disabledFiles.clear(), and warned = false at the
beginning of the collection phase (e.g. in the method that runs before
importFile), or add logic in onAfterRunSuite/onAfterRunTask to remove entries
for files not present in the current run; locate the importFile implementation
and the TeggVitestRunner lifecycle hooks (onAfterRunSuite, onAfterRunTask) and
implement the clearing or conditional re-population there so state does not
persist incorrectly between watch-mode reruns.

In `@core/vitest-adapter/test/get_store_restore.test.ts`:
- Around line 56-67: The afterAll block performs multiple assertions then calls
cleanup (await app.close(), await mm.restore()), so if any assertion throws the
cleanup is skipped; modify the afterAll handler (the function passed to
afterAll) to run the assertions inside a try block and call await app.close()
and await mm.restore() in a finally block to guarantee cleanup; reference
getCtx(), suiteCtx, testCtxList, afterEachCtxList and the existing assertions so
you wrap those checks in try { ...assertions... } finally { await app.close();
await mm.restore(); }.

In `@core/vitest-adapter/test/hooks.test.ts`:
- Around line 32-42: In afterAll, wrap the assertions and any code that may
throw in a try block and move the cleanup calls into a finally block so
app.close() and mm.restore() always run; specifically, keep the existing
assertions involving getCtx(), beforeCtx, afterCtx, itCtxList, beforeEachCtxList
and afterEachCtxList inside the try, and put app.close() and mm.restore() in the
finally to guarantee cleanup even if assertions fail.
🧹 Nitpick comments (9)
core/vitest-adapter/test/setup.ts (1)

17-21: Mutating process.execArgv in-place may affect the host process unexpectedly.

process.execArgv is the shared, live array for the Node process. Pushing to it in-place means any other code that reads execArgv (e.g., Vitest's worker spawning logic) sees the mutation. This is likely intentional for child-worker ts-node registration, but worth a brief comment in code to clarify intent.

core/vitest-adapter/src/runner.ts (3)

147-154: Single-warning flag suppresses errors from subsequent files.

this.warned = true means that if getApp() fails for multiple files with potentially different errors, only the first error is logged. Consider always logging the error but perhaps throttling the full stack trace.

Proposed fix — always log the error, throttle the preamble
         } catch (err) {
           this.disabledFiles.add(filepath);
-          if (!this.warned) {
-            this.warned = true;
-            // eslint-disable-next-line no-console
-            console.warn('[tegg-vitest-adapter] getApp failed, skip context injection.', err);
-          }
+          // eslint-disable-next-line no-console
+          console.warn(`[tegg-vitest-adapter] getApp failed for ${filepath}, skip context injection.`, err);

120-159: importFile — communication via globalThis.__teggVitestConfig is pragmatic but fragile.

The convention of setting globalThis.__teggVitestConfig during module evaluation (from configureTeggRunner) and reading it here works because super.importFile executes the module synchronously (within the same task). This is worth a brief doc comment since it's a non-obvious contract — if Vitest ever changes to async module evaluation, this would break silently.


161-192: No error handling around createHeldScope calls in hooks.

If createHeldScope throws (e.g., beginModuleScope rejects), the error propagates out of onBeforeRunSuite/onBeforeRunTask and may leave partial state in the maps (e.g., fileScopeMap or taskScopeMap). Consider wrapping in try/catch to clean up partial state and still call the super hook.

Example for onBeforeRunSuite
          if (typeof suiteCtx.beginModuleScope === 'function') {
-            suiteScope = await createHeldScope(suiteCtx);
-            debugLog('suite held scope created');
+            try {
+              suiteScope = await createHeldScope(suiteCtx);
+              debugLog('suite held scope created');
+            } catch (err) {
+              controller.restore();
+              throw err;
+            }
          }

Also applies to: 214-238

core/vitest-adapter/src/shared.ts (2)

4-13: ctxStorage type: optional method markers are redundant given the intersection with AsyncLocalStorage<any>.

AsyncLocalStorage<any> already defines getStore() and enterWith(). The ? markers on these methods in the object-literal part of the intersection don't actually make them optional on the resulting type — the intersection with AsyncLocalStorage<any> guarantees they exist. If the intent is to handle cases where ctxStorage might be a partial/duck-typed object (not a real AsyncLocalStorage), consider using a union or a standalone interface instead of the intersection.


54-58: defaultGetApp is async but performs no async work.

The require() call is synchronous. The async keyword here is harmless (it wraps the return in a resolved Promise), but it's slightly misleading. It does match the Promise<EggMockApp | undefined> return type in the interface, so this is a minor style point.

core/vitest-adapter/test/fixture_app.test.ts (1)

8-11: Mixing createRequire(import.meta.url) with __dirname.

Line 8 creates a CJS-style require from import.meta.url (ESM pattern), but lines 10 and 14 use __dirname, which is a CJS global. Vitest may shim __dirname for you, but the inconsistency is fragile — if the project switches to native ESM or changes Vitest's config, __dirname will be undefined. Consider deriving the directory from import.meta.url consistently:

import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
core/vitest-adapter/test/hooks.test.ts (1)

7-10: Cross-package fixture dependency is fragile.

This test reaches into plugin/tegg/test/fixtures/apps/egg-app (three directories up). If that fixture is refactored or moved, this test silently breaks. Consider either copying a minimal fixture into core/vitest-adapter/test/fixtures/ (as fixture_app.test.ts does with demo-app) or adding a comment documenting the dependency.

core/vitest-adapter/test/get_store_restore.test.ts (1)

7-10: Same cross-package fixture dependency as hooks.test.ts.

Both this file and hooks.test.ts reach into plugin/tegg/test/fixtures/apps/egg-app. Consider extracting a shared local fixture or a shared test helper to reduce duplication and fragility.

Comment on lines +52 to +73
async function createHeldScope(ctx: any): Promise<HeldScope> {
let endScope!: () => void;
const gate = new Promise<void>(resolve => {
endScope = resolve;
});

let scopeReady!: () => void;
const readyPromise = new Promise<void>(resolve => {
scopeReady = resolve;
});

const scopePromise = ctx.beginModuleScope(async () => {
// init() has completed at this point, signal readiness
scopeReady();
await gate;
});

// Wait for init() inside beginModuleScope to finish
await readyPromise;

return { ctx, scopePromise, endScope };
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

createHeldScope can hang if beginModuleScope rejects before invoking the inner function.

If ctx.beginModuleScope throws (or rejects) during its initialization phase—before calling the inner callback—scopeReady() is never invoked, so await readyPromise (line 70) will block forever. Meanwhile, scopePromise rejects with no handler, causing an unhandled rejection.

Race scopePromise against readyPromise so that an init failure propagates immediately:

Proposed fix
-  // Wait for init() inside beginModuleScope to finish
-  await readyPromise;
+  // Wait for init() inside beginModuleScope to finish,
+  // but bail immediately if beginModuleScope itself rejects.
+  await Promise.race([readyPromise, scopePromise]);

If scopePromise rejects first, the Promise.race will propagate the error. If readyPromise resolves first, we continue as before (and scopePromise remains pending, held open by the gate).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function createHeldScope(ctx: any): Promise<HeldScope> {
let endScope!: () => void;
const gate = new Promise<void>(resolve => {
endScope = resolve;
});
let scopeReady!: () => void;
const readyPromise = new Promise<void>(resolve => {
scopeReady = resolve;
});
const scopePromise = ctx.beginModuleScope(async () => {
// init() has completed at this point, signal readiness
scopeReady();
await gate;
});
// Wait for init() inside beginModuleScope to finish
await readyPromise;
return { ctx, scopePromise, endScope };
}
async function createHeldScope(ctx: any): Promise<HeldScope> {
let endScope!: () => void;
const gate = new Promise<void>(resolve => {
endScope = resolve;
});
let scopeReady!: () => void;
const readyPromise = new Promise<void>(resolve => {
scopeReady = resolve;
});
const scopePromise = ctx.beginModuleScope(async () => {
// init() has completed at this point, signal readiness
scopeReady();
await gate;
});
// Wait for init() inside beginModuleScope to finish,
// but bail immediately if beginModuleScope itself rejects.
await Promise.race([readyPromise, scopePromise]);
return { ctx, scopePromise, endScope };
}
🤖 Prompt for AI Agents
In `@core/vitest-adapter/src/runner.ts` around lines 52 - 73, createHeldScope can
hang if ctx.beginModuleScope rejects before it invokes the inner callback
because scopeReady() is never called; modify createHeldScope to race
scopePromise against readyPromise (e.g., await
Promise.race([scopePromise.catch(e => { throw e }), readyPromise])) so that if
beginModuleScope rejects the error propagates immediately instead of blocking,
and ensure scopePromise still refers to the original ctx.beginModuleScope result
(so gate/endScope behavior is preserved) and that any rejection is
awaited/handled to avoid unhandled rejections (reference createHeldScope,
ctx.beginModuleScope, scopePromise, readyPromise, scopeReady, gate, endScope).

Comment on lines +109 to +114
export default class TeggVitestRunner extends VitestTestRunner {
private fileScopeMap = new Map<string, FileScopeState>();
private taskScopeMap = new Map<string, TaskScopeState>();
private fileAppMap = new Map<string, FileAppState>();
private disabledFiles = new Set<string>();
private warned = false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the runner.ts file to understand the full context
cat -n core/vitest-adapter/src/runner.ts | head -150

Repository: eggjs/tegg

Length of output: 5326


🏁 Script executed:

# Search for how TeggVitestRunner is instantiated or used
rg -n "TeggVitestRunner\|new.*Runner" --type=ts -C 3 core/vitest-adapter/

Repository: eggjs/tegg

Length of output: 36


🏁 Script executed:

# Look for lifecycle cleanup methods like destroy, cleanup, or reset
rg -n "destroy\|cleanup\|reset\|teardown" --type=ts core/vitest-adapter/src/

Repository: eggjs/tegg

Length of output: 36


🌐 Web query:

Vitest runner instance lifecycle in watch mode

💡 Result:

In Vitest watch mode (i.e. vitest / vitest watch), there isn’t a single “runner instance” that’s torn down and recreated on every rerun. Instead, Vitest keeps the main process (and Vite dev server/module graph) alive and triggers new test runs on changes, with different lifetimes depending on which layer you’re talking about:

1) Main process (Vitest/Vite server)

  • Initialized once when you start watch mode; it stays alive between reruns. The “initialization” phase can run again only if the config (or its imports) changes. [1]
  • globalSetup runs once per Vitest process start, and its teardown runs only when the process exits, not between watch reruns. [1][2]

2) Workers / pool processes (where test files actually execute)

  • Vitest spawns workers based on your pool (forks, threads, vmForks, vmThreads, etc.). [2]
  • By default, workers are not reused (to preserve isolation). Workers are reused only when:
    • isolate is disabled, or
    • you use VM pools (vmThreads / vmForks) where the VM layer provides isolation. [2]

3) Per-rerun behavior (what repeats when you edit files / press r)

  • A rerun starts a new test run, and Vitest will only re-run affected test files (based on the module graph, HMR-style). [1][3]
  • For each test file that runs in that rerun:
    • setupFiles execute before the test file (again for those files). [1][2]
    • file-level code re-imports according to isolation/module caching rules (notably, if isolate is disabled, imported modules can remain cached even though setupFiles still re-run). [2]

4) If you need “global setup/teardown per rerun” in watch mode

Vitest doesn’t currently re-run globalSetup/teardown on every watch rerun. The supported workaround is a custom reporter using onWatcherRerun (and onInit/onFinished). [4][5]

Sources:
[1] Vitest “Test Run Lifecycle” (watch mode + phases) (vitest.dev)
[2] Vitest lifecycle details on worker reuse + teardown in watch mode (cn.vitest.dev)
[3] Vitest “Features” guide (watch mode reruns related tests via module graph) (main.vitest.dev)
[4] Vitest GitHub Discussion #6719 (maintainer: use reporter onWatcherRerun) (github.com)
[5] Vitest Reporters API (includes onWatcherRerun, onInit, etc.) (main.vitest.dev)

Citations:


🏁 Script executed:

# Get the complete runner.ts file to check for lifecycle cleanup methods
wc -l core/vitest-adapter/src/runner.ts

Repository: eggjs/tegg

Length of output: 91


🏁 Script executed:

# Read the rest of the file to look for cleanup/destroy/reset methods
tail -200 core/vitest-adapter/src/runner.ts | cat -n

Repository: eggjs/tegg

Length of output: 7523


🏁 Script executed:

# Search for any methods that might clear state maps
rg -n "fileScopeMap\|taskScopeMap\|fileAppMap\|disabledFiles" --type=ts core/vitest-adapter/src/runner.ts -A 2 -B 2

Repository: eggjs/tegg

Length of output: 36


🏁 Script executed:

# Check what methods VitestTestRunner base class might provide
rg -n "override|class.*VitestTestRunner" --type=ts core/vitest-adapter/src/ -A 5

Repository: eggjs/tegg

Length of output: 593


🏁 Script executed:

# Check if there are any lifecycle hooks like onRun, reset, or other cleanup mechanisms
rg -n "onRun|reset|clear|teardown" --type=ts core/vitest-adapter/src/runner.ts

Repository: eggjs/tegg

Length of output: 36


🏁 Script executed:

# Look for config files or setup that might instantiate or reset the runner
find core/vitest-adapter -name "*.ts" -o -name "*.js" | head -20

Repository: eggjs/tegg

Length of output: 692


🏁 Script executed:

# Check the package exports to see how runner is used
cat -n core/vitest-adapter/src/index.ts

Repository: eggjs/tegg

Length of output: 841


🏁 Script executed:

# Check the vitest config to understand how the runner is configured
cat -n core/vitest-adapter/vitest.config.ts

Repository: eggjs/tegg

Length of output: 1629


🏁 Script executed:

# Look at the test setup to understand collection behavior across runs
cat -n core/vitest-adapter/test/setup.ts

Repository: eggjs/tegg

Length of output: 997


State maps accumulate across runs — fileAppMap, disabledFiles, and warned never clear in watch mode.

The Vitest runner instance persists across watch-mode reruns. During each collection phase, importFile populates fileAppMap (line 85), adds to disabledFiles (line 89), and may set warned = true (line 91), but these are never cleared between runs. This causes:

  • fileAppMap to accumulate stale entries for files across reruns
  • disabledFiles entries to persist permanently (disabled files cannot recover)
  • warned flag to suppress warnings even if later test files fail with getApp

While fileScopeMap and taskScopeMap are properly cleaned in onAfterRunSuite and onAfterRunTask, the importFile-populated collections need reset or conditional re-population logic in watch mode.

🤖 Prompt for AI Agents
In `@core/vitest-adapter/src/runner.ts` around lines 109 - 114, TeggVitestRunner's
per-file state (fileAppMap, disabledFiles, and warned) is accumulating across
watch-mode reruns because importFile repopulates them but they are never
cleared; update the runner to reset or selectively clear these collections at
the start/end of a run: either clear fileAppMap.clear(), disabledFiles.clear(),
and warned = false at the beginning of the collection phase (e.g. in the method
that runs before importFile), or add logic in onAfterRunSuite/onAfterRunTask to
remove entries for files not present in the current run; locate the importFile
implementation and the TeggVitestRunner lifecycle hooks (onAfterRunSuite,
onAfterRunTask) and implement the clearing or conditional re-population there so
state does not persist incorrectly between watch-mode reruns.

Comment on lines +56 to +67
afterAll(async () => {
// After all tests, suite context is restored
assert.strictEqual(getCtx(), suiteCtx);
assert.strictEqual(testCtxList.length, afterEachCtxList.length);
testCtxList.forEach((ctx, index) => {
assert.notStrictEqual(ctx, suiteCtx);
assert.strictEqual(ctx, afterEachCtxList[index]);
});
assert.notStrictEqual(testCtxList[0], testCtxList[1]);
await app.close();
await mm.restore();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same afterAll cleanup risk as in hooks.test.ts — assertions can prevent app.close().

Same pattern: if any assertion on lines 58–64 throws, app.close() and mm.restore() on lines 65–66 are skipped. Wrap assertions in try/finally to guarantee cleanup.

Proposed fix
   afterAll(async () => {
-    // After all tests, suite context is restored
-    assert.strictEqual(getCtx(), suiteCtx);
-    assert.strictEqual(testCtxList.length, afterEachCtxList.length);
-    testCtxList.forEach((ctx, index) => {
-      assert.notStrictEqual(ctx, suiteCtx);
-      assert.strictEqual(ctx, afterEachCtxList[index]);
-    });
-    assert.notStrictEqual(testCtxList[0], testCtxList[1]);
-    await app.close();
-    await mm.restore();
+    try {
+      assert.strictEqual(getCtx(), suiteCtx);
+      assert.strictEqual(testCtxList.length, afterEachCtxList.length);
+      testCtxList.forEach((ctx, index) => {
+        assert.notStrictEqual(ctx, suiteCtx);
+        assert.strictEqual(ctx, afterEachCtxList[index]);
+      });
+      assert.notStrictEqual(testCtxList[0], testCtxList[1]);
+    } finally {
+      await app.close();
+      await mm.restore();
+    }
   });
🤖 Prompt for AI Agents
In `@core/vitest-adapter/test/get_store_restore.test.ts` around lines 56 - 67, The
afterAll block performs multiple assertions then calls cleanup (await
app.close(), await mm.restore()), so if any assertion throws the cleanup is
skipped; modify the afterAll handler (the function passed to afterAll) to run
the assertions inside a try block and call await app.close() and await
mm.restore() in a finally block to guarantee cleanup; reference getCtx(),
suiteCtx, testCtxList, afterEachCtxList and the existing assertions so you wrap
those checks in try { ...assertions... } finally { await app.close(); await
mm.restore(); }.

Comment on lines +32 to +42
afterAll(async () => {
afterCtx = getCtx();
assert(beforeCtx);
assert(beforeCtx !== itCtxList.foo);
assert(itCtxList.foo !== itCtxList.bar);
assert.strictEqual(afterCtx, beforeCtx);
assert.strictEqual(beforeEachCtxList.foo, afterEachCtxList.foo);
assert.strictEqual(beforeEachCtxList.foo, itCtxList.foo);
await app.close();
await mm.restore();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assertions before cleanup in afterAll risk leaking the app on failure.

If any assertion on lines 34–39 throws, app.close() and mm.restore() on lines 40–41 are never reached. This can leak the Egg app process/connections and affect subsequent test files.

Proposed fix: move cleanup into a finally block
   afterAll(async () => {
-    afterCtx = getCtx();
-    assert(beforeCtx);
-    assert(beforeCtx !== itCtxList.foo);
-    assert(itCtxList.foo !== itCtxList.bar);
-    assert.strictEqual(afterCtx, beforeCtx);
-    assert.strictEqual(beforeEachCtxList.foo, afterEachCtxList.foo);
-    assert.strictEqual(beforeEachCtxList.foo, itCtxList.foo);
-    await app.close();
-    await mm.restore();
+    try {
+      afterCtx = getCtx();
+      assert(beforeCtx);
+      assert(beforeCtx !== itCtxList.foo);
+      assert(itCtxList.foo !== itCtxList.bar);
+      assert.strictEqual(afterCtx, beforeCtx);
+      assert.strictEqual(beforeEachCtxList.foo, afterEachCtxList.foo);
+      assert.strictEqual(beforeEachCtxList.foo, itCtxList.foo);
+    } finally {
+      await app.close();
+      await mm.restore();
+    }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterAll(async () => {
afterCtx = getCtx();
assert(beforeCtx);
assert(beforeCtx !== itCtxList.foo);
assert(itCtxList.foo !== itCtxList.bar);
assert.strictEqual(afterCtx, beforeCtx);
assert.strictEqual(beforeEachCtxList.foo, afterEachCtxList.foo);
assert.strictEqual(beforeEachCtxList.foo, itCtxList.foo);
await app.close();
await mm.restore();
});
afterAll(async () => {
try {
afterCtx = getCtx();
assert(beforeCtx);
assert(beforeCtx !== itCtxList.foo);
assert(itCtxList.foo !== itCtxList.bar);
assert.strictEqual(afterCtx, beforeCtx);
assert.strictEqual(beforeEachCtxList.foo, afterEachCtxList.foo);
assert.strictEqual(beforeEachCtxList.foo, itCtxList.foo);
} finally {
await app.close();
await mm.restore();
}
});
🤖 Prompt for AI Agents
In `@core/vitest-adapter/test/hooks.test.ts` around lines 32 - 42, In afterAll,
wrap the assertions and any code that may throw in a try block and move the
cleanup calls into a finally block so app.close() and mm.restore() always run;
specifically, keep the existing assertions involving getCtx(), beforeCtx,
afterCtx, itCtxList, beforeEachCtxList and afterEachCtxList inside the try, and
put app.close() and mm.restore() in the finally to guarantee cleanup even if
assertions fail.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@core/vitest-adapter/package.json`:
- Around line 50-51: The peerDependencies claim compatibility with Vitest v1–v3
but only v1 is tested; update either the CI to test vitest@^2 and vitest@^3 (add
matrix entries in your workflow) to verify TeggVitestRunner (which extends
VitestTestRunner and invokes lifecycle methods like super.onTaskUpdate and
super.extendTaskContext) works with the changed payload/signature (onTaskUpdate
payload shape, extendTaskContext signature), or tighten peerDependencies in
package.json to only the tested range (e.g., ^1.6.0) until you add those CI
jobs; ensure devDependencies include the same tested vitest versions so local
dev/tests match CI.
🧹 Nitpick comments (1)
core/vitest-adapter/src/runner.ts (1)

119-123: source as any cast hides a potential type mismatch across vitest versions.

The as any cast on line 123 suggests the source parameter type doesn't match the base class signature. Given the broad peer dependency range (^1.6.0 || ^2.0.0 || ^3.0.0), verify that importFile's signature is compatible across all declared versions.

Comment on lines +50 to +51
"peerDependencies": {
"vitest": "^1.6.0 || ^2.0.0 || ^3.0.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the vitest-adapter implementation
find . -path "*/core/vitest-adapter*" -type f -name "*.ts" -o -name "*.js" | head -20

Repository: eggjs/tegg

Length of output: 1192


🏁 Script executed:

# Check the runner.ts implementation
find . -path "*/vitest-adapter*" -name "runner.ts" -o -path "*/vitest-adapter*" -name "runner.js"

Repository: eggjs/tegg

Length of output: 121


🏁 Script executed:

# Read the vitest-adapter package.json to confirm the peer dependencies and devDependencies
find . -path "*/core/vitest-adapter/package.json" -exec cat {} \;

Repository: eggjs/tegg

Length of output: 1354


🌐 Web query:

vitest VitestTestRunner API breaking changes v1 v2 v3 vitest/runners

💡 Result:

Vitest VitestTestRunner / vitest/runners + VitestRunner API breaking changes (v1 → v2 → v3)

v1 → v2 (Runner API)

  1. onTaskUpdate payload shape

    • v1: onTaskUpdate?: (task: [string, TaskResult | undefined][]) => Promise<void> (2-tuple items) [1]
    • v2: same 2-tuple shape (still no task meta in the callback payload) [2]
  2. File task gained local?: boolean in v2

    • v2 File: includes local?: boolean (“initiated without running any tests… populate state on the server side”) [2]
    • (This field is not present in v1’s doc page; it appears as part of the v2 Runner Tasks model.) [2]

v2 → v3 (Runner API)

  1. onTaskUpdate payload became a 3-tuple (meta added)

    • v2: onTaskUpdate?: (task: [string, TaskResult | undefined][]) => Promise<void> [2]
    • v3: onTaskUpdate?: (task: [string, TaskResult | undefined, TaskMeta | undefined][]) => Promise<void> (breaking) [3]
  2. extendTaskContext signature changed (generic TaskContext → TestContext)

    • v2: extendTaskContext?: <T extends Test | Custom>(context: TaskContext<T>) => TaskContext<T> [2]
    • v3: extendTaskContext?: (context: TestContext) => TestContext (breaking) [3]
  3. New runner hooks / fields in v3

    • injectValue?: (key: string) => unknown (used with test.extend(..., { injected: true })) [3]
    • pool?: string exposed on the runner instance [3]
  4. Runner “File task” shape changed

    • v2 File: has local?: boolean and describes projectName as “workspace project” [2]
    • v3 File: removes local?: boolean and describes projectName as “test project” [4]

Notes about VitestTestRunner / vitest/runners

  • Across v1/v2/v3 docs, Vitest recommends extending VitestTestRunner from vitest/runners to retain snapshot support and related runner features [1] [2] [4].

🏁 Script executed:

# Read the runner.ts implementation to see what vitest APIs are actually used
cat -n ./core/vitest-adapter/src/runner.ts

Repository: eggjs/tegg

Length of output: 9228


🏁 Script executed:

# Also check the main index.ts for any vitest API usage
cat -n ./core/vitest-adapter/src/index.ts

Repository: eggjs/tegg

Length of output: 841


🏁 Script executed:

# Check what's in shared.ts
cat -n ./core/vitest-adapter/src/shared.ts

Repository: eggjs/tegg

Length of output: 2589


Peer dependency claims v1–v3 compatibility, but only v1 is tested.

peerDependencies declares ^1.6.0 || ^2.0.0 || ^3.0.0, yet devDependencies only pins ^1.6.0. Vitest's vitest/runners API has documented breaking changes between v2 and v3 (e.g., onTaskUpdate payload shape, extendTaskContext signature). The TeggVitestRunner class extends VitestTestRunner and calls multiple super.*() lifecycle methods, which may be affected by these API changes. Add CI matrix entries for vitest v2 and v3 to verify compatibility, or narrow the peer dependency range until tested.

🤖 Prompt for AI Agents
In `@core/vitest-adapter/package.json` around lines 50 - 51, The peerDependencies
claim compatibility with Vitest v1–v3 but only v1 is tested; update either the
CI to test vitest@^2 and vitest@^3 (add matrix entries in your workflow) to
verify TeggVitestRunner (which extends VitestTestRunner and invokes lifecycle
methods like super.onTaskUpdate and super.extendTaskContext) works with the
changed payload/signature (onTaskUpdate payload shape, extendTaskContext
signature), or tighten peerDependencies in package.json to only the tested range
(e.g., ^1.6.0) until you add those CI jobs; ensure devDependencies include the
same tested vitest versions so local dev/tests match CI.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/vitest-adapter/src/runner.ts (1)

89-126: importFile resolves app during collection — consider error resilience.

Line 91 deletes __teggVitestConfig before the import, and lines 97-98 consume and delete it after. This is a clean approach for per-file config isolation. However, if config.getApp() throws (lines 108-121), the file is silently skipped with no entry in fileAppMap, meaning all lifecycle hooks gracefully no-op — which is the correct behavior.

One minor note: after the first getApp failure, this.warned = true suppresses warnings for all subsequent files, even if they use different getApp functions. This could hide distinct failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/vitest-adapter/src/runner.ts` around lines 89 - 126, importFile
currently sets a single boolean this.warned when config.getApp() throws, which
suppresses warnings for all subsequent files and can hide distinct failures;
change this to track warnings per-file (e.g., a Set or Map like
this.warnedFiles) or include the filepath in the console.warn and avoid flipping
a global flag so each failing getApp call logs at least once; update the error
handling in importFile (the try/catch around config.getApp()) to use
this.warnedFiles.has(filepath) / this.warnedFiles.add(filepath) or simply remove
the global this.warned and always include filepath in the warning message,
referencing importFile, config.getApp, this.warned (replace), and fileAppMap to
locate and adjust the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/vitest-adapter/test/get_app_throw.test.ts`:
- Around line 17-19: The comment in get_app_throw.test.ts incorrectly states
that getApp is called in onBeforeRunSuite; update the comment to reflect that
config.getApp() is invoked during the import/collect phase (importFile) in
runner.ts (see importFile and getApp references) rather than in
onBeforeRunSuite, and adjust any explanatory text or assertion comment
accordingly so it accurately mentions importFile/collect phase as the location
where getApp is called.

---

Duplicate comments:
In `@core/vitest-adapter/package.json`:
- Around line 1-59: The review contains a duplicate approval/comment and a
lingering note about the vitest peer dependency range; remove the duplicated
review message and update the PR description to explicitly state testing was
only done against vitest v1 (or alternatively adjust the "peerDependencies"
vitest range string to match the actually tested versions), referencing the
"peerDependencies" entry and the vitest version range "^1.6.0 || ^2.0.0 ||
^3.0.0" so reviewers know whether to broaden the test coverage or tighten the
declared range.

In `@core/vitest-adapter/src/runner.ts`:
- Around line 79-83: The fileAppMap and warned fields accumulate across
watch-mode reruns; update the runner to clear these at the start of each
collection cycle by resetting this.fileAppMap = new Map() and this.warned =
false when a collection begins (e.g., at the top of importFile when the incoming
parameter source === 'collect' or by adding/overriding an onBeforeCollect-style
hook); locate the logic in TeggVitestRunner (class name) and ensure the reset
happens before any per-file processing so state does not persist across reruns.
- Around line 42-63: createHeldScope can hang if ctx.beginModuleScope rejects
before the inner callback runs because scopeReady never gets called; fix by
awaiting a race between readyPromise and scopePromise while ensuring a rejection
from scopePromise triggers scopeReady so readyPromise doesn't hang: create
scopePromise via ctx.beginModuleScope(...) as before, then await
Promise.race([readyPromise, scopePromise.catch(err => { scopeReady(); throw err;
})]) (or equivalent) so any rejection from scopePromise resolves readyPromise
and propagates the error; keep references to
gate/endScope/scopeReady/readyPromise/scopePromise as in createHeldScope.

---

Nitpick comments:
In `@core/vitest-adapter/src/runner.ts`:
- Around line 89-126: importFile currently sets a single boolean this.warned
when config.getApp() throws, which suppresses warnings for all subsequent files
and can hide distinct failures; change this to track warnings per-file (e.g., a
Set or Map like this.warnedFiles) or include the filepath in the console.warn
and avoid flipping a global flag so each failing getApp call logs at least once;
update the error handling in importFile (the try/catch around config.getApp())
to use this.warnedFiles.has(filepath) / this.warnedFiles.add(filepath) or simply
remove the global this.warned and always include filepath in the warning
message, referencing importFile, config.getApp, this.warned (replace), and
fileAppMap to locate and adjust the logic.

Comment on lines +17 to +19
it('should not crash suite when getApp throws', () => {
// The runner calls getApp in onBeforeRunSuite, so it should have been called
assert(getAppCalls > 0);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inaccurate comment: getApp is called during importFile, not onBeforeRunSuite.

Looking at runner.ts, config.getApp() is invoked at line 109 inside importFile (during the collect phase), not in onBeforeRunSuite.

Suggested fix
   it('should not crash suite when getApp throws', () => {
-    // The runner calls getApp in onBeforeRunSuite, so it should have been called
+    // The runner calls getApp in importFile (collect phase), so it should have been called
     assert(getAppCalls > 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should not crash suite when getApp throws', () => {
// The runner calls getApp in onBeforeRunSuite, so it should have been called
assert(getAppCalls > 0);
it('should not crash suite when getApp throws', () => {
// The runner calls getApp in importFile (collect phase), so it should have been called
assert(getAppCalls > 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/vitest-adapter/test/get_app_throw.test.ts` around lines 17 - 19, The
comment in get_app_throw.test.ts incorrectly states that getApp is called in
onBeforeRunSuite; update the comment to reflect that config.getApp() is invoked
during the import/collect phase (importFile) in runner.ts (see importFile and
getApp references) rather than in onBeforeRunSuite, and adjust any explanatory
text or assertion comment accordingly so it accurately mentions
importFile/collect phase as the location where getApp is called.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
core/vitest-adapter/src/shared.ts (2)

40-44: defaultGetApp silently throws if egg-mock is not installed.

The bare require('egg-mock/bootstrap') will throw a MODULE_NOT_FOUND error that propagates as an unhandled rejection. The caller in the runner may or may not catch this gracefully. Consider wrapping in a try/catch returning undefined to match the declared return type, consistent with how the adapter is expected to bail when getApp fails.

Proposed fix
 export async function defaultGetApp(): Promise<EggMockApp | undefined> {
-  // eslint-disable-next-line `@typescript-eslint/no-var-requires`
-  const bootstrap = require('egg-mock/bootstrap');
-  return bootstrap?.app;
+  try {
+    // eslint-disable-next-line `@typescript-eslint/no-var-requires`
+    const bootstrap = require('egg-mock/bootstrap');
+    return bootstrap?.app;
+  } catch {
+    return undefined;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/vitest-adapter/src/shared.ts` around lines 40 - 44, defaultGetApp
currently does a bare require('egg-mock/bootstrap') which throws if egg-mock
isn't installed; wrap the require call in a try/catch inside defaultGetApp
(catching MODULE_NOT_FOUND and other errors) and return undefined on failure to
match the Promise<EggMockApp | undefined> signature and avoid unhandled
rejections; ensure you still return bootstrap.app when successful and keep the
existing eslint comment if needed.

46-54: Same unguarded require pattern in restoreEggMocksIfNeeded.

If egg-mock is not installed, require('egg-mock') on line 49 will throw. A try/catch would be consistent with the defensive mm?.restore check already present.

Proposed fix
 export async function restoreEggMocksIfNeeded(restoreMocks: boolean) {
   if (!restoreMocks) return;
-  // eslint-disable-next-line `@typescript-eslint/no-var-requires`
-  const eggMock = require('egg-mock');
-  const mm = eggMock?.default || eggMock;
-  if (mm?.restore) {
-    await mm.restore();
+  try {
+    // eslint-disable-next-line `@typescript-eslint/no-var-requires`
+    const eggMock = require('egg-mock');
+    const mm = eggMock?.default || eggMock;
+    if (mm?.restore) {
+      await mm.restore();
+    }
+  } catch {
+    // egg-mock not available, nothing to restore
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/vitest-adapter/src/shared.ts` around lines 46 - 54, The unguarded
require in restoreEggMocksIfNeeded (eggMock / mm) can throw if the optional
dependency isn't installed; wrap the require('egg-mock') in a try/catch inside
restoreEggMocksIfNeeded, and on catch simply return (or no-op) so execution
continues safely; then keep the existing mm?.restore check and await
mm.restore() when present. Ensure you reference eggMock and mm exactly as in the
diff so the change is applied in the right spot.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/vitest-adapter/src/runner.ts`:
- Around line 177-194: The StoreController override leaks if createHeldScope
throws: in onBeforeRunSuite (and similarly in onBeforeTryTask) call
createHeldScope inside a try/catch immediately after setActiveCtx and, on any
error, call controller.restore() to undo the monkey-patch before rethrowing the
error; ensure you only set fileScopeMap (or the analogous task map) after
successful scope creation and that the catch restores the controller and
propagates the exception so cleanup always runs even if createHeldScope rejects.
- Line 133: The override of importFile is using `source: string` and casting to
any before calling super.importFile; change the method signature to async
importFile(filepath: string, source: VitestRunnerImportSource): Promise<unknown>
and remove the `as any` cast when calling super.importFile so that you call
super.importFile(filepath, source); also add the needed import for
VitestRunnerImportSource from 'vitest' or 'vitest/runners' at the top of the
file to satisfy the type.

---

Duplicate comments:
In `@core/vitest-adapter/src/runner.ts`:
- Around line 48-69: The current createHeldScope can hang because awaiting
readyPromise blocks forever if ctx.beginModuleScope rejects before the callback
calls scopeReady; change the waiting step to race the readiness against the
scope promise so a rejection short-circuits instead of hanging. Specifically, in
createHeldScope replace the single await readyPromise with awaiting
Promise.race([readyPromise, scopePromise]) (so scopePromise rejection propagates
and prevents the deadlock), keeping scopePromise, scopeReady, gate and endScope
logic unchanged.
- Around line 119-123: Instance-level collections fileScopeMap, taskScopeMap,
fileAppMap and the warned flag are never cleared between watch-mode reruns,
causing stale state; fix by clearing these at the start (or end) of each
test-run lifecycle: add logic to reset fileScopeMap.clear(),
taskScopeMap.clear(), fileAppMap.clear() and warned = false in the runner's run
lifecycle hook (e.g., at the beginning of the overridden
run()/handleRun()/onRunStart method inside TeggVitestRunner) so each rerun
starts with a fresh state.

In `@core/vitest-adapter/test/get_app_throw.test.ts`:
- Around line 17-19: The inline comment in the test is incorrect: update the
comment in get_app_throw.test.ts to state that the runner calls config.getApp()
during importFile (collect phase) rather than onBeforeRunSuite; locate the test
block containing getApp and getAppCalls and replace the misleading text ("The
runner calls getApp in onBeforeRunSuite") with a brief accurate note referencing
importFile/collect phase (or remove the comment entirely) so it correctly
reflects where getApp is invoked.

In `@core/vitest-adapter/test/get_store_restore.test.ts`:
- Around line 101-112: The assertions inside the afterAll closure (references:
getCtx, suiteCtx, testCtxList, afterEachCtxList) may throw and prevent cleanup
calls app.close() and mm.restore() from executing; wrap the assertion block in a
try/finally where the try contains all current asserts (including the forEach
and notStrictEqual checks) and the finally always awaits app.close() and
mm.restore() so cleanup runs regardless of assertion failures.

---

Nitpick comments:
In `@core/vitest-adapter/src/shared.ts`:
- Around line 40-44: defaultGetApp currently does a bare
require('egg-mock/bootstrap') which throws if egg-mock isn't installed; wrap the
require call in a try/catch inside defaultGetApp (catching MODULE_NOT_FOUND and
other errors) and return undefined on failure to match the Promise<EggMockApp |
undefined> signature and avoid unhandled rejections; ensure you still return
bootstrap.app when successful and keep the existing eslint comment if needed.
- Around line 46-54: The unguarded require in restoreEggMocksIfNeeded (eggMock /
mm) can throw if the optional dependency isn't installed; wrap the
require('egg-mock') in a try/catch inside restoreEggMocksIfNeeded, and on catch
simply return (or no-op) so execution continues safely; then keep the existing
mm?.restore check and await mm.restore() when present. Ensure you reference
eggMock and mm exactly as in the diff so the change is applied in the right
spot.

killagu-claw and others added 6 commits February 17, 2026 23:17
- Custom VitestTestRunner using AsyncLocalStorage.enterWith() for context injection
- Each file suite gets its own mock context with held beginModuleScope
- Each test gets isolated context via onBeforeTryTask, restored after test
- Supports nested and concurrent ctxStorage.run() without conflicts
- Retry-safe: releases previous scope before creating new one
- Graceful degradation when getApp() fails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename core/vitest-adapter to core/vitest, package @eggjs/tegg-vitest-adapter to @eggjs/tegg-vitest
- Fix createHeldScope hang when beginModuleScope rejects before callback by racing readyPromise against scopePromise
- Fix watch-mode stale state by clearing fileAppMap per-file before re-collection and after suite completion
- Fix onBeforeTryTask signature to match VitestTestRunner base class
- Wrap afterAll assertions in try/finally to guarantee cleanup in tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add trailing comma in fixture_app.test.ts
- Add comment to empty arrow function in get_app_throw.test.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace `source: string` with proper `VitestRunnerImportSource` type
and remove unnecessary `as any` cast on super call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VitestRunnerImportSource is not re-exported from vitest/runners,
use Parameters<VitestTestRunner['importFile']>[1] instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@killagu killagu merged commit c853090 into eggjs:master Feb 17, 2026
10 of 11 checks passed
killagu-claw added a commit to killagu-claw/tegg that referenced this pull request Feb 19, 2026
eggjs#401)

- Rename package from @eggjs/tegg-vitest-adapter to @eggjs/tegg-vitest
- Use Promise.race for error handling in createHeldScope
- Add watch mode support (clear stale state on re-collection)
- Fix optional options parameter in onBeforeTryTask
- Update debug env var to DEBUG_TEGG_VITEST

cherry-pick from master c853090

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
killagu-claw added a commit to killagu-claw/tegg that referenced this pull request Feb 19, 2026
eggjs#401)

- Rename package from @eggjs/tegg-vitest-adapter to @eggjs/tegg-vitest
- Use Promise.race for error handling in createHeldScope
- Add watch mode support (clear stale state on re-collection)
- Fix optional options parameter in onBeforeTryTask
- Update debug env var to DEBUG_TEGG_VITEST

cherry-pick from master c853090

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
killagu added a commit that referenced this pull request Feb 19, 2026
## Summary
- Rename `core/vitest-adapter` to `core/vitest` (package
`@eggjs/tegg-vitest-adapter` → `@eggjs/tegg-vitest`) with code
improvements from master (#401)
- Upgrade CodeQL Action from v2 to v3 and actions/checkout from v3 to v4
(#405)

## Changes

**core/vitest (renamed from core/vitest-adapter):**
- Use `Promise.race` for error handling in `createHeldScope` to avoid
hanging on init failure
- Add watch mode support (clear stale state on re-collection)
- Fix optional `options` parameter in `onBeforeTryTask`
- Update debug env var to `DEBUG_TEGG_VITEST`

**CodeQL workflow:**
- `actions/checkout` v3 → v4
- `github/codeql-action/*` v2 → v3

## Test plan
- [ ] CI passes on next branch
- [ ] vitest adapter tests pass with renamed package

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: killa <killa07071201@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants