qwen-code/scripts/build.js
易良 2785480685
fix(devx): fail with actionable message when unit-test build prerequisites are missing (#9149) (#9171)
* fix(devx): fail with actionable message when unit-test build prerequisites are missing (#9149)

Package-local unit tests in packages/cli import workspace packages
(acp-bridge, sdk, web-templates, channels/*) through their built dist/
output plus the generated git-commit.ts. In a fresh worktree that shares
the main checkout's node_modules, or after a deep clean, those artifacts
do not exist and vitest fails during collection with resolution errors
that blame the wrong thing.

Add a vitest globalSetup guard that checks the prerequisites up front and
exits with a message naming every missing piece and the command that fixes
it (npm run build). Document the prerequisite in the AGENTS.md unit-testing
section.

* fix(devx): address R1 review findings on the unit-test prerequisite guard

- normalize win32 path separators so the guard works on Windows (R1-1)
- use Copyright 2026 Qwen Team header (R1-2)
- drop over-included packages/sdk-typescript; every sdk import in the cli
  test graph is an aliased /daemon* subpath (R1-3)
- add a sync assertion test: every builtin channel dynamically imported by
  channel-registry.ts must stay listed in DIST_PREREQUISITES (R1-4)
- cover the vitest-invoked entry point via exported checkAndReport (R1-6)
- mirror real manifest shapes (exports.default / import variants) in the
  test fixtures (R1-7)
- report missing/unreadable package manifests through the normal exit path
  instead of crashing with a raw stack trace (R1-8)
- derive the package key from vitest-s resolved project root, so
  vitest run --root packages/cli from elsewhere is covered (R1-9)
- probe every exports entry targeting dist/, not only the . entry, so
  missing unaliased subpath builds are reported too (R2-1)

* fix(devx): extend the unit-test prerequisite guard to packages/core

Issue #9149's scope names packages/cli AND packages/core, but the guard
only covered cli: eight core test files (providers/__tests__/presets/*,
provider-config.test.ts) import the bare '@qwen-code/qwen-code-core'
specifier, which resolves through the package's own exports to
dist/index.js — on a fresh checkout 'cd packages/core && npx vitest run
src/path/to/file.test.ts' (the AGENTS.md-documented command) still died
with the opaque 'Failed to resolve entry for package' error.

- Add 'packages/core': ['packages/core'] to DIST_PREREQUISITES
- Wire the same globalSetup guard into packages/core/vitest.config.ts
- Skip wildcard pattern exports entries ('./dist/*') in distEntryFiles:
  core's manifest carries them and they name no individual file —
  probing them literally would block core test runs even fully built
- Generalize the fixture builder to every DIST/GENERATED_PREREQUISITES
  entry, add coverage for the core dist requirement and the wildcard
  skip, and move the 'no known prerequisites' example off packages/core
- Note the core self-import in AGENTS.md

Probe-verified both arms at this commit: dist moved aside -> the guard
prints the actionable message and stops the run; dist restored -> the
test file passes (11/11).

* fix(devx): harden the prerequisite probe and its drift tests

- Probe manifest 'main' entries spelled without a leading './': all
  guarded manifests use "main": "dist/index.js", which the old
  startsWith('./dist/') predicate never matched, so the documented main
  probe silently collected nothing. Normalize before the prefix check.
- Tolerate digits in builtin channel names in the sync test's registry
  regex ('channel-[a-z0-9-]+'), or a future channel with a digit in its
  npm name escapes the drift check.
- Add the reverse sync assertion: every listed packages/channels/*
  prerequisite must map back to a channel-registry import (channel-base
  excepted as the channels' build dependency), so removing a builtin
  channel cannot leave a stale entry that hard-blocks cli test runs
  with a misleading 'fresh checkout' message.
- Cover the main-entry probe with a fixture test.

* fix(devx): fail loud on stale probes, align key derivation under symlinks

Round-3 review findings on the prerequisite guard:

- R3-1: a listed package whose manifest enumerates zero ./dist/ targets
  (require-only or nested-condition entries) passed the probe silently —
  report 'exposes no dist/ entry files to check (guard probe may be
  stale)' instead, so a stale probe cannot resurrect the raw resolution
  error this guard exists to replace.
- R3-3: when SOME dist entry files exist, the missing one is no longer
  diagnosed as 'has not been built' + a plain npm-run-build prescription
  (a successful build can legitimately leave a stale exports entry); the
  message now says the build output is incomplete or exports points at a
  file the build does not emit, and to check the package's exports
  entries when rebuilding does not help.
- Key derivation now realpaths both cwd and root (with a fallback to the
  raw path): repoRoot descends from import.meta.url, which Node resolves
  through symlinks, while vitest resolves root with a plain path.resolve
  — comparing them raw let a symlinked ancestor silently disable the
  guard.
- R3-2: the channel drift-check character class now tolerates digits,
  underscores and dots per npm naming rules.
- R3-4: renamed the test that claimed win32-separator coverage it never
  exercised; its body is the degenerate repo-root silent-yield path and
  the comment now says so.
- R1-6 (partial): added default-export coverage — project.config.root
  extraction and the process.cwd() fallback, asserting no exit in a
  built repo. The exit-1 arm of the default export stays untested: it
  needs a root-injectable seam the entry point deliberately does not
  have; checkAndReport's return-1 and message remain covered directly.

Tests: 21/21; mutation probes confirm the zero-enumeration and symlink
tests catch their regressions.

* fix(devx): hermetic guard tests, alias-aware probe, explicit gitlab build

- drive the default-export tests against a hermetic fixture checkout via
  QWEN_VITEST_GUARD_ROOT (in-process and subprocess), so they hold on an
  unbuilt worktree instead of depending on the real repository state
- skip dist targets the consumer aliases to TypeScript source when probing,
  so a missing-but-aliased dist file no longer blocks runs that would pass
- make the remedy message context-aware: the git-commit hint appears only
  when a generated file is missing, and a stale-probe line gets its own note
- add packages/channels/gitlab to buildOrder: it is a cli channel-registry
  builtin like its siblings and used to build only transitively
- add drift tests pinning the globalSetup wiring in both vitest configs

* fix(devx): ignore commented vitest aliases

* fix(devx): anchor the vitest globalSetup guard to the config file

R1-1: a relative globalSetup path is resolved against vitest's root (the
process cwd without --root), not the config file's directory, so the
prerequisite guard only loaded when vitest happened to run from inside the
package. `npx vitest run --config packages/<pkg>/vitest.config.ts` from the
repository root died with "Cannot find module .../vitest-global-setup.js"
before any test — the cause-hiding failure class this guard replaces.
Resolve it with path.resolve(__dirname, ...) in both packages/cli and
packages/core configs, and update the wiring-sync assertion (now robust to
prettier line-wrapping; flip-verified red when reverted to a bare string).

---------

Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-08-18 13:19:09 +00:00

118 lines
3.9 KiB
JavaScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
// npm install if node_modules was removed (e.g. via npm run clean or scripts/clean.js)
if (!existsSync(join(root, 'node_modules'))) {
execSync('npm install', { stdio: 'inherit', cwd: root });
}
// build all workspaces/packages in dependency order
execSync('npm run generate', { stdio: 'inherit', cwd: root });
// --cli-only: skip packages not needed by the CLI bundle
// (webui, web-shell, vscode-ide-companion are for IDE/web use only)
const cliOnly = process.argv.includes('--cli-only');
// Build in dependency order:
// 1. core (foundation package, includes test-utils)
// 2. web-templates (embeddable web templates - used by cli)
// 3. channel-base (base channel infrastructure - used by channel adapters and cli)
// 4. channel adapters (depend on channel-base)
// 5. audio-capture (native microphone backend used by cli)
// 6. acp-bridge (depends on core - used by cli)
// 7. sdk (build-time devDep on acp-bridge for shared constants, used by cli channel worker)
// 8. cli (depends on core, acp-bridge, web-templates, channel packages, sdk)
// 9. webui (shared UI components - used by vscode companion)
// 10. web-shell (depends on webui and sdk)
// 11. vscode-ide-companion (depends on webui)
// 12. external-context (private Qwen extension)
const buildOrder = [
'packages/core',
'packages/web-templates',
'packages/channels/base',
'packages/channels/telegram',
'packages/channels/weixin',
'packages/channels/dingtalk',
'packages/channels/wecom',
'packages/channels/feishu',
'packages/channels/qqbot',
'packages/channels/github',
// gitlab is a builtin of the cli channel registry like its siblings; it
// used to build only transitively via cli's tsconfig project reference.
'packages/channels/gitlab',
'packages/channels/plugin-example',
'packages/audio-capture',
'packages/acp-bridge',
'packages/sdk-typescript',
'packages/cli',
...(cliOnly
? []
: [
'packages/webui',
'packages/web-shell',
'packages/vscode-ide-companion',
'packages/chrome-extension',
'integrations/external-context',
]),
];
for (const workspace of buildOrder) {
const command =
workspace === 'packages/audio-capture'
? `npm run build:ts --workspace=${workspace}`
: `npm run build --workspace=${workspace}`;
execSync(command, { stdio: 'inherit', cwd: root });
// After cli is built, generate the JSON Schema for settings
// so the vscode-ide-companion extension can provide IntelliSense
if (workspace === 'packages/cli') {
execSync('node --import tsx/esm scripts/generate-settings-schema.ts', {
stdio: 'inherit',
cwd: root,
});
}
}
// also build container image if sandboxing is enabled
// skip (-s) npm install + build since we did that above
try {
execSync('node scripts/sandbox_command.js -q', {
stdio: 'inherit',
cwd: root,
});
if (
process.env.BUILD_SANDBOX === '1' ||
process.env.BUILD_SANDBOX === 'true'
) {
execSync('node scripts/build_sandbox.js -s', {
stdio: 'inherit',
cwd: root,
});
}
} catch {
// ignore
}