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>
This commit is contained in:
易良 2026-08-23 14:41:49 +00:00 committed by GitHub
parent e9ccd3c67e
commit 56db17bd4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
134 changed files with 907 additions and 355 deletions

View file

@ -221,7 +221,7 @@ function runQwen(options, prompt) {
let idleTimedOut = false;
let lastOutputAt = Date.now();
// The sandbox launcher prints the container name before the container
// starts (packages/cli/src/utils/sandbox.ts), so the FIRST match is this
// starts (packages/cli/src/serve/sandbox.ts), so the FIRST match is this
// run's own container — the kill-path reap below relies on that ownership.
let sandboxName = '';
let lineCarry = '';

View file

@ -26,7 +26,7 @@ The metafile showed eight value-import sites (type-only imports are free):
| cli | `commands/channel/proxy.ts` | `EnvHttpProxyAgent`, `setGlobalDispatcher` |
| cli | `utils/gitUtils.ts` | `ProxyAgent` |
| cli | `services/setup-github.ts` | `ProxyAgent` |
| cli | `utils/standalone-update.ts` | `fetch` |
| cli | `ui/standalone-update.ts` | `fetch` |
## Design

View file

@ -419,7 +419,7 @@ not be re-plumbed through a running session.
### Decision: Reuse the schema's `requiresRestart` flag (single source of truth)
`settingsSchema.ts` already declares `requiresRestart: boolean` on **every** key,
and `packages/cli/src/utils/settingsUtils.ts` already exposes the lookups:
and `packages/cli/src/config/settingsUtils.ts` already exposes the lookups:
- `requiresRestart(key: string): boolean` — flag for a dot-path key
- `getFlattenedSchema()` — full flattened `key → definition` map

View file

@ -155,6 +155,13 @@ section; the benefit is that neither document lies about its flow.
**Decisions** (rationale in the prose below):
> **Implementation note (2026-08-23):** #9146 moved the existing review
> findings schema to `packages/cli/src/commands/review/findings.ts` and made
> `packages/cli/src/utils/` a mechanically enforced leaf layer. The proposed
> shared-home placement below is retained as design history, not as an
> instruction to restore `utils/findings.ts`. A future `/audit` implementation
> must revisit the neutral contract ownership explicitly.
- `/audit` is a new skill with its own SKILL.md; `/review`'s SKILL.md and
certifying path stay untouched — no in-place target-kind branches in
the files `/review`'s coverage gate recomputes.

View file

@ -577,7 +577,7 @@ const slashCommands = await getAvailableCommands(
### 9.3 不变的文件
- `packages/cli/src/utils/commands.ts``parseSlashCommand` 无需修改)
- `packages/cli/src/ui/commands/commands.ts``parseSlashCommand` 无需修改)
- `packages/cli/src/ui/hooks/slashCommandProcessor.ts`interactive 路径无需修改)
- `packages/cli/src/ui/noninteractive/nonInteractiveUi.ts`stub UI 无需修改)
- 所有命令的 `action` 实现Phase 1 不修改任何命令行为)

View file

@ -73,7 +73,7 @@ export type CommandSource =
| ACP `argumentHint` | 已映射到 `availableCommands[].input.hint` | `acp-integration/session/Session.ts` |
| ACP source/supportedModes/subcommands/modelInvocable | 未暴露 | `acp-integration/session/Session.ts` |
| 冲突处理 | extension 命令冲突时已重命名为 `extensionName.commandName`,非 extension 同名为后加载覆盖前加载 | `services/CommandService.ts` |
| `/doctor` | 已实现,支持 `interactive` / `non_interactive` / `acp` | `ui/commands/doctorCommand.ts``utils/doctorChecks.ts` |
| `/doctor` | 已实现,支持 `interactive` / `non_interactive` / `acp` | `ui/commands/doctorCommand.ts``ui/commands/doctorChecks.ts` |
### 2.3 Claude Code 可借鉴点
@ -548,7 +548,7 @@ type AcpSubcommandMeta = {
- 模式:`['interactive', 'non_interactive', 'acp']`
- interactive展示 `HistoryItemDoctor`
- non_interactive/acp返回 JSON `message`
- 诊断逻辑:`packages/cli/src/utils/doctorChecks.ts`
- 诊断逻辑:`packages/cli/src/ui/commands/doctorChecks.ts`
Phase 3 只需在 Help 和补全中为 `/doctor` 正确展示来源、mode如需优化可将 headless JSON 改为更适合人读的 Markdown但这不是必需项。

View file

@ -766,7 +766,7 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe
| `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to. When set, overrides OTLP export. | Overrides the `telemetry.outfile` setting. |
| `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. |
| `QWEN_SANDBOX_IMAGE` | Overrides sandbox image selection for Docker/Podman. | Takes precedence over `tools.sandboxImage`. |
| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). |
| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/serve/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). |
| `DEBUG` or `DEBUG_MODE` | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting. | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically. |
| `NO_COLOR` | Set to any value to disable all color output in the CLI. | |
| `FORCE_HYPERLINK` | Override the OSC 8 clickable-link detection in the markdown renderer. Set to `1` (or any non-zero integer, or empty string) to force-enable; set to `0` or a non-numeric value such as `false` / `off` to force-disable. Honors `NO_COLOR` / `QWEN_DISABLE_HYPERLINKS` opt-outs above it. | Use this to opt into OSC 8 inside `tmux` / GNU `screen` (auto-detection refuses by default because the host terminal's capabilities are hidden behind the multiplexer). Requires `set -g allow-passthrough on` on tmux 3.3+. Also enables Hyper, which isn't auto-detected. |

View file

@ -0,0 +1,192 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
/**
* `packages/cli/src/utils/` is the leaf layer that every other directory
* imports. It must not import back up into a domain directory (`config/`,
* `ui/`, `i18n/`, `nonInteractive/`, `commands/`, `serve/`,
* `acp-integration/`, ...): that is the dependency-direction invariant
* tracked in #9146.
*
* The only permitted "upward" references are the type-only constructs that
* are genuinely erased at compile time: statement-level `import type`,
* `export type ... from`, and TS `import('...').T` type queries. Inline type
* specifiers (`import { type X } from` / `export { type X } from`) are
* reported instead: under this repo's `verbatimModuleSyntax`, tsc keeps the
* declaration and emits `import {} from` / `export {} from`, a runtime edge
* that still evaluates the target module. Everything else (value imports,
* value re-exports, dynamic `import()`) is reported too: a literal or
* single-segment template source is checked against its resolved path. CLI
* baseUrl specifiers rooted at `src/` are resolved from `packages/cli/` and
* checked the same way. A
* computed source (a multi-segment template or a `+` concatenation) whose
* statically known prefix is local is reported fail-closed, because
* interpolation can contribute a `../` step no static check can rule out. A
* computed source with no statically known local prefix is dropped, the same
* boundary applied to package and builtin specifiers. The two remaining
* instances (`Settings` in `modelConfigUtils.ts`, `CommandContext` in
* `sessionPaths.ts`) are this irreducible type-level coupling.
*/
const CLI_PACKAGE_MARKER = 'packages/cli/';
const CLI_UTILS_MARKER = `${CLI_PACKAGE_MARKER}src/utils/`;
const TEST_OR_FIXTURE_SEGMENTS = new Set(['__tests__', 'fixtures']);
function isCliUtilsProductionFile(filename) {
if (!filename || filename === '<input>' || filename === '<text>') {
return false;
}
const normalized = path.normalize(filename).replaceAll('\\', '/');
const start = normalized.lastIndexOf(CLI_UTILS_MARKER);
if (start < 0) {
return false;
}
const relativePath = normalized.slice(start + CLI_UTILS_MARKER.length);
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(relativePath)) {
return false;
}
return !relativePath.split('/').some((s) => TEST_OR_FIXTURE_SEGMENTS.has(s));
}
function escapesUtils(filename, importedPath) {
const normalized = path.normalize(filename).replaceAll('\\', '/');
const markerStart = normalized.lastIndexOf(CLI_UTILS_MARKER);
const utilsRoot = normalized.slice(0, markerStart + CLI_UTILS_MARKER.length);
const cliRoot = normalized.slice(0, markerStart + CLI_PACKAGE_MARKER.length);
const resolved = path.resolve(
importedPath.startsWith('src/') ? cliRoot : path.dirname(filename),
importedPath,
);
return path
.relative(utilsRoot, resolved)
.replaceAll('\\', '/')
.startsWith('..');
}
/**
* The statically known leading characters of a computed dynamic-import
* source: the first quasi of a template literal, the string literal itself,
* or the leftmost operand of a `+` concatenation. Anything else (a bare
* identifier, a call, an empty first quasi) has no statically known prefix.
*/
function knownDynamicPrefix(node) {
if (node.type === 'Literal') {
return typeof node.value === 'string' ? node.value : null;
}
if (node.type === 'TemplateLiteral') {
return node.quasis[0].value.cooked;
}
if (node.type === 'BinaryExpression' && node.operator === '+') {
return knownDynamicPrefix(node.left);
}
return null;
}
/** Whether a known prefix spells a relative specifier (`./`, `../`, `.`, `..`). */
function isRelativePrefix(prefix) {
return (
prefix === '.' ||
prefix === '..' ||
prefix.startsWith('./') ||
prefix.startsWith('../')
);
}
function isCliBaseUrlPrefix(prefix) {
return prefix.startsWith('src/');
}
export default {
meta: {
type: 'problem',
docs: {
description:
'packages/cli/src/utils must not import outside utils/ (leaf-layer dependency direction).',
},
messages: {
noUtilsUpwardImport:
'packages/cli/src/utils must not import outside utils/. ' +
'Invert the dependency (pass the value in) or move the module to the ' +
'domain directory that owns it (#9146).',
noUtilsUnprovableDynamicImport:
'packages/cli/src/utils cannot statically prove this computed ' +
'dynamic import() stays inside utils/ — interpolation can ' +
'contribute a `../` step. Resolve the target through a literal or ' +
'single-segment template source, or pass the module in (#9146).',
},
},
create(context) {
const { filename } = context;
if (!isCliUtilsProductionFile(filename)) {
return {};
}
const reportIfEscaping = (sourceNode, importedPath) => {
if (
typeof importedPath === 'string' &&
(importedPath.startsWith('.') || isCliBaseUrlPrefix(importedPath)) &&
escapesUtils(filename, importedPath)
) {
context.report({ node: sourceNode, messageId: 'noUtilsUpwardImport' });
}
};
const checkStatic = (node) => {
// Statement-level type-only imports (`import type`, `export type ...
// from`) are erased at compile time and cannot create a runtime cycle.
// Inline type specifiers (`import { type X } from ...`) are NOT exempt:
// under this repo's `verbatimModuleSyntax`, tsc keeps the declaration
// and emits `import {} from ...` / `export {} from ...`, a runtime edge
// that evaluates the target module — so they are reported like value
// imports.
if (node.importKind === 'type' || node.exportKind === 'type') {
return;
}
reportIfEscaping(node.source, node.source?.value);
};
const checkDynamic = (node) => {
const { source } = node;
if (source.type === 'Literal') {
reportIfEscaping(source, source.value);
return;
}
if (source.type === 'TemplateLiteral' && source.quasis.length === 1) {
reportIfEscaping(source, source.quasis[0].value.cooked);
return;
}
// Computed sources — multi-segment templates and `+` concatenations —
// fail closed when their statically known prefix is relative:
// interpolation can contribute a `../` step, so no static check can
// prove the import stays inside utils/ (a leading `../` cannot be
// undone by interpolation at all). A computed source with no known
// local prefix — a bare identifier or a package-like prefix — is
// dropped, the same boundary applied to package and builtin static
// specifiers. CLI baseUrl sources rooted at `src/` are local too.
const prefix = knownDynamicPrefix(source);
if (
typeof prefix === 'string' &&
(isRelativePrefix(prefix) || isCliBaseUrlPrefix(prefix))
) {
context.report({
node: source,
messageId: 'noUtilsUnprovableDynamicImport',
});
}
};
return {
ImportDeclaration: checkStatic,
ExportNamedDeclaration: checkStatic,
ExportAllDeclaration: checkStatic,
ImportExpression: checkDynamic,
// TSImportType (`import('../config/x').T`) is type-only by definition, so
// it is intentionally not reported.
};
},
};

