+
+
diff --git a/packages/ui/src/components/SelectWithConfig.vue b/packages/ui/src/components/SelectWithConfig.vue
index 67efae52..d4da7e36 100644
--- a/packages/ui/src/components/SelectWithConfig.vue
+++ b/packages/ui/src/components/SelectWithConfig.vue
@@ -13,6 +13,7 @@
{{ emptyText || t('model.select.noAvailableModels') }}
[key, '']))
+ : {}),
...(USE_VCR_PLACEHOLDER_KEYS
? {
VITE_SILICONFLOW_API_KEY: process.env.VITE_SILICONFLOW_API_KEY || 'vcr',
diff --git a/scripts/e2e-groups.js b/scripts/e2e-groups.js
index 45e2f242..e6273ba6 100644
--- a/scripts/e2e-groups.js
+++ b/scripts/e2e-groups.js
@@ -1,4 +1,7 @@
const groups = {
+ uiSmoke: [
+ 'tests/e2e/regression/model-configuration-smoke.spec.ts',
+ ],
gate: [
'tests/e2e/workflows/p0-route-smoke.spec.ts',
'tests/e2e/regression/root-route-bootstrap.spec.ts',
diff --git a/scripts/package-scripts.test.mjs b/scripts/package-scripts.test.mjs
index 64ecd219..9270985c 100644
--- a/scripts/package-scripts.test.mjs
+++ b/scripts/package-scripts.test.mjs
@@ -90,3 +90,18 @@ test('mcp-server bin points to a file that exists before build output is generat
`Expected ${binTargetPath} to exist so pnpm can create the workspace bin shim during install`,
)
})
+
+test('desktop package includes every runtime window icon', () => {
+ const desktopPackage = readJson(path.join('packages', 'desktop', 'package.json'))
+ const packagedFiles = desktopPackage.build?.files ?? []
+
+ assert.ok(
+ packagedFiles.includes('icons/**/*'),
+ 'Desktop build.files must include the icons directory used by main.js at runtime',
+ )
+
+ for (const iconName of ['app-icon.ico', 'app-icon.icns', 'app-icon.png']) {
+ const iconPath = path.join(process.cwd(), 'packages', 'desktop', 'icons', iconName)
+ assert.equal(fs.existsSync(iconPath), true, `Expected runtime icon ${iconPath} to exist`)
+ }
+})
diff --git a/scripts/pnpm-runner.js b/scripts/pnpm-runner.js
new file mode 100644
index 00000000..78be4375
--- /dev/null
+++ b/scripts/pnpm-runner.js
@@ -0,0 +1,39 @@
+const { spawn, spawnSync } = require('node:child_process')
+
+function getPnpmRunnerSpec(platform = process.platform, env = process.env) {
+ if (platform === 'win32') {
+ return {
+ command: env.ComSpec || env.COMSPEC || 'cmd.exe',
+ argsPrefix: ['/d', '/s', '/c', 'pnpm'],
+ shell: false,
+ }
+ }
+
+ return {
+ command: 'pnpm',
+ argsPrefix: [],
+ shell: false,
+ }
+}
+
+function spawnPnpm(args, options = {}) {
+ const { command, argsPrefix, shell } = getPnpmRunnerSpec()
+ return spawn(command, [...argsPrefix, ...args], {
+ shell,
+ ...options,
+ })
+}
+
+function spawnPnpmSync(args, options = {}) {
+ const { command, argsPrefix, shell } = getPnpmRunnerSpec()
+ return spawnSync(command, [...argsPrefix, ...args], {
+ shell,
+ ...options,
+ })
+}
+
+module.exports = {
+ getPnpmRunnerSpec,
+ spawnPnpm,
+ spawnPnpmSync,
+}
diff --git a/scripts/run-e2e-group.js b/scripts/run-e2e-group.js
index 720dc4be..d96e1273 100644
--- a/scripts/run-e2e-group.js
+++ b/scripts/run-e2e-group.js
@@ -1,7 +1,7 @@
#!/usr/bin/env node
-const { spawnSync } = require('child_process')
const { groups } = require('./e2e-groups')
+const { spawnPnpmSync } = require('./pnpm-runner')
function runGroup(groupName, extraArgs = []) {
const specs = groups[groupName]
@@ -17,11 +17,9 @@ function runGroup(groupName, extraArgs = []) {
}
console.log('')
- const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
- const result = spawnSync(pnpmCommand, ['exec', 'playwright', 'test', ...specs, ...extraArgs], {
+ const result = spawnPnpmSync(['exec', 'playwright', 'test', ...specs, ...extraArgs], {
stdio: 'inherit',
- env: process.env,
- shell: process.platform === 'win32'
+ env: process.env
})
if (result.error) {
diff --git a/scripts/run-e2e-record.js b/scripts/run-e2e-record.js
index ee313abe..e5a7dd22 100644
--- a/scripts/run-e2e-record.js
+++ b/scripts/run-e2e-record.js
@@ -1,6 +1,6 @@
#!/usr/bin/env node
-const { spawnSync } = require('child_process')
+const { spawnPnpmSync } = require('./pnpm-runner')
const extraArgs = process.argv.slice(2)
@@ -10,14 +10,12 @@ if (extraArgs.length === 0) {
process.exit(1)
}
-const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
-const result = spawnSync(pnpmCommand, ['exec', 'playwright', 'test', ...extraArgs], {
+const result = spawnPnpmSync(['exec', 'playwright', 'test', ...extraArgs], {
stdio: 'inherit',
env: {
...process.env,
E2E_VCR_MODE: 'record'
- },
- shell: process.platform === 'win32'
+ }
})
if (result.error) {
diff --git a/scripts/run-many.js b/scripts/run-many.js
index f4bca4d2..0b18063f 100644
--- a/scripts/run-many.js
+++ b/scripts/run-many.js
@@ -1,20 +1,6 @@
#!/usr/bin/env node
-const { spawn } = require('node:child_process');
-
-function getRunnerSpec(platform = process.platform) {
- if (platform === 'win32') {
- return {
- command: 'pnpm',
- shell: true,
- };
- }
-
- return {
- command: 'pnpm',
- shell: false,
- };
-}
+const { getPnpmRunnerSpec, spawnPnpm } = require('./pnpm-runner');
function parseArgs(argv) {
let parallel = false;
@@ -40,10 +26,8 @@ function parseArgs(argv) {
function runPackageScript(script, options = {}) {
return new Promise((resolve, reject) => {
- const { command, shell } = getRunnerSpec();
- const child = spawn(command, ['run', script], {
+ const child = spawnPnpm(['run', script], {
stdio: 'inherit',
- shell,
env: process.env,
...options,
});
@@ -90,7 +74,7 @@ if (require.main === module) {
}
module.exports = {
- getRunnerSpec,
+ getRunnerSpec: getPnpmRunnerSpec,
main,
parseArgs,
runPackageScript,
diff --git a/scripts/run-many.test.mjs b/scripts/run-many.test.mjs
index badc19c8..b68265fc 100644
--- a/scripts/run-many.test.mjs
+++ b/scripts/run-many.test.mjs
@@ -21,13 +21,15 @@ test('parseArgs supports parallel aliases', () => {
});
});
-test('getRunnerSpec uses shell mode for Windows pnpm invocation', () => {
- assert.deepEqual(getRunnerSpec('win32'), {
- command: 'pnpm',
- shell: true,
+test('getRunnerSpec avoids shell mode while preserving Windows pnpm compatibility', () => {
+ assert.deepEqual(getRunnerSpec('win32', { ComSpec: 'C:\\Windows\\System32\\cmd.exe' }), {
+ command: 'C:\\Windows\\System32\\cmd.exe',
+ argsPrefix: ['/d', '/s', '/c', 'pnpm'],
+ shell: false,
});
assert.deepEqual(getRunnerSpec('linux'), {
command: 'pnpm',
+ argsPrefix: [],
shell: false,
});
});
diff --git a/tests/e2e/regression/model-configuration-smoke.spec.ts b/tests/e2e/regression/model-configuration-smoke.spec.ts
new file mode 100644
index 00000000..edcfa781
--- /dev/null
+++ b/tests/e2e/regression/model-configuration-smoke.spec.ts
@@ -0,0 +1,34 @@
+import { test, expect } from '../fixtures'
+
+test.describe('Model configuration smoke', () => {
+ test('first-use model selector opens a manager with switchable text, image, and function tabs', async ({ page }) => {
+ await page.goto('/')
+ await expect(page.locator('[data-testid="workspace"][data-mode="basic-system"]')).toBeVisible({
+ timeout: 45000,
+ })
+
+ const modelSelect = page.getByTestId('basic-system-model-select')
+ await expect(modelSelect).toBeVisible()
+ await modelSelect.click()
+
+ const emptyConfigAction = page.getByTestId('select-empty-config-action')
+ await expect(emptyConfigAction).toBeVisible()
+ await expect(page.getByText(/暂无可用模型|No available models/)).toBeVisible()
+ await emptyConfigAction.click()
+
+ const dialog = page.getByTestId('model-manager-dialog')
+ await expect(dialog).toBeVisible()
+
+ const tabs = page.getByTestId('model-manager-tabs')
+ const tabItems = tabs.locator('.n-tabs-tab')
+ await expect(tabs).toContainText(/文本模型|Text Models/)
+ await expect(tabs).toContainText(/图像模型|Image Models/)
+ await expect(tabs).toContainText(/功能模型|Function Models/)
+
+ for (const tabLabel of [/图像模型|Image Models/, /功能模型|Function Models/, /文本模型|Text Models/]) {
+ const tab = tabItems.filter({ hasText: tabLabel }).first()
+ await tab.click()
+ await expect(tab).toHaveClass(/n-tabs-tab--active/)
+ }
+ })
+})