qwen-code/scripts/tests/no-utils-upward-import.test.js
易良 56db17bd4c
refactor(cli): enforce utils leaf-layer dependency direction (#9146) (#9737)
* refactor(cli): enforce utils leaf-layer dependency direction (#9146)

Move domain-coupled modules out of packages/cli/src/utils into the
directories that own them: config/ (dialogScopeUtils, settingsUtils),
i18n/ (languageUtils), ui/ (handleAutoUpdate, standalone-update,
systemInfo, systemInfoFields, update-relaunch, commands, doctorChecks),
nonInteractive/ (nonInteractiveHelpers, chat-recording-failure,
tool-result-boundary-diagnostics, permission-suggestions), serve/
(sandbox), services/housekeeping/ (scheduler, non-interactive-scheduler),
and commands/review/ (findings).

Extract the generic normalizePartList helper into
utils/normalize-part-list.ts so utils consumers keep importing downward,
and move the MergeStrategy enum into utils/deepMerge.ts (its owner).

Add an eslint architecture rule (no-utils-upward-import) that forbids
value imports from utils/ back up into a domain directory. Type-only
imports stay exempt: they are erased at compile time and cannot create a
runtime cycle (Settings in modelConfigUtils, CommandContext in
sessionPaths).

No behavior change: typecheck, build, and the affected unit tests pass.

* fix: use Qwen Team 2026 license header on new files (#9146)

* chore: refresh stale utils/ path references after leaf-layer move (#9146)

* docs: reconcile no-utils-upward-import header with the allowed type-only set (#9146)

* fix(cli): allowlist sandbox process.env accesses after leaf-layer move (#9146)

* chore(ci): re-record qwen-autofix.yml size baseline after #9677 (#9146)

#9677 recorded qwen-autofix.yml at 392111 bytes while the file it
committed was already 397656, so every PR that merged main after it
tripped the growth ratchet. Re-record the actual size; the file itself
is unchanged by this PR.

* fix(review): drop the stale utils/findings.ts digest root after the leaf-layer move (#9146)

The #9146 move returned findings.ts to commands/review/, but the digest
root lists merged from main still pinned it under utils/, where the file
no longer exists — the absent root darkened every review's staleness
check and failed review-source-digest.test.ts. Drop the stale file-shaped
root from both digest copies and their pins; the commands/review/
directory root covers the validator at its new home, and the two utils
helpers keep their file-shaped roots.

* fix(review): colocate seatbelt profiles with the sandbox module (#9146)

* fix(review): exempt inline type-only specifiers from the utils upward-import rule (#9146)

* fix(review): report upward inline type-specifier imports under verbatimModuleSyntax (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): pin mixed-specifier and zero-specifier upward imports in the utils rule (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): anchor the nested-checkout utils rule fixture on the last marker (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(review): pin that the utils/findings.ts digest root stays removed (#9146)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(review): reword stale-bundle SCOPE header to the post-move helper shape (#9146)

* test(review): drop the pre-move utils/findings.ts from the skill-parity fixture (#9146)

* test(serve): derive the seatbelt colocation tripwire from BUILTIN_SEATBELT_PROFILES (#9146)

* fix(architecture): fail closed on computed dynamic imports in the utils leaf rule (#9146)

* fix(cli): point settings.test.ts at the post-move settingsUtils path (#9146)

main updated settings.test.ts after this branch moved settingsUtils.ts
from utils/ into config/, and the merge kept main's old import
specifier, which vite fails to resolve. Repoint it at ./settingsUtils.js;
every other consumer already uses the new path.

* fix(cli): close utils boundary review gaps

* test(cli): cover utils boundary allow paths

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-23 14:41:49 +00:00

178 lines
6.1 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { Linter } from 'eslint';
import rule from '../../eslint-rules/no-utils-upward-import.js';
function runRule(code, filename) {
const linter = new Linter({ configType: 'eslintrc' });
linter.defineRule('architecture/no-utils-upward-import', rule);
return linter.verify(
code,
{
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
rules: { 'architecture/no-utils-upward-import': 'error' },
},
{ filename },
);
}
describe('no-utils-upward-import', () => {
it.each([
['packages/cli/src/utils/deepMerge.ts', '../config/settings.js'],
[
'packages/cli/src/utils/housekeeping/cleanup.ts',
'../../config/settings.js',
],
['packages/cli/src/utils/foo.ts', '../ui/commands/types.js'],
['packages/cli/src/utils/foo.ts', '../i18n/index.js'],
['packages/cli/src/utils/foo.ts', 'src/config/settings.js'],
[
'packages/cli/src/utils/foo.ts',
'../nonInteractive/nonInteractiveHelpers.js',
],
// nested checkout: the marker appears twice, so the utils root must be
// anchored on the LAST one — an indexOf anchor would resolve the import
// inside the outer utils root and report nothing
[
'/tmp/packages/cli/src/utils/nested/packages/cli/src/utils/foo.ts',
'../config/settings.js',
],
])('rejects upward imports from %s', (filename, importedPath) => {
expect(
runRule(`import value from '${importedPath}';`, filename),
).toHaveLength(1);
});
it('rejects export and dynamic upward sources', () => {
const file = 'packages/cli/src/utils/foo.ts';
expect(
runRule("export { value } from '../config/settings.js';", file),
).toHaveLength(1);
// a zero-specifier re-export is a runtime edge (`export {} from` survives
// emission), not a vacuous `[].every(...)` type-only exemption
expect(
runRule("export {} from '../config/settings.js';", file),
).toHaveLength(1);
expect(
runRule("export * from '../config/settings.js';", file),
).toHaveLength(1);
expect(runRule("import('../config/settings.js');", file)).toHaveLength(1);
expect(runRule('import(`../config/settings.js`);', file)).toHaveLength(1);
expect(runRule("import('src/config/settings.js');", file)).toHaveLength(1);
});
it('fails closed on computed dynamic sources with a local known prefix', () => {
const file = 'packages/cli/src/utils/foo.ts';
// A multi-segment template: interpolation can contribute a `../` step,
// and a leading `../` cannot be undone, so the import cannot be proven
// to stay inside utils/ — reported even though part is interpolated.
expect(runRule('import(`../i18n/${locale}.js`);', file)).toHaveLength(1);
// A `+` concatenation with a relative leftmost literal, same reasoning.
expect(runRule("import('../config/' + name + '.js');", file)).toHaveLength(
1,
);
// A relative `./` prefix is also unprovable (interpolation could still
// climb), so it is reported too.
expect(runRule('import(`./sub/${name}.js`);', file)).toHaveLength(1);
expect(runRule('import(`src/config/${name}.js`);', file)).toHaveLength(1);
});
it('drops computed dynamic sources with no statically known local prefix', () => {
const file = 'packages/cli/src/utils/foo.ts';
// A bare identifier or a package-like prefix is the same boundary the
// static check applies to non-relative specifiers — not reported.
expect(runRule('import(moduleName);', file)).toHaveLength(0);
expect(runRule('import(`${pkg}/entry.js`);', file)).toHaveLength(0);
});
it('allows imports that stay within utils', () => {
expect(
runRule('import(`./sibling.js`);', 'packages/cli/src/utils/foo.ts'),
).toHaveLength(0);
expect(
runRule(
"import value from './sibling.js';",
'packages/cli/src/utils/foo.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from '../cleanup.js';",
'packages/cli/src/utils/housekeeping/cleanup.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from '../../utils/sibling.js';",
'packages/cli/src/utils/housekeeping/cleanup.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from 'src/utils/sibling.js';",
'packages/cli/src/utils/foo.ts',
),
).toHaveLength(0);
// same doubly-nested shape as the reject case: a sibling import stays
// inside the LAST marker's utils root
expect(
runRule(
"import value from './sibling.js';",
'/tmp/packages/cli/src/utils/nested/packages/cli/src/utils/foo.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from 'src/utils/sibling.js';",
'/tmp/packages/cli/src/utils/nested/packages/cli/src/utils/foo.ts',
),
).toHaveLength(0);
});
it('allows package and builtin imports', () => {
const file = 'packages/cli/src/utils/foo.ts';
expect(runRule("import fs from 'node:fs';", file)).toHaveLength(0);
expect(
runRule("import value from '@qwen-code/qwen-code-core';", file),
).toHaveLength(0);
});
it('ignores tests, fixtures, __tests__, and non-utils consumers', () => {
expect(
runRule(
"import value from '../config/settings.js';",
'packages/cli/src/utils/foo.test.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from '../config/settings.js';",
'packages/cli/src/utils/foo.spec.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from '../config/settings.js';",
'packages/cli/src/utils/__tests__/helper.ts',
),
).toHaveLength(0);
expect(
runRule(
"import value from '../config/settings.js';",
'packages/cli/src/utils/fixtures/helper.ts',
),
).toHaveLength(0);
// non-utils consumers may import utils freely
expect(
runRule(
"import value from '../utils/sibling.js';",
'packages/cli/src/config/foo.ts',
),
).toHaveLength(0);
});
});