View file

@ -16,6 +16,7 @@ import globals from 'globals';
import storybook from 'eslint-plugin-storybook';
import checkFile from 'eslint-plugin-check-file';
import noCoreRootBarrelImport from './eslint-rules/no-core-root-barrel-import.js';
import noUtilsUpwardImport from './eslint-rules/no-utils-upward-import.js';
import { legacyFilenames } from './eslint.legacy-filenames.mjs';
// General syntax restrictions applied to every TS/TSX source file. Hoisted so
@ -109,24 +110,20 @@ export default tseslint.config(
},
},
{
// `utils/` is the layer every other directory imports, so it must not
// import back into one. The daemon direction is clean and enforced here;
// the remaining `ui/`, `config/`, `i18n/` and `nonInteractive/` edges are
// tracked in #9146 and will be added to this group as they are resolved.
// `utils/` is the leaf layer that every other directory imports, so it
// must not import back up into a domain directory. Type-only imports are
// exempt: they are erased at compile time and cannot create a runtime
// cycle. See #9146.
files: ['packages/cli/src/utils/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/serve/*', '**/serve/**'],
message:
'packages/cli/src/utils must not import serve/. Move lifecycle-free logic down into utils/ instead (#9146).',
},
],
plugins: {
architecture: {
rules: {
'no-utils-upward-import': noUtilsUpwardImport,
},
],
},
},
rules: {
'architecture/no-utils-upward-import': 'error',
},
},
{

View file

@ -12,7 +12,7 @@ const WINDOWS_ABSOLUTE_PATH_RE = /^([A-Za-z]):[\\/](.*)$/;
/**
* Maps a Windows-shaped absolute path to the container mount produced by the
* host-side sandbox launcher (`C:\work\proj` `/c/work/proj`, mirroring
* `getContainerPath` in `cli/src/utils/sandbox.ts`).
* `getContainerPath` in `cli/src/serve/sandbox.ts`).
*
* A Windows host relaunching `qwen serve` into a Linux Docker/Podman sandbox
* translates the bind mount and `--workdir`, but path-valued CLI arguments

View file

@ -35,7 +35,7 @@ const { mockMcpPoolDrainAll } = vi.hoisted(() => ({
vi.mock('../utils/cleanup.js', () => ({
runExitCleanup: mockRunExitCleanup,
}));
vi.mock('../utils/housekeeping/scheduler.js', () => ({
vi.mock('../services/housekeeping/scheduler.js', () => ({
startNonInteractiveOpenAILogHousekeeping:
mockStartNonInteractiveOpenAILogHousekeeping,
}));
@ -886,7 +886,7 @@ vi.mock('./session/Session.js', () => {
}),
};
});
vi.mock('../utils/languageUtils.js', () => ({
vi.mock('../i18n/languageUtils.js', () => ({
updateOutputLanguageFile: vi.fn(),
writeOutputLanguageAndRegisterPath: vi.fn(
(
@ -1011,7 +1011,7 @@ import {
resolveOutputLanguageOrPreserveAuto,
updateOutputLanguageFile,
writeOutputLanguageAndRegisterPath,
} from '../utils/languageUtils.js';
} from '../i18n/languageUtils.js';
import { buildAuthMethods } from './authMethods.js';
import {
ACTIVE_WORK_HEARTBEAT_META_KEY,

View file

@ -187,7 +187,7 @@ import {
ACP_EVENT_LOOP_STALL_RESTART_MS,
CHANNEL_PROMPT_META_KEY,
} from '@qwen-code/channel-base';
import { observeAcpToolResultWire } from '../utils/tool-result-boundary-diagnostics.js';
import { observeAcpToolResultWire } from '../nonInteractive/tool-result-boundary-diagnostics.js';
import { Readable, Writable } from 'node:stream';
import { normalizeDisabledToolList } from '../config/normalizeDisabledTools.js';
import { pipeline } from 'node:stream/promises';
@ -279,11 +279,11 @@ import {
resolveOutputLanguageOrPreserveAuto,
getOutputLanguageFilePath,
writeOutputLanguageAndRegisterPath,
} from '../utils/languageUtils.js';
} from '../i18n/languageUtils.js';
import { runWithAcpRuntimeOutputDir } from './runtimeOutputDirContext.js';
import { ACP_ERROR_CODES } from './errorCodes.js';
import { runExitCleanup } from '../utils/cleanup.js';
import { startNonInteractiveOpenAILogHousekeeping } from '../utils/housekeeping/scheduler.js';
import { startNonInteractiveOpenAILogHousekeeping } from '../services/housekeeping/scheduler.js';
import { appEvents, AppEvent } from '../utils/events.js';
import {
setLanguageAsync,

View file

@ -270,10 +270,8 @@ import type {
AgentSideConnection,
} from '@agentclientprotocol/sdk';
import { SettingScope, type LoadedSettings } from '../../config/settings.js';
import {
insertAfterFunctionResponses,
normalizePartList,
} from '../../utils/nonInteractiveHelpers.js';
import { insertAfterFunctionResponses } from '../../nonInteractive/nonInteractiveHelpers.js';
import { normalizePartList } from '../../utils/normalize-part-list.js';
import { prefixMidTurnUserMessageParts } from '../../utils/midTurnUserMessage.js';
import {
handleSlashCommand,
@ -322,7 +320,7 @@ import type {
} from './types.js';
import { HistoryReplayer } from './history-replayer.js';
import { projectAcpToolResultUpdate } from './acp-tool-result-text-projection.js';
import { observeAcpToolResultProjection } from '../../utils/tool-result-boundary-diagnostics.js';
import { observeAcpToolResultProjection } from '../../nonInteractive/tool-result-boundary-diagnostics.js';
import { ToolCallEmitter } from './emitters/tool-call-emitter.js';
import { ToolCallPreparationTracker } from './tool-call-preparation-tracker.js';
import { PlanEmitter } from './emitters/PlanEmitter.js';

View file

@ -31,7 +31,7 @@ import {
createTranscriptToolCallStartUpdate,
} from '@qwen-code/acp-bridge/transcriptReplay';
import { sanitizeTerminalText } from '../../../ui/utils/textUtils.js';
import { associateAcpToolResultArtifact } from '../../../utils/tool-result-boundary-diagnostics.js';
import { associateAcpToolResultArtifact } from '../../../nonInteractive/tool-result-boundary-diagnostics.js';
const KIND_MAP: Record<Kind, ToolKind> = {
[Kind.Read]: 'read',

View file

@ -28,10 +28,10 @@ import {
const observeAcpProjectionMock = vi.hoisted(() => vi.fn());
vi.mock(
'../../utils/tool-result-boundary-diagnostics.js',
'../../nonInteractive/tool-result-boundary-diagnostics.js',
async (original) => ({
...(await original<
typeof import('../../utils/tool-result-boundary-diagnostics.js')
typeof import('../../nonInteractive/tool-result-boundary-diagnostics.js')
>()),
observeAcpToolResultProjection: observeAcpProjectionMock,
}),

View file

@ -21,7 +21,7 @@ import type { SessionUpdate } from '@agentclientprotocol/sdk';
import type { TranscriptReplayStateV1 } from '@qwen-code/acp-bridge/transcriptReplay';
import { Buffer } from 'node:buffer';
import { projectAcpToolResultUpdate } from './acp-tool-result-text-projection.js';
import { observeAcpToolResultProjection } from '../../utils/tool-result-boundary-diagnostics.js';
import { observeAcpToolResultProjection } from '../../nonInteractive/tool-result-boundary-diagnostics.js';
import { HistoryReplayer } from './history-replayer.js';
import type { PendingReplayToolCall } from './history-replayer.js';
import type { CumulativeUsage, SessionEmitterContext } from './types.js';

View file

@ -12,7 +12,7 @@ import type { Argv, CommandModule } from 'yargs';
import { parseArgsCommand } from './review/parse-args.js';
import { matchRemoteCommand } from './review/match-remote.js';
import { composeReviewCommand } from './review/compose-review.js';
import { findingsCommand } from '../utils/findings.js';
import { findingsCommand } from './review/findings.js';
import { recoverFindingsCommand } from './review/recover-findings.js';
import { fetchPrCommand } from './review/fetch-pr.js';
import { captureLocalCommand } from './review/capture-local.js';

View file

@ -37,7 +37,7 @@ import {
SOURCES,
type Severity,
type Source,
} from '../../utils/findings.js';
} from './findings.js';
import { BRIEFS } from './lib/agent-briefs.js';
import {
budgetStopDisclosure,

View file

@ -42,9 +42,9 @@ import {
} from 'node:fs';
import type { Stats } from 'node:fs';
import { dirname, resolve, sep } from 'node:path';
import { writeStdoutLine, writeStderrLine } from './stdioHelpers.js';
import type { AnchorRequest } from '../commands/review/lib/anchors.js';
import { isSameFile } from '../commands/review/lib/same-file.js';
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
import type { AnchorRequest } from './lib/anchors.js';
import { isSameFile } from './lib/same-file.js';
// These four lists have a second consumer: the Web Shell review renderer
// (packages/web-shell/client/components/artifacts/CodeReviewArtifactDetail.tsx)

View file

@ -112,7 +112,7 @@ describe('openedBrief / readBrief', () => {
const arg = `{"absolute_path":${needle}}`;
it('does not credit a shell command that merely MENTIONS the brief', () => {
// The trap a prose matcher walks into: `utils/findings.ts` has a
// The trap a prose matcher walks into: `findings.ts` has a
// same-purpose-looking `namesPath` that matches on a name boundary, and
// it credits this arg. Deleting a brief is not opening it — so this atom
// matches the whole JSON string value instead, and keeps a different

View file

@ -70,7 +70,7 @@ export function declaresOwnUncoverable(
* not one per half. This wrapper only spreads it over a record's call list;
* every path atom below routes through it.
*
* The name is deliberately not `namesPath`: `utils/findings.ts` has a
* The name is deliberately not `namesPath`: `findings.ts` has a
* module-private `namesPath` that matches a path named in PROSE on a name
* boundary it credits `rm /plan/chunk-3.brief.md` for naming the brief.
* Unifying these two would make `openedBrief` credit an agent for deleting a

View file

@ -36,7 +36,7 @@ vi.mock('../../../config/settings.js', async (importOriginal) => {
return { ...actual, loadSettings: loadSettingsMock };
});
import { operatorReviewSettings } from './review-settings.js';
import { getDialogSettingKeys } from '../../../utils/settingsUtils.js';
import { getDialogSettingKeys } from '../../../config/settingsUtils.js';
function setReview(review: unknown): void {
loadSettingsMock.mockReturnValue({ merged: { review } });

View file

@ -10,7 +10,7 @@
* plain `'…'` wrap traded that for breaking on the first embedded apostrophe
* (`~/Documents/John's Projects/…` is an ordinary macOS workspace). The
* `'\''` dance closes both: end the quote, emit a literal `'`, reopen.
* Same pattern as `shellQuoteForSh` in utils/standalone-update.ts.
* Same pattern as `shellQuoteForSh` in ui/standalone-update.ts.
*/
export function shellQuotePath(p: string): string {
return `'${p.replace(/'/g, "'\\''")}'`;

View file

@ -477,9 +477,12 @@ describe('the bundled skill stops on what this module prints', () => {
const services = join(root, 'packages', 'cli', 'src', 'services');
mkdirSync(services, { recursive: true });
writeFileSync(join(services, 'review-worktree-lease.ts'), 'leases');
// No `findings.ts` under `utils/`: it moved back under
// `commands/review/` (#9146), which the directory root already covers
// via `drive.ts`. No root digests `utils/` wholesale, so the materialized
// tree mirrors `reviewSourceRoots` exactly.
const utils = join(root, 'packages', 'cli', 'src', 'utils');
mkdirSync(utils, { recursive: true });
writeFileSync(join(utils, 'findings.ts'), 'validates');
writeFileSync(join(utils, 'shell-args.ts'), 'tokenizes');
writeFileSync(join(utils, 'paths.ts'), 'flattens');
const skillDir = join(
@ -586,12 +589,9 @@ describe('reviewSourceRoots', () => {
),
kind: 'code',
},
// The review helpers lifted out of `commands/review/`; the digest
// covered them there before the lift.
{
path: join('/w', 'packages', 'cli', 'src', 'utils', 'findings.ts'),
kind: 'code',
},
// The helpers of the findings validator live in `utils/`, outside the
// `review/` directory root; the validator itself lives back under
// `commands/review/` (#9146), which the directory root covers.
{
path: join('/w', 'packages', 'cli', 'src', 'utils', 'shell-args.ts'),
kind: 'code',

View file

@ -30,8 +30,9 @@
//
// SCOPE, so silence is not read as more than it is: the roots are the review
// commands, the file that registers them, the review-only lease they import
// from `services/`, the three review helpers lifted into `utils/`, and the
// bundled skill — not the modules those import. Editing
// from `services/`, the two review helpers left in `utils/`, and the bundled
// skill — not the modules those import. The validator itself is back under
// `commands/review/`, which the directory root covers. Editing
// `utils/stdioHelpers.ts` or a core helper on a review path and skipping the
// rebuild produces no warning. The line drawn here is the code
// whose behaviour a review is about; a quiet run means that code matches the
@ -361,14 +362,11 @@ export function reviewSourceRoots(repoRoot: string): ReviewSourceRoot[] {
),
kind: 'code',
},
// The findings validator and its two helpers were lifted out of
// `commands/review/` into `utils/`; the digest covered them there, and a
// root list that lost them would keep both digest copies equal while a
// skipped rebuild silently runs the bundle's old validator.
{
path: join(repoRoot, 'packages', 'cli', 'src', 'utils', 'findings.ts'),
kind: 'code',
},
// The two helpers of the findings validator live in `utils/`, outside
// the `review/` directory root; a root list that lost them would keep
// both digest copies equal while a skipped rebuild silently runs the
// bundle's old validator. The validator itself moved back into
// `commands/review/` (#9146), which the directory root covers.
{
path: join(repoRoot, 'packages', 'cli', 'src', 'utils', 'shell-args.ts'),
kind: 'code',

View file

@ -827,7 +827,7 @@ describe('serializedArgsNamePath — the one needle both halves use', () => {
it('does not credit a shell command that merely mentions the path', () => {
// The divergence the review measured between this and the prose-boundary
// `namesPath` in `utils/findings.ts`, which returns true here. Both the
// `namesPath` in `findings.ts`, which returns true here. Both the
// diff-read half and the brief atoms route through THIS one, so the
// certification bar cannot credit `rm <file>` as opening it.
expect(

View file

@ -264,7 +264,7 @@ function rangeOf(args: Record<string, unknown>): [number, number] | null {
* (normalisation, escaping, a stricter compare) reaches the whole bar at once
* rather than half of it.
*
* NOT `namesPath` in `utils/findings.ts`: that one matches a path mentioned in
* NOT `namesPath` in `findings.ts`: that one matches a path mentioned in
* PROSE on a name boundary, so it credits `rm /plan/chunk-3.brief.md` for
* naming the brief. Crediting an agent for deleting a file it never opened is
* precisely what this predicate must not do, which is why the two keep

View file

@ -51,11 +51,7 @@ import {
type AssetsManifest,
type PublishedAsset,
} from './lib/assets.js';
import {
validateFindings,
buildReport,
type Finding,
} from '../../utils/findings.js';
import { validateFindings, buildReport, type Finding } from './findings.js';
interface PublishAssetsArgs {
pr: number;

View file

@ -20,7 +20,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import yargs from 'yargs';
import type { Argv } from 'yargs';
import { buildReport, type Finding } from '../../utils/findings.js';
import { buildReport, type Finding } from './findings.js';
import { saveArtifactCommand, saveReviewArtifact } from './save-artifact.js';
// On a case-sensitive filesystem the alias below never exists, so that test

View file

@ -28,7 +28,7 @@ import {
buildReport,
type FindingsReport,
validateFindings,
} from '../../utils/findings.js';
} from './findings.js';
import { EFFORT_LEVELS, type ReviewEffort } from './parse-args.js';
import { REVIEWS_DIR } from './lib/paths.js';
import { isSameFile } from './lib/same-file.js';

View file

@ -52,7 +52,7 @@ vi.mock('../utils/installationInfo.js', () => ({
getInstallationInfo,
resolveUpdateCommand,
}));
vi.mock('../utils/standalone-update.js', () => ({ performStandaloneUpdate }));
vi.mock('../ui/standalone-update.js', () => ({ performStandaloneUpdate }));
vi.mock('../utils/package.js', () => ({ getPackageJson }));
vi.mock('../utils/stdioHelpers.js', () => ({
writeStdoutLine,

View file

@ -24,7 +24,7 @@ export const updateCommand: CommandModule = {
import('../config/settings.js'),
import('../ui/utils/updateCheck.js'),
import('../utils/installationInfo.js'),
import('../utils/standalone-update.js'),
import('../ui/standalone-update.js'),
import('../utils/stdioHelpers.js'),
import('../utils/updateEventEmitter.js'),
]);

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { LoadedSettings } from '../config/settings.js';
import { SettingScope } from '../config/settings.js';
import type { LoadedSettings } from './settings.js';
import { SettingScope } from './settings.js';
import { settingExistsInScope } from './settingsUtils.js';
/**

View file

@ -10,9 +10,8 @@ import { SettingScope } from './settings.js';
// settingsUtils makes real fs calls in backup/restore — stub them out so the
// tests can focus on adapter behavior without touching disk.
vi.mock('../utils/settingsUtils.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../utils/settingsUtils.js')>();
vi.mock('./settingsUtils.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./settingsUtils.js')>();
return {
...actual,
backupSettingsFile: vi.fn(),

View file

@ -22,7 +22,7 @@ import {
cleanupSettingsBackup,
restoreSettingsFromBackup,
getNestedProperty,
} from '../utils/settingsUtils.js';
} from './settingsUtils.js';
export function createLoadedSettingsAdapter(
settings: LoadedSettings,

View file

@ -12,7 +12,7 @@ import {
V1_TO_V2_PRESERVE_DISABLE_MAP,
V2_CONTAINER_KEYS,
} from './v1-to-v2-shared.js';
import { setNestedPropertySafe } from '../../../utils/settingsUtils.js';
import { setNestedPropertySafe } from '../../settingsUtils.js';
/**
* Heuristic indicators for deciding whether an object is "V1-like".

View file

@ -9,7 +9,7 @@ import {
deleteNestedPropertySafe,
getNestedProperty,
setNestedPropertySafe,
} from '../../../utils/settingsUtils.js';
} from '../../settingsUtils.js';
/**
* Path mapping for boolean polarity migration (V2 disable* -> V3 enable*).

View file

@ -8,7 +8,7 @@ import type { SettingsMigration } from '../types.js';
import {
getNestedProperty,
setNestedPropertySafe,
} from '../../../utils/settingsUtils.js';
} from '../../settingsUtils.js';
const GIT_CO_AUTHOR_PATH = 'general.gitCoAuthor';

View file

@ -74,7 +74,7 @@ import {
import {
WORKSPACE_RESTRICTED_SETTINGS,
WORKSPACE_RESTRICTED_SETTING_KEYS,
} from '../utils/settingsUtils.js';
} from './settingsUtils.js';
import { needsMigration } from './migration/index.js';
import { QWEN_DIR } from '@qwen-code/qwen-code-core';

View file

@ -25,7 +25,6 @@ import { hasOwnModelProviders } from './modelProvidersScope.js';
import {
type Settings,
type MemoryImportFormat,
type MergeStrategy,
type SettingsSchema,
type SettingDefinition,
getSettingsSchema,
@ -34,8 +33,8 @@ import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
import {
setNestedPropertySafe,
WORKSPACE_RESTRICTED_SETTINGS,
} from '../utils/settingsUtils.js';
import { customDeepMerge } from '../utils/deepMerge.js';
} from './settingsUtils.js';
import { customDeepMerge, type MergeStrategy } from '../utils/deepMerge.js';
import { updateSettingsFilePreservingFormat } from '../utils/jsonc-editor.js';
import { runMigrations, needsMigration } from './migration/index.js';
import {

View file

@ -12,11 +12,11 @@ import {
} from '@qwen-code/qwen-code-core';
import {
getSettingsSchema,
MergeStrategy,
type SettingDefinition,
type Settings,
type SettingsSchema,
} from './settingsSchema.js';
import { MergeStrategy } from '../utils/deepMerge.js';
import {
MAX_CONCURRENT_SUB_SESSIONS_PER_CALLER,
MAX_CONCURRENT_SUB_SESSIONS_TOTAL,

View file

@ -29,6 +29,7 @@ import {
} from '@qwen-code/qwen-code-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import { getLanguageSettingsOptions } from '../i18n/languages.js';
import { MergeStrategy } from '../utils/deepMerge.js';
export const DEFAULT_OPENAI_LOG_RETENTION_DAYS = 7;
@ -64,17 +65,6 @@ export interface SettingEnumOption {
label: string;
}
export enum MergeStrategy {
// Replace the old value with the new value. This is the default.
REPLACE = 'replace',
// Concatenate arrays.
CONCAT = 'concat',
// Merge arrays, ensuring unique values.
UNION = 'union',
// Shallow merge objects.
SHALLOW_MERGE = 'shallow_merge',
}
export interface SettingDefinition {
type: SettingsType;
label: string;

View file

@ -32,11 +32,10 @@ import {
type Settings,
type SettingsSchema,
type SettingsSchemaType,
} from '../config/settingsSchema.js';
} from './settingsSchema.js';
vi.mock('../config/settingsSchema.js', async (importOriginal) => {
const original =
await importOriginal<typeof import('../config/settingsSchema.js')>();
vi.mock('./settingsSchema.js', async (importOriginal) => {
const original = await importOriginal<typeof import('./settingsSchema.js')>();
return {
...original,
getSettingsSchema: vi.fn(),

View file

@ -5,19 +5,15 @@
*/
import * as fs from 'node:fs';
import type {
Settings,
SettingScope,
LoadedSettings,
} from '../config/settings.js';
import type { Settings, SettingScope, LoadedSettings } from './settings.js';
import type {
SettingDefinition,
SettingsSchema,
SettingsValue,
} from '../config/settingsSchema.js';
import { getSettingsSchema } from '../config/settingsSchema.js';
} from './settingsSchema.js';
import { getSettingsSchema } from './settingsSchema.js';
import { t } from '../i18n/index.js';
import { isAutoLanguage } from './languageUtils.js';
import { isAutoLanguage } from '../i18n/languageUtils.js';
// The schema is now nested, but many parts of the UI and logic work better
// with a flattened structure and dot-notation keys. This section flattens the

View file

@ -9,7 +9,7 @@ import * as path from 'node:path';
import { watch as watchFs, type FSWatcher } from 'chokidar';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
import { type LoadedSettings, SettingScope } from './settings.js';
import { getFlattenedSchema } from '../utils/settingsUtils.js';
import { getFlattenedSchema } from './settingsUtils.js';
const debugLogger = createDebugLogger('SETTINGS_WATCHER');

View file

@ -21,7 +21,7 @@ import type { PermissionSuggestion } from '../nonInteractive/types.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
import type { Part } from '@google/genai';
import { StreamJsonOutputAdapter } from '../nonInteractive/io/index.js';
import { reportChatRecordingFailureToAdapter } from '../utils/chat-recording-failure.js';
import { reportChatRecordingFailureToAdapter } from '../nonInteractive/chat-recording-failure.js';
const debugLogger = createDebugLogger('DUAL_OUTPUT');

View file

@ -169,7 +169,7 @@ vi.mock('./utils/events.js', async (importOriginal) => {
};
});
vi.mock('./utils/sandbox.js', () => ({
vi.mock('./serve/sandbox.js', () => ({
sandbox_command: vi.fn(() => ''), // Default to no sandbox command
start_sandbox: vi.fn(() => Promise.resolve()), // Mock as an async function that resolves
}));
@ -206,7 +206,7 @@ vi.mock('./startup/startup-prefetch.js', () => ({
mockStartPostRenderPrefetches(...args),
}));
vi.mock('./utils/update-relaunch.js', () => ({
vi.mock('./ui/update-relaunch.js', () => ({
updateBeforeRelaunch: (...args: unknown[]) =>
mockUpdateBeforeRelaunch(...args),
}));
@ -219,9 +219,11 @@ vi.mock('./acp-integration/acpAgent.js', () => ({
runAcpAgent: (...args: unknown[]) => mockRunAcpAgent(...args),
}));
vi.mock('./utils/housekeeping/scheduler.js', async (importOriginal) => {
vi.mock('./services/housekeeping/scheduler.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('./utils/housekeeping/scheduler.js')>();
await importOriginal<
typeof import('./services/housekeeping/scheduler.js')
>();
return {
...actual,
startNonInteractiveOpenAILogHousekeeping: (...args: unknown[]) =>
@ -1232,7 +1234,7 @@ describe('gemini.tsx main function', () => {
);
const { loadSettings } = await import('./config/settings.js');
const { loadSandboxConfig } = await import('./config/sandboxConfig.js');
const { start_sandbox } = await import('./utils/sandbox.js');
const { start_sandbox } = await import('./serve/sandbox.js');
const { relaunchOnExitCode } = await import('./utils/relaunch.js');
vi.mocked(start_sandbox).mockClear();

View file

@ -87,14 +87,14 @@ import {
relaunchAppInChildProcess,
relaunchOnExitCode,
} from './utils/relaunch.js';
import { start_sandbox } from './utils/sandbox.js';
import { start_sandbox } from './serve/sandbox.js';
import { getStartupWarnings } from './utils/startupWarnings.js';
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
import { initializeWarningHandler } from './utils/warningHandler.js';
import { writeStderrLine, writeStderrLineSafe } from './utils/stdioHelpers.js';
import { sanitizeTerminalText } from './ui/utils/textUtils.js';
import { getHeadlessYoloSafetyWarning } from './utils/headlessSafetyWarnings.js';
import { initializeLlmOutputLanguage } from './utils/languageUtils.js';
import { initializeLlmOutputLanguage } from './i18n/languageUtils.js';
import {
CUSTOM_SANDBOX_IMAGE_ENV_VAR,
HOST_UPDATE_RELAUNCH_ENV_VAR,
@ -518,9 +518,7 @@ export async function main() {
await initializeI18n(
resolveLanguageSetting(settings.merged.general?.language as string),
);
const { updateBeforeRelaunch } = await import(
'./utils/update-relaunch.js'
);
const { updateBeforeRelaunch } = await import('./ui/update-relaunch.js');
const shouldRelaunch = await updateBeforeRelaunch(
settings,
updateProjectRoot,
@ -873,7 +871,7 @@ export async function main() {
const nonInteractiveHousekeeping =
!config.isInteractive() || config.getExperimentalZedIntegration()
? await import('./utils/housekeeping/scheduler.js')
? await import('./services/housekeeping/scheduler.js')
: undefined;
if (nonInteractiveHousekeeping) {
registerCleanup(() =>

View file

@ -17,7 +17,7 @@ vi.mock('node:fs', () => ({
}));
// Mock i18n module
vi.mock('../i18n/index.js', () => ({
vi.mock('./index.js', () => ({
detectSystemLanguage: vi.fn(),
getLanguageNameFromLocale: vi.fn((locale: string) => {
const map: Record<string, string> = {
@ -42,7 +42,7 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
},
}));
import * as i18n from '../i18n/index.js';
import * as i18n from './index.js';
import {
OUTPUT_LANGUAGE_AUTO,
isAutoLanguage,

View file

@ -13,8 +13,8 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Storage } from '@qwen-code/qwen-code-core';
import { getLanguageNameFromLocale } from '../i18n/index.js';
import { SUPPORTED_LANGUAGES } from '../i18n/languages.js';
import { getLanguageNameFromLocale } from './index.js';
import { SUPPORTED_LANGUAGES } from './languages.js';
const LLM_OUTPUT_LANGUAGE_RULE_FILENAME = 'output-language.md';
const LLM_OUTPUT_LANGUAGE_MARKER_PREFIX = 'qwen-code:llm-output-language:';

View file

@ -11,7 +11,7 @@ import {
type ChatRecordingFailureListener,
type Config,
} from '@qwen-code/qwen-code-core';
import type { JsonOutputAdapterInterface } from '../nonInteractive/io/BaseJsonOutputAdapter.js';
import type { JsonOutputAdapterInterface } from './io/BaseJsonOutputAdapter.js';
import {
createChatRecordingFailureSystemMessage,
settleChatRecording,
@ -22,7 +22,7 @@ const { mockWriteStderrLine } = vi.hoisted(() => ({
mockWriteStderrLine: vi.fn(),
}));
vi.mock('./stdioHelpers.js', () => ({
vi.mock('../utils/stdioHelpers.js', () => ({
writeStderrLine: mockWriteStderrLine,
}));

View file

@ -11,10 +11,10 @@ import {
type ChatRecordingFailureEvent,
type Config,
} from '@qwen-code/qwen-code-core';
import type { JsonOutputAdapterInterface } from '../nonInteractive/io/BaseJsonOutputAdapter.js';
import type { CLISystemMessage } from '../nonInteractive/types.js';
import type { JsonOutputAdapterInterface } from './io/BaseJsonOutputAdapter.js';
import type { CLISystemMessage } from './types.js';
import { t } from '../i18n/index.js';
import { writeStderrLine } from './stdioHelpers.js';
import { writeStderrLine } from '../utils/stdioHelpers.js';
export const CHAT_RECORDING_FAILURE_MESSAGE =
'Session recording stopped after a write failure. New messages for the affected session will not be saved. Check disk space and permissions, then start a new session to resume recording. See the debug log for details.';

View file

@ -36,7 +36,7 @@ import type {
PermissionSuggestion,
} from '../../types.js';
import { BaseController } from './baseController.js';
import { buildPermissionSuggestions } from '../../../utils/permission-suggestions.js';
import { buildPermissionSuggestions } from '../../permission-suggestions.js';
const DEFAULT_CAN_USE_TOOL_TIMEOUT_MS = 60_000;

View file

@ -42,9 +42,9 @@ import type {
ToolUseBlock,
Usage,
} from '../types.js';
import { functionResponsePartsToString } from '../../utils/nonInteractiveHelpers.js';
import { functionResponsePartsToString } from '../nonInteractiveHelpers.js';
import { projectHeadlessToolResultContent } from './headless-tool-result-text-projection.js';
import { observeHeadlessToolResultProjection } from '../../utils/tool-result-boundary-diagnostics.js';
import { observeHeadlessToolResultProjection } from '../tool-result-boundary-diagnostics.js';
/**
* Internal state for managing a single message context (main agent or subagent).

View file

@ -11,7 +11,7 @@ import {
type JsonOutputAdapterInterface,
type ResultOptions,
} from './BaseJsonOutputAdapter.js';
import { observeHeadlessJsonToolResultWire } from '../../utils/tool-result-boundary-diagnostics.js';
import { observeHeadlessJsonToolResultWire } from '../tool-result-boundary-diagnostics.js';
/**
* JSON output adapter that collects all messages and emits them

View file

@ -29,7 +29,7 @@ import {
type ResultOptions,
type JsonOutputAdapterInterface,
} from './BaseJsonOutputAdapter.js';
import { observeHeadlessToolResultWire } from '../../utils/tool-result-boundary-diagnostics.js';
import { observeHeadlessToolResultWire } from '../tool-result-boundary-diagnostics.js';
/**
* Stream JSON output adapter that emits messages immediately

View file

@ -17,13 +17,9 @@ import {
OutputFormat,
} from '@qwen-code/qwen-code-core';
import type { Part } from '@google/genai';
import type {
CLIUserMessage,
PermissionMode,
} from '../nonInteractive/types.js';
import type { JsonOutputAdapterInterface } from '../nonInteractive/io/BaseJsonOutputAdapter.js';
import type { CLIUserMessage, PermissionMode } from './types.js';
import type { JsonOutputAdapterInterface } from './io/BaseJsonOutputAdapter.js';
import {
normalizePartList,
extractPartsFromUserMessage,
computeUsageFromMetrics,
buildSystemMessage,
@ -89,42 +85,6 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
};
});
describe('normalizePartList', () => {
it('should return empty array for null input', () => {
expect(normalizePartList(null)).toEqual([]);
});
it('should return empty array for undefined input', () => {
expect(normalizePartList(undefined as unknown as null)).toEqual([]);
});
it('should convert string to Part array', () => {
const result = normalizePartList('test string');
expect(result).toEqual([{ text: 'test string' }]);
});
it('should convert array of strings to Part array', () => {
const result = normalizePartList(['hello', 'world']);
expect(result).toEqual([{ text: 'hello' }, { text: 'world' }]);
});
it('should convert array of mixed strings and Parts to Part array', () => {
const part: Part = { text: 'existing' };
const result = normalizePartList(['new', part]);
expect(result).toEqual([{ text: 'new' }, part]);
});
it('should convert single Part object to array', () => {
const part: Part = { text: 'single part' };
const result = normalizePartList(part);
expect(result).toEqual([part]);
});
it('should handle empty array', () => {
expect(normalizePartList([])).toEqual([]);
});
});
describe('extractPartsFromUserMessage', () => {
it('should return null for undefined message', () => {
expect(extractPartsFromUserMessage(undefined)).toBeNull();

View file

@ -30,40 +30,16 @@ import type {
Usage,
PermissionMode,
CLISystemMessage,
} from '../nonInteractive/types.js';
} from './types.js';
import type {
JsonOutputAdapterInterface,
MessageEmitter,
} from '../nonInteractive/io/BaseJsonOutputAdapter.js';
} from './io/BaseJsonOutputAdapter.js';
import { computeSessionStats } from '../ui/utils/computeStats.js';
import { getAvailableCommands } from '../nonInteractiveCliCommands.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE');
/**
* Normalizes various part list formats into a consistent Part[] array.
*
* @param parts - Input parts in various formats (string, Part, Part[], or null)
* @returns Normalized array of Part objects
*/
export function normalizePartList(parts: PartListUnion | null): Part[] {
if (!parts) {
return [];
}
if (typeof parts === 'string') {
return [{ text: parts }];
}
if (Array.isArray(parts)) {
return parts.map((part) =>
typeof part === 'string' ? { text: part } : (part as Part),
);
}
return [parts as Part];
}
/**
* Extracts user message parts from a CLI protocol message.
*

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { PermissionSuggestion } from '../nonInteractive/types.js';
import type { PermissionSuggestion } from './types.js';
function withWarnings(
description: string,

View file

@ -49,7 +49,7 @@ import {
import {
settleChatRecording,
subscribeToHeadlessChatRecordingFailures,
} from '../utils/chat-recording-failure.js';
} from './chat-recording-failure.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE_SESSION');

View file

@ -8,7 +8,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { SessionUpdate } from '@agentclientprotocol/sdk';
import { LOAD_REPLAY_META_KEY } from '@qwen-code/acp-bridge/bridgeTypes';
import type { ToolResultBoundaryObservation } from '@qwen-code/qwen-code-core';
import type { CLIUserMessage } from '../nonInteractive/types.js';
import type { CLIUserMessage } from './types.js';
const { mockObserveBoundary } = vi.hoisted(() => ({
mockObserveBoundary: vi.fn(

View file

@ -11,7 +11,7 @@ import {
type ToolResultBoundaryArtifact,
type ToolResultBoundaryValue,
} from '@qwen-code/qwen-code-core';
import type { CLIMessage, ToolResultBlock } from '../nonInteractive/types.js';
import type { CLIMessage, ToolResultBlock } from './types.js';
interface ProjectedToolResult {
mutated: boolean;

View file

@ -94,7 +94,7 @@ import { RunBudgetEnforcer } from './utils/runBudget.js';
import {
settleChatRecording,
subscribeToHeadlessChatRecordingFailures,
} from './utils/chat-recording-failure.js';
} from './nonInteractive/chat-recording-failure.js';
import { registerCleanup } from './utils/cleanup.js';
import { cleanupReviewWorktreeLeases } from './services/review-worktree-lease.js';
@ -151,8 +151,8 @@ function suppressedOutputBody(structuredCaptured: boolean): string {
: SUPPRESSED_OUTPUT_RETRY;
}
import { normalizePartList } from './utils/normalize-part-list.js';
import {
normalizePartList,
extractPartsFromUserMessage,
buildSystemMessage,
createToolProgressHandler,
@ -160,7 +160,7 @@ import {
computeUsageFromMetrics,
buildInitialSystemReminders,
insertAfterFunctionResponses,
} from './utils/nonInteractiveHelpers.js';
} from './nonInteractive/nonInteractiveHelpers.js';
// Human-readable labels for the detectors that can fire mid-stream.
// Surfaced to stderr in TEXT mode so a headless run that halts on a loop

View file

@ -8,7 +8,7 @@ import type { PartListUnion } from '@google/genai';
import {
parseSlashCommand,
parseStackedSlashCommands,
} from './utils/commands.js';
} from './ui/commands/commands.js';
import {
Logger,
uiTelemetryService,

View file

@ -26,7 +26,7 @@ import {
type ServePreflightKind,
type ServeWorkspaceEnvStatus,
} from '@qwen-code/acp-bridge';
import { getGitVersion, getNpmVersion } from '../utils/systemInfo.js';
import { getGitVersion, getNpmVersion } from '../ui/systemInfo.js';
import { buildEnvStatusFromEnv, snapshotProcessEnv } from './env-snapshot.js';
const REQUIRED_NODE_MAJOR = 22;

View file

@ -164,6 +164,60 @@ const allowedProcessEnvAccesses = normalizeAllowances([
},
},
],
[
'packages/cli/src/serve/sandbox.ts',
{
reason:
'The sandbox launcher assembles the sandboxed child environment: ' +
'it passes through the process environment, forwards provider keys, ' +
'proxy settings, and debug switches, and reads the SANDBOX_* control ' +
'variables. It entered the scanned serve/ layer via the #9146 ' +
'leaf-layer move; its access surface is unchanged.',
accesses: {
'computed:envVar': 2,
'key:BUILD_SANDBOX': 2,
'key:COLORTERM': 2,
'key:DEBUG': 5,
'key:DEBUG_MODE': 1,
'key:DEBUG_PORT': 2,
'key:GEMINI_API_KEY': 2,
'key:GEMINI_MODEL': 2,
'key:GOOGLE_API_KEY': 2,
'key:GOOGLE_APPLICATION_CREDENTIALS': 2,
'key:GOOGLE_CLOUD_LOCATION': 2,
'key:GOOGLE_CLOUD_PROJECT': 2,
'key:GOOGLE_GENAI_USE_GCA': 2,
'key:GOOGLE_GENAI_USE_VERTEXAI': 2,
'key:HTTP_PROXY': 2,
'key:HTTPS_PROXY': 2,
'key:NO_PROXY': 2,
'key:NODE_ENV': 1,
'key:NODE_OPTIONS': 1,
'key:OPENAI_API_KEY': 2,
'key:OPENAI_BASE_URL': 2,
'key:OPENAI_MODEL': 2,
'key:PATH': 2,
'key:PYTHONPATH': 2,
'key:QWEN_CODE_INTEGRATION_TEST': 1,
'key:QWEN_CODE_MCP_APPROVALS_PATH': 2,
'key:QWEN_CODE_SCRUB_ELECTRON_RUN_AS_NODE': 1,
'key:QWEN_CODE_TEST_VAR': 2,
'key:QWEN_SANDBOX_PROXY_COMMAND': 2,
'key:SANDBOX_ENV': 2,
'key:SANDBOX_FLAGS': 2,
'key:SANDBOX_MOUNTS': 2,
'key:SANDBOX_PORTS': 1,
'key:SANDBOX_SET_UID_GID': 1,
'key:SEATBELT_PROFILE': 1,
'key:TERM': 2,
'key:VIRTUAL_ENV': 1,
'key:http_proxy': 2,
'key:https_proxy': 2,
'key:no_proxy': 2,
whole: 6,
},
},
],
[
'packages/cli/src/serve/server/fs-factory.ts',
{

View file

@ -11,7 +11,7 @@ import {
getOwnKeyScope,
getWritableScopes,
} from '../../config/modelProvidersScope.js';
import { getSettingDefinition } from '../../utils/settingsUtils.js';
import { getSettingDefinition } from '../../config/settingsUtils.js';
import { writeStderrLine } from '../../utils/stdioHelpers.js';
import {
isActiveModelSelection,

View file

@ -85,10 +85,9 @@ function makeQualifiedApp() {
safeBody: (req) =>
req.body && typeof req.body === 'object' ? req.body : {},
persistSetting,
workspaceRegistry:
registry as unknown as Parameters<
typeof registerWorkspaceQualifiedSettingsRoutes
>[1]['workspaceRegistry'],
workspaceRegistry: registry as unknown as Parameters<
typeof registerWorkspaceQualifiedSettingsRoutes
>[1]['workspaceRegistry'],
invalidateServeFeaturesCache: () => {},
});

View file

@ -21,7 +21,7 @@ import {
getSettingDefinition,
validateSettingValue,
WORKSPACE_RESTRICTED_SETTING_KEYS,
} from '../../utils/settingsUtils.js';
} from '../../config/settingsUtils.js';
import { writeStderrLine } from '../../utils/stdioHelpers.js';
import { parseAndValidateWorkspaceClientId } from '../server/request-helpers.js';
import {

View file

@ -32,14 +32,15 @@ vi.mock('node:child_process', async (importOriginal) => {
};
});
import { isContainerPathWithinWorkdir } from './sandbox-path.js';
import { isContainerPathWithinWorkdir } from '../utils/sandbox-path.js';
import {
BUILTIN_SEATBELT_PROFILES,
getSandboxPassthroughEnvArgs,
resolveSeatbeltProfileFile,
start_sandbox,
} from './sandbox.js';
import { parseSandboxImageName } from './sandboxImageName.js';
import { parseSandboxMountSpec } from './sandboxMounts.js';
import { parseSandboxImageName } from '../utils/sandboxImageName.js';
import { parseSandboxMountSpec } from '../utils/sandboxMounts.js';
afterEach(() => {
vi.restoreAllMocks();
@ -157,23 +158,37 @@ describe('resolveSeatbeltProfileFile', () => {
});
it('keeps source-mode seatbelt profile paths next to the module', () => {
const utilsDir = path.resolve(
const serveDir = path.resolve(
path.sep,
'repo',
'packages',
'cli',
'src',
'utils',
'serve',
);
const sourceUrl = pathToFileURL(
path.join(utilsDir, 'sandbox.ts'),
path.join(serveDir, 'sandbox.ts'),
).toString();
expect(resolveSeatbeltProfileFile('restrictive-closed', sourceUrl)).toBe(
path.join(utilsDir, 'sandbox-macos-restrictive-closed.sb'),
path.join(serveDir, 'sandbox-macos-restrictive-closed.sb'),
);
});
it('keeps every builtin seatbelt profile colocated with the real module', () => {
// Uses the default `import.meta.url` (the real module location), so this
// fails loudly if sandbox.ts or the .sb profiles move without the other.
// Iterate the module's own list rather than a hand-copied snapshot, so a
// profile added to `BUILTIN_SEATBELT_PROFILES` without its `.sb` file
// fails here instead of on a `sandbox-exec` ENOENT at launch. The length
// guard keeps an emptied list from passing the loop vacuously.
expect(BUILTIN_SEATBELT_PROFILES.length).toBeGreaterThan(0);
for (const profile of BUILTIN_SEATBELT_PROFILES) {
const profileFile = resolveSeatbeltProfileFile(profile);
expect(fs.existsSync(profileFile), `missing ${profileFile}`).toBe(true);
}
});
it('keeps custom seatbelt profiles under project settings', () => {
const bundleDir = path.resolve(path.sep, 'tmp', 'qwen', 'lib');
const chunkUrl = pathToFileURL(

View file

@ -22,15 +22,15 @@ import {
resolveBundleDir,
} from '@qwen-code/qwen-code-core';
import { randomBytes } from 'node:crypto';
import { writeStderrLine } from './stdioHelpers.js';
import { parseSandboxImageName } from './sandboxImageName.js';
import { isContainerPathWithinWorkdir } from './sandbox-path.js';
import { parseSandboxMountSpec } from './sandboxMounts.js';
import { writeStderrLine } from '../utils/stdioHelpers.js';
import { parseSandboxImageName } from '../utils/sandboxImageName.js';
import { isContainerPathWithinWorkdir } from '../utils/sandbox-path.js';
import { parseSandboxMountSpec } from '../utils/sandboxMounts.js';
import {
CUSTOM_SANDBOX_IMAGE_ENV_VAR,
HOST_UPDATE_RELAUNCH_ENV_VAR,
SKIP_UPDATE_CHECK_ENV_VAR,
} from './processUtils.js';
} from '../utils/processUtils.js';
import {
QWEN_CODE_DESKTOP_ENV,
QWEN_CODE_SERVE_ENV,
@ -61,7 +61,12 @@ function ensureDirectoryAndGetRealPath(dir: string): string {
const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'qwen-code-sandbox';
const SANDBOX_NETWORK_NAME = 'qwen-code-sandbox';
const SANDBOX_PROXY_NAME = 'qwen-code-sandbox-proxy';
const BUILTIN_SEATBELT_PROFILES = [
/**
* Exported so the colocation tripwire in `sandbox.test.ts` can iterate every
* builtin profile by construction instead of pinning a hand-copied snapshot
* that silently stops at the list as written.
*/
export const BUILTIN_SEATBELT_PROFILES = [
'permissive-open',
'permissive-closed',
'permissive-proxied',

View file

@ -16,15 +16,18 @@ const mocks = vi.hoisted(() => ({
runThrottledOnce: vi.fn(),
}));
vi.mock('./cleanup.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./cleanup.js')>();
vi.mock('../../utils/housekeeping/cleanup.js', async (importOriginal) => {
const actual =
await importOriginal<
typeof import('../../utils/housekeeping/cleanup.js')
>();
return {
...actual,
cleanupOldOpenAILogs: mocks.cleanupOldOpenAILogs,
};
});
vi.mock('./throttledOnce.js', () => ({
vi.mock('../../utils/housekeeping/throttledOnce.js', () => ({
runThrottledOnce: mocks.runThrottledOnce,
}));

View file

@ -25,7 +25,7 @@ import {
noteInteraction,
_resetForTesting as resetInteraction,
_setLastInteractionForTesting,
} from './lastInteractionAt.js';
} from '../../utils/housekeeping/lastInteractionAt.js';
const MS_PER_HOUR = 60 * 60 * 1000;
const MS_PER_DAY = 24 * MS_PER_HOUR;

View file

@ -21,9 +21,9 @@ import {
cleanupOldOpenAILogs,
cleanupOldSubagentTranscripts,
getCutoffDate,
} from './cleanup.js';
import { runThrottledOnce } from './throttledOnce.js';
import { msSinceLastInteraction } from './lastInteractionAt.js';
} from '../../utils/housekeeping/cleanup.js';
import { runThrottledOnce } from '../../utils/housekeeping/throttledOnce.js';
import { msSinceLastInteraction } from '../../utils/housekeeping/lastInteractionAt.js';
const debugLogger = createDebugLogger('HOUSEKEEPING');

View file

@ -64,7 +64,7 @@ vi.mock('../utils/processUtils.js', () => ({
requestUpdateOnExit: (...args: unknown[]) => mockRequestUpdateOnExit(...args),
}));
vi.mock('../utils/handleAutoUpdate.js', () => ({
vi.mock('../ui/handleAutoUpdate.js', () => ({
handleAutoUpdate: (...args: unknown[]) => mockHandleAutoUpdate(...args),
}));
@ -92,7 +92,7 @@ vi.mock('../core/initializer.js', () => ({
mockConnectIdeForStartup(...args),
}));
vi.mock('../utils/housekeeping/scheduler.js', () => ({
vi.mock('../services/housekeeping/scheduler.js', () => ({
startBackgroundHousekeeping: (...args: unknown[]) =>
mockStartBackgroundHousekeeping(...args),
}));

View file

@ -166,7 +166,7 @@ export function startPostRenderPrefetches(
{ t },
] = await Promise.all([
import('../ui/utils/updateCheck.js'),
import('../utils/handleAutoUpdate.js'),
import('../ui/handleAutoUpdate.js'),
import('../utils/installationInfo.js'),
import('../utils/updateEventEmitter.js'),
import('../i18n/index.js'),
@ -273,7 +273,7 @@ export function startPostRenderPrefetches(
if (config.isInteractive()) {
runDeferredTask('background_housekeeping', async () => {
const { startBackgroundHousekeeping } = await import(
'../utils/housekeeping/scheduler.js'
'../services/housekeeping/scheduler.js'
);
startBackgroundHousekeeping(config, settings);
});

View file

@ -184,7 +184,7 @@ vi.mock('../services/prompt-stash.js');
// Mock external utilities
vi.mock('../utils/events.js');
vi.mock('../utils/handleAutoUpdate.js');
vi.mock('./handleAutoUpdate.js');
vi.mock('../utils/cleanup.js');
const mockLoadHierarchicalGeminiMemory = vi.hoisted(() => vi.fn());

View file

@ -179,7 +179,7 @@ import {
detectWorkflowKeyword,
buildWorkflowSteeringNotice,
} from './utils/workflow-keyword.js';
import { parseSlashCommand } from '../utils/commands.js';
import { parseSlashCommand } from './commands/commands.js';
import { type LoadedSettings, SettingScope } from '../config/settings.js';
import { type InitializationResult } from '../core/initializer.js';
import { ExtensionRefreshState } from '../config/extension-refresh-state.js';
@ -199,7 +199,7 @@ import { useCommandMigration } from './hooks/useCommandMigration.js';
import { migrateTomlCommands } from '../services/command-migration-tool.js';
import { sendNotification } from '../services/notificationService.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { setUpdateHandler } from './handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import {
useMessageQueue,
@ -231,8 +231,8 @@ import {
} from './contexts/BackgroundTaskViewContext.js';
import { getLiveAgentPanelLayoutKey } from './components/background-view/liveAgentPanelVisibility.js';
import { t } from '../i18n/index.js';
import { TUI_CHAT_RECORDING_FAILURE_MESSAGE } from '../utils/chat-recording-failure.js';
import { buildPermissionSuggestions } from '../utils/permission-suggestions.js';
import { TUI_CHAT_RECORDING_FAILURE_MESSAGE } from '../nonInteractive/chat-recording-failure.js';
import { buildPermissionSuggestions } from '../nonInteractive/permission-suggestions.js';
import { useWelcomeBack } from './hooks/useWelcomeBack.js';
import { useDialogClose } from './hooks/useDialogClose.js';
import { useInitializationAuthError } from './hooks/useInitializationAuthError.js';

View file

@ -30,9 +30,9 @@ vi.mock('../hooks/useQwenAuth.js', () => ({
})),
}));
vi.mock('../../utils/settingsUtils.js', async (importOriginal) => {
vi.mock('../../config/settingsUtils.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../../utils/settingsUtils.js')>();
await importOriginal<typeof import('../../config/settingsUtils.js')>();
return {
...actual,
backupSettingsFile: vi.fn(),

View file

@ -9,10 +9,10 @@ import { aboutCommand } from './aboutCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import * as systemInfoUtils from '../../utils/systemInfo.js';
import * as systemInfoUtils from '../systemInfo.js';
import * as sessionPathsUtils from '../../utils/sessionPaths.js';
vi.mock('../../utils/systemInfo.js');
vi.mock('../systemInfo.js');
vi.mock('../../utils/sessionPaths.js');
describe('aboutCommand', () => {

View file

@ -7,7 +7,7 @@
import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js';
import { MessageType, type HistoryItemAbout } from '../types.js';
import { getExtendedSystemInfo } from '../../utils/systemInfo.js';
import { getExtendedSystemInfo } from '../systemInfo.js';
import { t } from '../../i18n/index.js';
import {
collectSessionPathInfo,

View file

@ -9,7 +9,7 @@ import { bugCommand } from './bugCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { AuthType } from '@qwen-code/qwen-code-core';
import * as systemInfoUtils from '../../utils/systemInfo.js';
import * as systemInfoUtils from '../systemInfo.js';
const mockOpenBrowserSecurely = vi.hoisted(() => vi.fn());
@ -22,7 +22,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
openBrowserSecurely: mockOpenBrowserSecurely,
};
});
vi.mock('../../utils/systemInfo.js');
vi.mock('../systemInfo.js');
describe('bugCommand', () => {
beforeEach(() => {

View file

@ -11,8 +11,8 @@ import {
} from './types.js';
import { openBrowserSecurely } from '@qwen-code/qwen-code-core';
import { MessageType, type HistoryItem } from '../types.js';
import { getExtendedSystemInfo } from '../../utils/systemInfo.js';
import { getSystemInfoFields } from '../../utils/systemInfoFields.js';
import { getExtendedSystemInfo } from '../systemInfo.js';
import { getSystemInfoFields } from '../systemInfoFields.js';
import { t } from '../../i18n/index.js';
export const bugCommand: SlashCommand = {

View file

@ -10,7 +10,7 @@ import {
parseSlashCommand,
parseStackedSlashCommands,
} from './commands.js';
import { CommandKind, type SlashCommand } from '../ui/commands/types.js';
import { CommandKind, type SlashCommand } from './types.js';
// Mock command structure for testing
const mockCommands: readonly SlashCommand[] = [

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandKind, type SlashCommand } from '../ui/commands/types.js';
import { CommandKind, type SlashCommand } from './types.js';
/** Maximum number of stacked skill commands that can be loaded in one prompt. */
export const MAX_STACKED_SKILLS = 5;

View file

@ -19,7 +19,7 @@ import {
getNestedProperty,
getSettingDefinition,
validateSettingValue,
} from '../../utils/settingsUtils.js';
} from '../../config/settingsUtils.js';
const SETTABLE_TYPES = new Set(['boolean', 'string', 'number', 'enum']);

View file

@ -6,14 +6,14 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { runDoctorChecks } from './doctorChecks.js';
import { type CommandContext } from '../ui/commands/types.js';
import { createMockCommandContext } from '../test-utils/mockCommandContext.js';
import * as systemInfoUtils from './systemInfo.js';
import * as authModule from '../config/auth.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import * as systemInfoUtils from '../systemInfo.js';
import * as authModule from '../../config/auth.js';
import * as allProviders from '@qwen-code/qwen-code-core';
vi.mock('./systemInfo.js');
vi.mock('../config/auth.js');
vi.mock('../systemInfo.js');
vi.mock('../../config/auth.js');
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actual =
(await importOriginal()) as typeof import('@qwen-code/qwen-code-core');

View file

@ -6,17 +6,17 @@
import process from 'node:process';
import os from 'node:os';
import { getNpmVersion, getGitVersion } from './systemInfo.js';
import { validateAuthMethod } from '../config/auth.js';
import { getNpmVersion, getGitVersion } from '../systemInfo.js';
import { validateAuthMethod } from '../../config/auth.js';
import {
findProviderByCredentials,
canUseRipgrep,
getMCPServerStatus,
MCPServerStatus,
} from '@qwen-code/qwen-code-core';
import type { CommandContext } from '../ui/commands/types.js';
import type { DoctorCheckResult } from '../ui/types.js';
import { t } from '../i18n/index.js';
import type { CommandContext } from './types.js';
import type { DoctorCheckResult } from '../types.js';
import { t } from '../../i18n/index.js';
const MIN_NODE_MAJOR = 22;

View file

@ -8,14 +8,14 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { doctorCommand } from './doctorCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import * as doctorChecksModule from '../../utils/doctorChecks.js';
import * as doctorChecksModule from './doctorChecks.js';
import * as memoryDiagnosticsModule from '../../utils/memoryDiagnostics.js';
import * as cpuProfilerModule from '../../utils/cpuProfiler.js';
import { collectMemoryDiagnostics } from '@qwen-code/qwen-code-core';
import type { Content } from '@google/genai';
import type { DoctorCheckResult } from '../types.js';
vi.mock('../../utils/doctorChecks.js');
vi.mock('./doctorChecks.js');
vi.mock('../../utils/memoryDiagnostics.js');
vi.mock('../../utils/cpuProfiler.js');
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({

View file

@ -7,7 +7,7 @@
import type { CommandContext, SlashCommand } from './types.js';
import { CommandKind } from './types.js';
import type { HistoryItemDoctor } from '../types.js';
import { runDoctorChecks } from '../../utils/doctorChecks.js';
import { runDoctorChecks } from './doctorChecks.js';
import {
collectMemoryPressureSamples,
formatMemoryDiagnostics,
@ -21,7 +21,7 @@ import {
startCpuProfile,
stopCpuProfile,
} from '../../utils/cpuProfiler.js';
import { rollbackStandaloneUpdate } from '../../utils/standalone-update.js';
import { rollbackStandaloneUpdate } from '../standalone-update.js';
import { getInstallationInfo } from '../../utils/installationInfo.js';
import { t } from '../../i18n/index.js';
import {

View file

@ -79,7 +79,7 @@ import { languageCommand } from './languageCommand.js';
import {
initializeLlmOutputLanguage,
writeOutputLanguageFile,
} from '../../utils/languageUtils.js';
} from '../../i18n/languageUtils.js';
describe('languageCommand', () => {
let mockContext: CommandContext;

View file

@ -28,7 +28,7 @@ import {
isAutoLanguage,
resolveOutputLanguageOrPreserveAuto,
writeOutputLanguageAndRegisterPath,
} from '../../utils/languageUtils.js';
} from '../../i18n/languageUtils.js';
import { createDebugLogger } from '@qwen-code/qwen-code-core';
const debugLogger = createDebugLogger('LANGUAGE_COMMAND');

View file

@ -45,7 +45,7 @@ vi.mock('../../utils/processUtils.js', () => ({
HOST_UPDATE_RELAUNCH_ENV_VAR: 'QWEN_CODE_HOST_UPDATE_RELAUNCH',
relaunchForUpdate,
}));
vi.mock('../../utils/standalone-update.js', () => ({
vi.mock('../standalone-update.js', () => ({
performStandaloneUpdate,
}));
vi.mock('../../utils/installationInfo.js', () => ({

Some files were not shown because too many files have changed in this diff Show more