qwen-code/scripts/tests/vitest-global-setup.test.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

533 lines
19 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import { spawnSync } from 'node:child_process';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import checkUnitTestPrerequisites, {
DIST_PREREQUISITES,
GENERATED_PREREQUISITES,
aliasedSpecifiers,
checkAndReport,
findMissingPrerequisites,
formatPrerequisiteMessage,
normalizePackageKey,
} from '../vitest-global-setup.js';
const guardUrl = new URL('../vitest-global-setup.js', import.meta.url);
// Mirrors the real manifest shapes: most channel packages declare their entry
// via exports['.'].default, while acp-bridge/web-templates use exports['.'].import.
function manifestFor(rel) {
if (
rel === 'packages/acp-bridge' ||
rel === 'packages/web-templates' ||
rel === 'packages/core'
) {
return {
name: `fake-${path.basename(rel)}`,
exports: {
'.': { types: './dist/index.d.ts', import: './dist/index.js' },
},
};
}
return {
name: `fake-${path.basename(rel)}`,
exports: {
'.': { types: './dist/index.d.ts', default: './dist/index.js' },
},
};
}
function buildFixtureRoot() {
const root = mkdtempSync(path.join(tmpdir(), 'vitest-prereq-'));
for (const deps of Object.values(DIST_PREREQUISITES)) {
for (const rel of deps) {
mkdirSync(path.join(root, rel, 'dist'), { recursive: true });
writeFileSync(
path.join(root, rel, 'package.json'),
JSON.stringify(manifestFor(rel)),
);
writeFileSync(path.join(root, rel, 'dist', 'index.js'), '');
}
}
for (const files of Object.values(GENERATED_PREREQUISITES)) {
for (const rel of files) {
mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
writeFileSync(path.join(root, rel), '');
}
}
return root;
}
describe('normalizePackageKey', () => {
it('maps win32 backslash separators onto the forward-slash keys', () => {
expect(normalizePackageKey('packages\\cli')).toBe('packages/cli');
expect(normalizePackageKey('packages/cli')).toBe('packages/cli');
expect(normalizePackageKey('packages\\cli\\')).toBe('packages/cli');
});
});
describe('vitest-global-setup prerequisite guard', () => {
let root;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
vi.restoreAllMocks();
});
it('reports nothing when every prerequisite exists', () => {
root = buildFixtureRoot();
expect(findMissingPrerequisites('packages/cli', root)).toEqual([]);
expect(findMissingPrerequisites('packages/core', root)).toEqual([]);
});
it('accepts a win32-style relative key', () => {
root = buildFixtureRoot();
expect(findMissingPrerequisites('packages\\cli', root)).toEqual([]);
});
it('reports an unbuilt workspace package and a missing generated file', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/channels/base/dist/index.js'));
rmSync(path.join(root, 'packages/cli/src/generated/git-commit.ts'));
const missing = findMissingPrerequisites('packages/cli', root);
expect(missing).toHaveLength(2);
expect(missing[0]).toContain('packages/channels/base');
expect(missing[0]).toContain('has not been built');
expect(missing[1]).toContain('packages/cli/src/generated/git-commit.ts');
});
it('reports a missing package directory instead of throwing', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/web-templates'), { recursive: true });
const missing = findMissingPrerequisites('packages/cli', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('packages/web-templates');
expect(missing[0]).toContain('missing/unreadable');
});
it('skips packages without known prerequisites', () => {
root = buildFixtureRoot();
expect(findMissingPrerequisites('packages/web-shell', root)).toEqual([]);
});
it('reports an unbuilt packages/core dist for core tests', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/core/dist/index.js'));
const missing = findMissingPrerequisites('packages/core', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('packages/core');
expect(missing[0]).toContain('has not been built');
});
it('names the fix command and the git-commit remedy only when applicable', () => {
const withGenerated = formatPrerequisiteMessage([
' - packages/cli/src/generated/git-commit.ts: generated file does not exist',
]);
expect(withGenerated).toContain('npm run build');
expect(withGenerated).toContain('npm run generate');
const distOnly = formatPrerequisiteMessage([
' - packages/x: workspace package "x" has not been built (missing packages/x/dist/index.js)',
]);
expect(distOnly).toContain('npm run build');
expect(distOnly).not.toContain('npm run generate');
});
it('flags a stale probe line with its own remedy note', () => {
const message = formatPrerequisiteMessage([
' - packages/x: package.json exposes no dist/ entry files to check (guard probe may be stale)',
]);
expect(message).toContain('npm run build');
expect(message).toContain('guard itself');
expect(message).toContain('will not clear it');
});
});
describe('subpath exports entries are probed', () => {
let root;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
});
it('probes main entries spelled without a leading ./', () => {
root = buildFixtureRoot();
writeFileSync(
path.join(root, 'packages/core', 'package.json'),
JSON.stringify({ name: 'fake-core', main: 'dist/index.js' }),
);
rmSync(path.join(root, 'packages/core/dist/index.js'));
const missing = findMissingPrerequisites('packages/core', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('has not been built');
});
it('skips wildcard pattern entries instead of treating them as files', () => {
root = buildFixtureRoot();
writeFileSync(
path.join(root, 'packages/core', 'package.json'),
JSON.stringify({
name: 'fake-core',
exports: {
'.': { import: './dist/index.js' },
'./dist/*': './dist/*',
'./src/*': './src/*',
},
}),
);
expect(findMissingPrerequisites('packages/core', root)).toEqual([]);
});
it('flags a missing subpath dist file even when the . entry exists', () => {
root = buildFixtureRoot();
const bridgeDir = path.join(root, 'packages/acp-bridge');
writeFileSync(
path.join(bridgeDir, 'package.json'),
JSON.stringify({
name: 'fake-acp-bridge',
exports: {
'.': { import: './dist/index.js' },
'./sessionRestoreTimeout': {
import: './dist/session-restore-timeout.js',
},
},
}),
);
// dist/index.js exists; the subpath target does not.
const missing = findMissingPrerequisites('packages/cli', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('session-restore-timeout.js');
});
it('does not probe dist targets the consumer aliases to source', () => {
root = buildFixtureRoot();
const bridgeDir = path.join(root, 'packages/acp-bridge');
writeFileSync(
path.join(bridgeDir, 'package.json'),
JSON.stringify({
name: 'fake-acp-bridge',
exports: {
'.': { import: './dist/index.js' },
'./aliasedSubpath': { import: './dist/aliased-subpath.js' },
'./plainSubpath': { import: './dist/plain-subpath.js' },
},
}),
);
writeFileSync(
path.join(root, 'packages/cli', 'vitest.config.ts'),
[
"import { defineConfig } from 'vitest/config';",
'export default defineConfig({',
' resolve: {',
' alias: {',
" 'fake-acp-bridge/aliasedSubpath': '../acp-bridge/src/aliased-subpath.ts',",
' },',
' },',
'});',
'',
].join('\n'),
);
// Both subpath dist files are missing, but only the unaliased one may
// block: the aliased specifier resolves from source during collection.
const missing = findMissingPrerequisites('packages/cli', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('plain-subpath.js');
expect(missing[0]).not.toContain('aliased-subpath.js');
});
it('reads the consumer alias set from its vitest config', () => {
root = buildFixtureRoot();
const configPath = path.join(root, 'packages/cli', 'vitest.config.ts');
writeFileSync(
configPath,
[
"alias: { '@qwen-code/acp-bridge/status': 'x', '@qwen-code/sdk/daemon': 'y' }",
"// '@qwen-code/acp-bridge/commented': 'x'",
"/* '@qwen-code/acp-bridge/blocked': 'x' */",
].join('\n'),
);
const aliases = aliasedSpecifiers(configPath);
expect(aliases.has('@qwen-code/acp-bridge/status')).toBe(true);
expect(aliases.has('@qwen-code/sdk/daemon')).toBe(true);
expect(aliases.has('@qwen-code/acp-bridge/commented')).toBe(false);
expect(aliases.has('@qwen-code/acp-bridge/blocked')).toBe(false);
// A missing config yields an empty set rather than throwing.
expect(aliasedSpecifiers(path.join(root, 'nope.ts'))).toEqual(new Set());
});
});
describe('checkAndReport', () => {
let root;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
vi.restoreAllMocks();
});
it('returns 0 and prints nothing when prerequisites are satisfied', () => {
root = buildFixtureRoot();
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const cwd = path.join(root, 'packages/cli');
expect(checkAndReport({ cwd, root })).toBe(0);
expect(errSpy).not.toHaveBeenCalled();
});
it('returns 1 and prints the actionable message when prerequisites are missing', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/channels/base/dist/index.js'));
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const cwd = path.join(root, 'packages/cli');
expect(checkAndReport({ cwd, root })).toBe(1);
expect(errSpy).toHaveBeenCalledTimes(1);
const printed = errSpy.mock.calls[0][0];
expect(printed).toContain('npm run build');
expect(printed).toContain('packages/channels/base');
});
it('yields silently when cwd matches the repo root (no known package key)', () => {
root = buildFixtureRoot();
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// cwd equal to the repo root yields no known package key, so the guard
// yields silently by design.
expect(checkAndReport({ cwd: root, root })).toBe(0);
expect(errSpy).not.toHaveBeenCalled();
});
});
function registryChannelEntries() {
const registryPath = fileURLToPath(
new URL(
'../../packages/cli/src/commands/channel/channel-registry.ts',
import.meta.url,
),
);
const source = readFileSync(registryPath, 'utf8');
// npm package names may contain digits, underscores and dots; the class
// must tolerate them or a future builtin channel silently escapes this
// drift check.
const specifiers = [
...source.matchAll(/import\('@qwen-code\/(channel-[a-z0-9._-]+)'\)/g),
].map((match) => match[1]);
return specifiers.map(
(name) => `packages/channels/${name.replace('channel-', '')}`,
);
}
describe('DIST_PREREQUISITES stays in sync with channel-registry', () => {
it('every builtin channel dynamically imported by channel-registry.ts is listed', () => {
const entries = registryChannelEntries();
expect(entries.length).toBeGreaterThan(0);
const listed = DIST_PREREQUISITES['packages/cli'];
for (const entry of entries) {
expect(listed, `missing prerequisite entry for ${entry}`).toContain(
entry,
);
}
});
it('every listed packages/channels/* entry maps back to a registry import', () => {
const imported = new Set(registryChannelEntries());
// channel-base is a build dependency of the channel packages even though
// the registry never imports it directly.
imported.add('packages/channels/base');
for (const entry of DIST_PREREQUISITES['packages/cli']) {
if (!entry.startsWith('packages/channels/')) continue;
expect(
imported,
`stale prerequisite entry ${entry}: no matching channel-registry import`,
).toContain(entry);
}
});
});
describe('vitest configs stay wired to the guard', () => {
it.each(['packages/cli', 'packages/core'])(
'%s/vitest.config.ts wires the globalSetup guard',
(pkg) => {
const configPath = fileURLToPath(
new URL(`../../${pkg}/vitest.config.ts`, import.meta.url),
);
const source = readFileSync(configPath, 'utf8');
// The guard must be anchored to the config file via path.resolve so it
// loads regardless of vitest's root/cwd; a bare relative string would
// only resolve when vitest runs from inside the package directory.
// Checked as two substrings so the assertion is robust to prettier's
// line-wrapping of the path.resolve(...) call.
expect(source).toContain('globalSetup: path.resolve(');
expect(source).toContain("'../../scripts/vitest-global-setup.js'");
},
);
});
describe('guard probe edge cases (round 3)', () => {
let root;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
vi.restoreAllMocks();
});
it('reports a package whose manifest exposes no dist entry to probe', () => {
root = buildFixtureRoot();
writeFileSync(
path.join(root, 'packages/core', 'package.json'),
JSON.stringify({
name: 'fake-core',
exports: { '.': { require: './dist/index.cjs' } },
}),
);
const missing = findMissingPrerequisites('packages/core', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('exposes no dist/ entry files to check');
});
it('distinguishes a partial build from an unbuilt package', () => {
root = buildFixtureRoot();
writeFileSync(
path.join(root, 'packages/core', 'package.json'),
JSON.stringify({
name: 'fake-core',
exports: {
'.': { import: './dist/index.js' },
'./stale': { import: './dist/stale.js' },
},
}),
);
const missing = findMissingPrerequisites('packages/core', root);
expect(missing).toHaveLength(1);
expect(missing[0]).toContain('build output is');
expect(missing[0]).toContain('stale.js');
expect(missing[0]).toContain("check the package's exports entries");
expect(missing[0]).not.toContain('has not been built');
});
it('derives the package key when cwd reaches the root through a symlink', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/channels/base/dist/index.js'));
const link = `${root}-link`;
symlinkSync(root, link);
try {
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(
checkAndReport({ cwd: path.join(link, 'packages/cli'), root }),
).toBe(1);
expect(errSpy.mock.calls[0][0]).toContain('packages/channels/base');
} finally {
rmSync(link);
}
});
});
// The default export is what vitest actually invokes. These tests drive it
// against a hermetic fixture checkout (via QWEN_VITEST_GUARD_ROOT) instead of
// the real repository state, so they hold on an unbuilt worktree too — the
// exact scenario the guard targets.
describe('default export (the vitest globalSetup entry)', () => {
let root;
afterEach(() => {
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
delete process.env['QWEN_VITEST_GUARD_ROOT'];
vi.restoreAllMocks();
});
it('uses the vitest project root and exits 1 when prerequisites are missing', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/channels/base/dist/index.js'));
process.env['QWEN_VITEST_GUARD_ROOT'] = root;
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined);
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
checkUnitTestPrerequisites({
config: { root: path.join(root, 'packages/cli') },
});
expect(exitSpy).toHaveBeenCalledWith(1);
expect(errSpy.mock.calls[0][0]).toContain('packages/channels/base');
});
it('does not exit when the hermetic checkout is ready', () => {
root = buildFixtureRoot();
process.env['QWEN_VITEST_GUARD_ROOT'] = root;
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined);
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
checkUnitTestPrerequisites({
config: { root: path.join(root, 'packages/cli') },
});
expect(exitSpy).not.toHaveBeenCalled();
expect(errSpy).not.toHaveBeenCalled();
});
it('falls back to process.cwd() when invoked without a project', () => {
root = buildFixtureRoot();
process.env['QWEN_VITEST_GUARD_ROOT'] = root;
vi.spyOn(process, 'cwd').mockReturnValue(path.join(root, 'packages/cli'));
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined);
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
checkUnitTestPrerequisites(undefined);
expect(exitSpy).not.toHaveBeenCalled();
expect(errSpy).not.toHaveBeenCalled();
});
// Subprocess-level coverage: a real `node` process runs the default export
// end to end, so neutralizing `process.exit` or breaking the root
// derivation flips the observable exit status.
function runGuardSubprocess(cwd) {
return spawnSync(
process.execPath,
[
'--input-type=module',
'-e',
`import guard from ${JSON.stringify(guardUrl.href)}; guard();`,
],
{
cwd,
encoding: 'utf8',
env: { ...process.env, QWEN_VITEST_GUARD_ROOT: root },
},
);
}
it('subprocess: exits 1 and prints the fix command on a broken checkout', () => {
root = buildFixtureRoot();
rmSync(path.join(root, 'packages/channels/base/dist/index.js'));
const res = runGuardSubprocess(path.join(root, 'packages/cli'));
expect(res.status).toBe(1);
expect(res.stderr).toContain('npm run build');
expect(res.stderr).toContain('packages/channels/base');
});
it('subprocess: exits 0 silently on a ready checkout', () => {
root = buildFixtureRoot();
const res = runGuardSubprocess(path.join(root, 'packages/cli'));
expect(res.status).toBe(0);
expect(res.stderr).toBe('');
});
});