qwen-code/scripts/workspaces.js
易良 5ec29581a5
ci: give every workspace the shared-pool test timeout, not just five (#10915)
* ci: give every workspace the shared-pool test timeout, not just five

`npm run test:ci --workspaces` runs 22 vitest projects and they all land on the
same shared ECS pool, where an identical suite takes about 5x longer depending
only on which host it draws (#10490). Five of those configs were given a raised
ceiling there, one at a time, as each package's flakiness became somebody's
problem: cli, core, acp-bridge, web-shell and vscode-ide-companion. Three more
carry a generous flat value (node-repl 60s, sdk-typescript E2E_TIMEOUT_MINUTES,
scripts/tests 90s).

The remaining fifteen were still on vitest's 5000 ms default: all eleven
channels packages, audio-capture, chrome-extension, webui and both
external-context integrations. That is 99 test files and roughly 3170 cases
whose only timeout on a contended host was 5s.

The workaround pattern was already visible in the tree — four tests had been
hand-patched past the default individually, each in its own commit:

    integrations/external-context/src/auto-recall.test.ts           12s
    integrations/external-context/src/provider-extension-local.ts   20s, 30s
    packages/channels/base/src/ChannelBase.test.ts                   8s

Every one of those is a case that needed more than 5s, found the hard way. This
raises the ceiling once, at the lane, instead of one test at a time.

Uses the `? 60_000 : undefined` form that acp-bridge and web-shell already use
rather than cli/core's `? 60_000 : 15_000`, so the change is confined to the
pool: off it these configs keep vitest's own default and a genuine hang on a
developer machine still fails in 5s, not 60s. 60_000 is the value the five
already-raised configs settled on.

The parity sweep in scripts/tests/unit-vitest-configs.test.ts now covers all 22
configs plus webui's function-form vite.config.ts, so a new workspace cannot
quietly join the lane on the 5s default. It asserts a floor rather than
equality, which lets scripts/tests (90s) and sdk-typescript (180s) keep their
own numbers, and a companion case pins that the off-pool branch stays
`undefined`. Verified both ways: the sweep reports 60000 for every config under
an `ecs-qwen-*` stub, `undefined` off it, and fails when the ceiling is removed
from any single config.

No test, fixture or production code changes; a test can only get more time
than before, and only on the pool.

Prerequisite for quarantining the seven raw wall-clock budgets that live in
these packages (#10912): channels/base and channels/dingtalk guard quadratic
stalls at 1000 ms whose measured regression cost is ~4.1s, so under the old 5s
ceiling there was no room to relax those bounds for a contended host at all.

* fix(ci): close the shared-pool timeout pin's own coverage holes

The review found four ways the pin could certify a green the lane does
not actually have. Close each one in the pin itself:

- packages/qwen-live defines test:ci and already runs on the pool lane,
  but was absent from the swept map; register it (its 60s ceiling is
  unconditional, so it stays out of the off-pool sample).
- Run every on-pool assertion under two runner names sharing only the
  documented ecs-qwen- prefix, so a gate narrowed past the prefix fails
  instead of passing on the stub's own name.
- Stub QWEN_SCRIPTS_TEST_TIMEOUT_MS and E2E_TIMEOUT_MINUTES alongside
  RUNNER_NAME: scripts/tests and sdk-typescript derive their ceiling
  from those knobs, so the pin must test the configs' logic, not the
  ambient environment. The scripts-suite knob test's unset arm now
  deletes the variable for real (stubEnv undefined) instead of relying
  on the ambient value being absent.
- Extend the off-pool sample to every gated ternary (all fourteen this
  PR gates plus acp-bridge/web-shell, which carried the shape already,
  plus the webui function form), so a flat 60s in any of them fails.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtlz4ubdgq

* fix(ci): pin the unset-RUNNER_NAME state and correct the pin's comment

Add an unset-state arm to the shared-pool timeout pin: the off-pool test
only stubbed a foreign RUNNER_NAME, so a ternary treating a missing
variable as pool (`?? true`, `=== undefined || startsWith(...)`) passed
every test while developer machines — which run with RUNNER_NAME unset —
silently moved to the 60s ceiling. The new arm deletes the variable via
`vi.stubEnv(..., undefined)` and samples every gated ternary plus the
webui function form. Mutation check: channels/base mutated to `?? true`
fails only the new arm; restored, the suite is green.

Reword the comment above configModules: the pin does not replace the
hand-patched per-test timeouts (auto-recall 12s, provider-extension-local
20s/30s, ChannelBase 8s). A per-test timeout outranks the config-level
field and stays load-bearing off the pool, so the pin guards the
config-level ceiling only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtme4yqxhl

* test(ci): pin the shared-pool guard's own coverage instead of sampling it

Three holes in the guard added for the #10490 flake class, all in the guard
rather than in any config:

cli and core were kept out of the off-pool sample on the stated grounds that
they pin a lower value, but nothing asserted that value — flattening either
ternary to the pool number passed the whole file, and a real hang off CI would
have surfaced at 60s instead of 15s. Both off-pool arms now assert 15s.

Those two arms also carried verbatim copies of the same 16-entry sample, so a
newly gated workspace had to be registered in three places, and missing one
left it with a pool floor but no off-pool assertion. One shared list and one
helper now back both arms.

configModules was hand-maintained with nothing cross-checking it against the
workspace set, while `npm run test:ci --workspaces` runs whatever the root glob
resolves — so a new workspace with a test:ci script could join the lane on
vitest's 5s default with the guard green. The map is now checked against the
repo's own workspace resolver, one-directionally.

* refactor(ci): share the test:ci workspace selector between both suites

The shared-pool timeout cross-check re-implemented the test:ci workspace
selector that release-workflow.test.js already carried as
getTestCiWorkspaces: same getWorkspacePackageJsonPaths root glob, same
scripts?.['test:ci'] filter. Two copies of the selection could drift
and let the two suites pin different workspace sets.

Extract the selection once into scripts/workspaces.js as
getTestCiWorkspacePaths(root) and consume it from both tests; the
release-workflow site keeps only a pair adapter attaching the parsed
package.json its assertions read. Pure extraction with no behavior
change: both selectors resolved the same 23 workspaces before and
after.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtmxfessij

* refactor(ci): rename selector to getTestCiWorkspacePackageJsonPaths

The selector returns workspace package.json paths, so carry the same
PackageJson qualifier as its sibling getWorkspacePackageJsonPaths
instead of making every caller privately know the entries end in
/package.json. Updates both consumers added in this PR.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtnaadh9j8

* test(scripts): pin the test:ci workspace selector directly

Every consumer derives its workspace set from the selector, so a
filter mutation that drops a subset stays invisible to all of them.
Add an exact-set fixture pin next to the getWorkspacePackageJsonPaths
tests, with the with-ci workspace outside packages/ to mirror the
integrations/* layout. Verified red against both an over-inclusion
mutant (filter -> true) and a partial-drop mutant.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtnaadh9j8

* test(scripts): make the selector pin and off-pool arms witness-bearing

Addresses the two low-severity coverage findings from the sandboxed
verify report (comment 5548150043), both measured open at this head:

- Finding A: the exact-set pin mirrored only the flat integrations/*
  layout, so a filter mutation dropping the nested packages/channels/*
  subtree stayed invisible to all three selector consumers (mutant S3
  survived 105/105). Mirror the nested layout in the fixture too:
  explicit packages/channels/* entries in the root workspaces list
  (packages/* does not match nested directories), one member with a
  test:ci script and one without. S3 now fails the pin, naming it;
  S1/S2/S4 stay caught.
- Finding B: OFF_POOL_DEFAULT and OFF_POOL_PINNED are hand-maintained
  and nothing cross-checked them, so dropping a name silently removed
  its off-pool assertion and a flattened ternary still passed the
  on-pool floor (mutants G5/G6 survived). Add a partition-
  exhaustiveness pin: every configModules entry must sit in exactly
  one off-pool arm and every arm entry must exist in the map. Both
  mutants now fail it.

No production logic changed; pins and fixtures only.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtnn5c4cjw

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-09-07 09:02:39 +00:00

50 lines
1.3 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { globSync } from 'glob';
function toGlobPath(filePath) {
return filePath.replaceAll('\\', '/');
}
export function getWorkspacePackageJsonPaths(root, workspaces) {
const packageJsonPaths = new Set();
for (const workspace of workspaces) {
const isExcluded = workspace.startsWith('!');
const pattern = toGlobPath(isExcluded ? workspace.slice(1) : workspace);
const matches = globSync(`${pattern}/package.json`, { cwd: root });
for (const match of matches) {
const packageJsonPath = toGlobPath(match);
if (isExcluded) {
packageJsonPaths.delete(packageJsonPath);
} else {
packageJsonPaths.add(packageJsonPath);
}
}
}
return [...packageJsonPaths].sort();
}
export function getTestCiWorkspacePackageJsonPaths(root) {
const { workspaces } = JSON.parse(
readFileSync(join(root, 'package.json'), 'utf8'),
);
return getWorkspacePackageJsonPaths(root, workspaces).filter(
(packageJsonPath) => {
const { scripts } = JSON.parse(
readFileSync(join(root, packageJsonPath), 'utf8'),
);
return Boolean(scripts?.['test:ci']);
},
);
}