/** * @license * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import { existsSync, readFileSync, readdirSync, mkdtempSync, mkdirSync, rmSync, } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import { parse } from 'yaml'; describe('TUI product evidence', () => { const workflow = parse( readFileSync('.github/workflows/tui-parity.yml', 'utf8'), ); const steps = workflow.jobs.parity.steps; const classify = new (Object.getPrototypeOf(async () => {}).constructor)( 'github', 'context', 'core', workflow.jobs.changes.steps[0].with.script, ); const serverFile = 'packages/managed-agent-server/src/main/java/Service.java'; const companionFile = 'packages/vscode-ide-companion/src/services/settingsWriter.ts'; async function impact( files, count = files.length, eventName = 'pull_request', apiError, ) { const github = { paginate: vi.fn().mockResolvedValue(files), rest: { pulls: { listFiles: vi.fn() } }, }; if (apiError) github.paginate.mockRejectedValue(apiError); const outputs = {}; const summary = { addHeading: vi.fn().mockReturnThis(), addTable: vi.fn().mockReturnThis(), addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue(), }; const core = { setOutput: (key, value) => { outputs[key] = value; }, setFailed: vi.fn(), summary, }; // A real dispatch payload carries no `pull_request` key. Handing the // dispatch case a populated one would keep this suite green if the // `context.payload.pull_request` read were ever hoisted above the // eventName guard, while every real dispatch then throws. const payload = eventName === 'pull_request' ? { pull_request: { number: 15261, changed_files: count } } : {}; await classify( github, { eventName, repo: { owner: 'QwenLM', repo: 'qwen-code' }, payload, }, core, ); if (eventName === 'pull_request') { expect(github.paginate).toHaveBeenCalledWith( github.rest.pulls.listFiles, { owner: 'QwenLM', repo: 'qwen-code', pull_number: 15261, per_page: 100, }, ); } else { expect(github.paginate).not.toHaveBeenCalled(); } expect(summary.write).toHaveBeenCalledOnce(); expect(summary.addRaw).toHaveBeenCalledWith(outputs.reason); return { outputs, core }; } const required = async (...args) => (await impact(...args)).outputs.component === 'true'; const condition = ( expression, outputs, result = 'success', cancelled = false, event = 'pull_request', ) => { if (!expression) return true; return new Function( 'needs', 'cancelled', 'github', `return (${expression.slice(3, -2)});`, )({ changes: { result, outputs } }, () => cancelled, { event_name: event }); }; const named = (job, prefix) => workflow.jobs[job].steps.find((step) => step.name?.startsWith(prefix)); const lanes = [ 'component', 'render', 'input', 'recovery', 'integration', 'full', ]; const flags = (selected) => Object.fromEntries( lanes.map((lane) => [lane, String(selected.includes(lane))]), ); it.each([ ['packages/cli/src/ui/components/LoadingIndicator.tsx', ['component']], ['packages/core/src/utils/debugLogger.ts', ['component']], ['packages/new-dependency/src/unknown.ts', ['component']], [ 'packages/cli/src/ui/opentui/status-line-text.ts', ['component', 'render'], ], ['packages/core/src/telemetry/uiTelemetry.ts', ['component', 'render']], ['packages/cli/src/ui/opentui/clipboard.ts', ['component', 'input']], [ 'packages/cli/src/ui/components/shared/text-buffer.ts', ['component', 'input'], ], [ 'packages/core/src/services/fileHistoryService.ts', ['component', 'recovery'], ], [ 'packages/core/src/services/visionBridge/vision-bridge-service.ts', ['component', 'input', 'recovery'], ], [ 'packages/cli/src/ui/contexts/SessionContext.tsx', ['component', 'render', 'recovery'], ], ['packages/core/src/config/config.ts', lanes], ['packages/cli/src/llm.tsx', lanes], ['packages/cli/src/utils/tui-runtime.ts', lanes], ['packages/cli/src/utils/npm-runtime.ts', lanes], ['packages/cli/src/ui/opentui/opentui-assets.ts', lanes], ['scripts/tests/unrelated.test.js', ['component']], [ 'packages/core/src/tools/write-file.ts', ['component', 'recovery', 'integration'], ], [ 'packages/core/src/services/shellExecutionService.ts', ['component', 'integration'], ], [ 'packages/cli/src/config/extension-runtime-reload.ts', ['component', 'integration'], ], ['packages/cli/src/dualOutput/index.ts', ['component', 'integration']], ['packages/core/src/core/coreToolScheduler.ts', lanes.slice(0, -1)], ['packages/core/src/core/client.ts', lanes.slice(0, -1)], ['packages/cli/src/ui/opentui/live-turn.ts', lanes.slice(0, -1)], ['packages/cli/src/ui/opentui/opentui-runtime.ts', lanes], ['pnpm-lock.yaml', lanes], ['packages/cli/package.json', lanes], ['scripts/tui-parity/component-parity.sh', lanes], ['integration-tests/test-helper.ts', lanes], ['.github/workflows/tui-parity.yml', lanes], ['docs/design/unrelated.md', []], ['packages/web-shell/client/App.tsx', []], ['packages/chrome-extension/package.json', []], ['packages/mobile-mcp/package.json', []], ['packages/qwen-live/build.mjs', []], ['integrations/external-context/package.json', []], ['integrations/external-context-mem0/package.json', []], ['integrations/test-failure-explorer/package.json', []], ['packages/desktop/package.json', []], ['packages/live-host/electron-builder.yml', []], ['packages/mobile-shell/build.gradle.kts', []], ['packages/sdk-java/client/pom.xml', []], ['packages/sdk-python/pyproject.toml', []], ['packages/zed-extension/extension.toml', []], ['packages/cua-driver/typescript/src/native-assets.ts', []], ['scripts/unrelated-maintenance.js', ['component']], ])( 'selects mechanisms rather than blanket full acceptance for %s', async (filename, selected) => { if (!/unknown|unrelated|new-dependency/.test(filename)) { expect(existsSync(filename), filename).toBe(true); } const { outputs, core } = await impact([{ filename }]); expect(core.setFailed).not.toHaveBeenCalled(); expect(outputs).toMatchObject(flags(selected)); expect(condition(workflow.jobs.parity.if, outputs)).toBe( selected.includes('component'), ); expect(condition(workflow.jobs.noflicker.if, outputs)).toBe( selected.some((lane) => ['render', 'recovery', 'full'].includes(lane)), ); expect(condition(named('parity', 'Component parity').if, outputs)).toBe( selected.includes('component'), ); expect(condition(named('parity', 'Bundle the product').if, outputs)).toBe( selected.some((lane) => lane !== 'component'), ); expect(condition(named('parity', 'Three-leg product').if, outputs)).toBe( selected.includes('full'), ); expect(condition(named('parity', 'Targeted product').if, outputs)).toBe( !selected.includes('full') && selected.some((lane) => lane !== 'component'), ); expect( condition(named('noflicker', 'OpenTUI no-flicker').if, outputs), ).toBe(selected.includes('render')); for (const prefix of ['Bundle the product', 'Cold checkpoint recovery']) { expect(condition(named('noflicker', prefix).if, outputs)).toBe( selected.includes('recovery'), ); } if (selected.length === 1) expect(outputs.reason).toContain( 'maintainers should consider manual full', ); }, ); it('unions both rename paths and reports only fixed reasons, never untrusted path text', async () => { const filename = 'packages/unknown/$(touch injected)\n