diff --git a/eslint.config.js b/eslint.config.js index 84a3f66a3f..66533fd13a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -83,7 +83,7 @@ export default tseslint.config( { patterns: [ { - group: ['**/serve/*', '**/serve/**'], + group: ['**/serve', '**/serve/*', '**/serve/**'], message: 'acp-integration must not import serve/ internals. Put shared, lifecycle-free logic in packages/cli/src/runtime/ instead (#8084).', }, diff --git a/scripts/tests/acp-serve-boundary-guard.test.js b/scripts/tests/acp-serve-boundary-guard.test.js new file mode 100644 index 0000000000..6691e60e21 --- /dev/null +++ b/scripts/tests/acp-serve-boundary-guard.test.js @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ESLint } from 'eslint'; +import { expect, it } from 'vitest'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '../..'); + +const eslint = new ESLint({ cwd: root }); + +async function restrictedReports(statement) { + const filePath = join( + root, + 'packages/cli/src/acp-integration/boundary-probe.ts', + ); + const [result] = await eslint.lintText(`${statement}\n`, { filePath }); + return result.messages.filter((m) => m.ruleId === 'no-restricted-imports'); +} + +// Bare-directory specifiers resolve to packages/cli/src/serve/index.ts, a +// barrel re-exporting the full daemon surface — they must be caught by the +// same guard that blocks deep serve/ internals (#8084). +it.each(['../serve', '../../serve'])( + 'blocks the bare barrel specifier %s from acp-integration', + async (specifier) => { + const reports = await restrictedReports( + `import { createServeApp } from '${specifier}';`, + ); + expect(reports).toHaveLength(1); + expect(reports[0].message).toContain('acp-integration'); + }, +); + +it('blocks a bare barrel re-export from acp-integration', async () => { + const reports = await restrictedReports( + `export { createServeApp } from '../serve';`, + ); + expect(reports).toHaveLength(1); +}); + +it('still blocks deep serve/ internals from acp-integration', async () => { + const reports = await restrictedReports( + `import { createServeApp } from '../serve/index.js';`, + ); + expect(reports).toHaveLength(1); +}); + +it('allows neutral runtime/ contracts from acp-integration', async () => { + const reports = await restrictedReports( + `import { something } from '../runtime/contracts.js';`, + ); + expect(reports).toHaveLength(0); +}); diff --git a/scripts/tests/cross-package-contracts.test.js b/scripts/tests/cross-package-contracts.test.js index e9a47fe80a..ce3afc86f8 100644 --- a/scripts/tests/cross-package-contracts.test.js +++ b/scripts/tests/cross-package-contracts.test.js @@ -55,7 +55,7 @@ const imports = [ ], [ 'LIVE_TASK_TOOL_NAMES', - 'packages/cli/src/serve/live/live-task-tools.ts', + 'packages/cli/src/acp-integration/live/live-task-tools.ts', '@qwen-code/acp-bridge/bridgeOptions', ], [ @@ -65,7 +65,7 @@ const imports = [ ], [ 'LiveTaskToolName', - 'packages/cli/src/serve/live/live-task-tools.ts', + 'packages/cli/src/acp-integration/live/live-task-tools.ts', '@qwen-code/acp-bridge/bridgeOptions', ], [