0bfc5768cc
让Linux no-bundle smoke不再误用Windows随包资源目标。 统一dev与release的Cargo目标和feature解析,补充跨平台回归测试。 同步随包资源staging技术方案中的实际构建目标约定。
628 lines
19 KiB
TypeScript
628 lines
19 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
||
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
|
||
import { describe, expect, test, vi } from 'vitest';
|
||
|
||
import {
|
||
readCargoTarget,
|
||
resolveEditorFeatures,
|
||
} from '../scripts/cargo-features.mjs';
|
||
import {
|
||
isProcessGroupAlive,
|
||
spawnChild,
|
||
terminateChildTree,
|
||
} from '../scripts/start-dev-stack.mjs';
|
||
import {
|
||
buildTauriArguments,
|
||
buildTauriDevProcessEnv,
|
||
prepareBundledResourcesBeforeTauri,
|
||
runTauriDev as runTauriDevImpl,
|
||
withDevCargoFeatures,
|
||
} from '../scripts/start-tauri-dev.mjs';
|
||
|
||
const testEndpoint = {
|
||
host: '127.0.0.1',
|
||
port: 10005,
|
||
url: 'http://127.0.0.1:10005/',
|
||
markerUrl: 'http://127.0.0.1:10005/__agc_dev_server.json',
|
||
portRange: { start: 10000, end: 10099, label: '10000-10099' },
|
||
};
|
||
const resolveTestEndpoint = async () => testEndpoint;
|
||
const runTauriDev = (
|
||
argv: string[],
|
||
options: Parameters<typeof runTauriDevImpl>[1],
|
||
) =>
|
||
runTauriDevImpl(argv, {
|
||
prepareFrontend: async () => {},
|
||
// 随包资源准备会读取真实上游包与仓库插件工作区;需要断言的用例自行注入。
|
||
prepareResources: () => {},
|
||
...options,
|
||
});
|
||
|
||
async function waitForFile(path: string, timeoutMs = 5000) {
|
||
const deadline = Date.now() + timeoutMs;
|
||
while (Date.now() < deadline) {
|
||
if (existsSync(path)) {
|
||
return;
|
||
}
|
||
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
||
}
|
||
throw new Error(`等待测试进程标记超时: ${path}`);
|
||
}
|
||
|
||
describe('AI 游戏创作 Tauri dev 启动参数', () => {
|
||
test('开发构建默认带上 Cocos 编辑器 feature', () => {
|
||
expect(
|
||
withDevCargoFeatures(['--no-watch'], ['cocos-editor-execute']),
|
||
).toEqual(['--features=cocos-editor-execute', '--no-watch']);
|
||
expect(withDevCargoFeatures(['--no-watch'], [])).toEqual(['--no-watch']);
|
||
expect(
|
||
withDevCargoFeatures(
|
||
['--features', 'custom-feature'],
|
||
['cocos-editor-execute'],
|
||
),
|
||
).toEqual(['--features', 'custom-feature']);
|
||
});
|
||
|
||
test('普通 dev 参数原样交给 Tauri CLI', () => {
|
||
expect(buildTauriArguments(['--no-watch'], testEndpoint.url)).toEqual([
|
||
'dev',
|
||
'--no-watch',
|
||
'--config',
|
||
'{"build":{"devUrl":"http://127.0.0.1:10005/","beforeDevCommand":""}}',
|
||
]);
|
||
});
|
||
|
||
test('动态 devUrl 配置位于用户 Tauri 配置之后且不越过参数分隔符', () => {
|
||
expect(
|
||
buildTauriArguments(
|
||
['--config', 'custom.json', '--', '--', '--example-app-arg'],
|
||
testEndpoint.url,
|
||
),
|
||
).toEqual([
|
||
'dev',
|
||
'--config',
|
||
'custom.json',
|
||
'--config',
|
||
'{"build":{"devUrl":"http://127.0.0.1:10005/","beforeDevCommand":""}}',
|
||
'--',
|
||
'--',
|
||
'--example-app-arg',
|
||
]);
|
||
});
|
||
|
||
test('单个启动器分隔符后的参数进入应用而不是 Cargo', () => {
|
||
expect(
|
||
buildTauriArguments(
|
||
['--', '--config-dir', 'C:\\temp\\agc-dev-config'],
|
||
testEndpoint.url,
|
||
),
|
||
).toEqual([
|
||
'dev',
|
||
'--config',
|
||
'{"build":{"devUrl":"http://127.0.0.1:10005/","beforeDevCommand":""}}',
|
||
'--',
|
||
'--',
|
||
'--config-dir',
|
||
'C:\\temp\\agc-dev-config',
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||
test('前端就绪前不启动 Tauri,准备失败时清理自有服务', async () => {
|
||
const spawnCli = vi.fn();
|
||
const frontend = { pid: 1234 };
|
||
const terminateTree = vi.fn(async () => ({ stopped: true }));
|
||
await expect(
|
||
runTauriDev([], {
|
||
resolveDevEndpoint: resolveTestEndpoint,
|
||
preflight: async () => {},
|
||
prepareFrontend: async (_, { onChild }) => {
|
||
onChild(frontend);
|
||
expect(spawnCli).not.toHaveBeenCalled();
|
||
throw new Error('backend failed');
|
||
},
|
||
spawnCli,
|
||
terminateTree,
|
||
}),
|
||
).rejects.toThrow('backend failed');
|
||
expect(spawnCli).not.toHaveBeenCalled();
|
||
expect(terminateTree).toHaveBeenCalledWith(frontend);
|
||
});
|
||
test('动态端口预检失败时不启动 Tauri CLI', async () => {
|
||
const spawnCli = vi.fn();
|
||
|
||
await expect(
|
||
runTauriDev([], {
|
||
resolveDevEndpoint: resolveTestEndpoint,
|
||
preflight: async () => {
|
||
throw new Error('stale AGC Vite');
|
||
},
|
||
spawnCli,
|
||
}),
|
||
).rejects.toThrow('stale AGC Vite');
|
||
|
||
expect(spawnCli).not.toHaveBeenCalled();
|
||
});
|
||
|
||
test('预检先于 CLI 启动且 CLI 退出后始终清理进程树', async () => {
|
||
const order: string[] = [];
|
||
const child = Object.assign(new EventEmitter(), {
|
||
pid: 1234,
|
||
exitCode: 1,
|
||
signalCode: null,
|
||
kill: vi.fn(),
|
||
});
|
||
const result = await runTauriDev([], {
|
||
resolveDevEndpoint: resolveTestEndpoint,
|
||
preflight: async () => {
|
||
order.push('preflight');
|
||
},
|
||
prepareFrontend: async () => {
|
||
order.push('frontend-ready');
|
||
},
|
||
prepareResources: (features) => {
|
||
expect(Array.isArray(features)).toBe(true);
|
||
order.push('resources');
|
||
},
|
||
spawnCli: () => {
|
||
order.push('spawn');
|
||
return child;
|
||
},
|
||
waitForCli: async () => {
|
||
order.push('exit');
|
||
return { type: 'exit', code: 1, signal: null };
|
||
},
|
||
terminateTree: async (receivedChild) => {
|
||
expect(receivedChild).toBe(child);
|
||
order.push('cleanup');
|
||
return { stopped: true, forced: false };
|
||
},
|
||
});
|
||
|
||
expect(result).toBe(1);
|
||
expect(order).toEqual([
|
||
'preflight',
|
||
'resources',
|
||
'frontend-ready',
|
||
'spawn',
|
||
'exit',
|
||
'cleanup',
|
||
]);
|
||
});
|
||
|
||
const posixTest = process.platform === 'win32' ? test.skip : test;
|
||
|
||
posixTest('Tauri CLI leader 先退出后仍收束同 PGID 的客户端后代', async () => {
|
||
const tempDir = mkdtempSync(join(tmpdir(), 'agc-tauri-tree-'));
|
||
const readyPath = join(tempDir, 'client-ready');
|
||
const stoppedPath = join(tempDir, 'client-stopped');
|
||
const descendantSource = `
|
||
const { writeFileSync } = require('node:fs');
|
||
const [readyPath, stoppedPath] = process.argv.slice(1);
|
||
process.on('SIGTERM', () => {
|
||
writeFileSync(stoppedPath, 'stopped');
|
||
process.exit(0);
|
||
});
|
||
writeFileSync(readyPath, 'ready');
|
||
setInterval(() => {}, 1000);
|
||
`;
|
||
const leaderSource = `
|
||
const { existsSync } = require('node:fs');
|
||
const { spawn } = require('node:child_process');
|
||
const [readyPath, stoppedPath, descendantSource] = process.argv.slice(1);
|
||
const descendant = spawn(
|
||
process.execPath,
|
||
['-e', descendantSource, readyPath, stoppedPath],
|
||
{ stdio: 'ignore' },
|
||
);
|
||
descendant.unref();
|
||
const timer = setInterval(() => {
|
||
if (existsSync(readyPath)) {
|
||
clearInterval(timer);
|
||
process.exit(42);
|
||
}
|
||
}, 10);
|
||
`;
|
||
let cliChild;
|
||
try {
|
||
const result = await runTauriDev([], {
|
||
resolveDevEndpoint: resolveTestEndpoint,
|
||
preflight: async () => {},
|
||
spawnCli: () => {
|
||
cliChild = spawnChild(
|
||
process.execPath,
|
||
['-e', leaderSource, readyPath, stoppedPath, descendantSource],
|
||
{ cwd: process.cwd() },
|
||
);
|
||
return cliChild;
|
||
},
|
||
});
|
||
|
||
expect(result).toBe(42);
|
||
await waitForFile(stoppedPath);
|
||
} finally {
|
||
if (Number.isInteger(cliChild?.pid)) {
|
||
try {
|
||
process.kill(-cliChild.pid, 'SIGKILL');
|
||
} catch {
|
||
// 进程组已经由启动器收束。
|
||
}
|
||
}
|
||
rmSync(tempDir, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
posixTest('客户端后代忽略 TERM 时在有界宽限后升级 KILL', async () => {
|
||
const tempDir = mkdtempSync(join(tmpdir(), 'agc-tauri-force-tree-'));
|
||
const readyPath = join(tempDir, 'client-ready');
|
||
const descendantSource = `
|
||
const { writeFileSync } = require('node:fs');
|
||
const [readyPath] = process.argv.slice(1);
|
||
process.on('SIGTERM', () => {});
|
||
writeFileSync(readyPath, 'ready');
|
||
setInterval(() => {}, 1000);
|
||
`;
|
||
const leaderSource = `
|
||
const { existsSync } = require('node:fs');
|
||
const { spawn } = require('node:child_process');
|
||
const [readyPath, descendantSource] = process.argv.slice(1);
|
||
const descendant = spawn(
|
||
process.execPath,
|
||
['-e', descendantSource, readyPath],
|
||
{ stdio: 'ignore' },
|
||
);
|
||
descendant.unref();
|
||
const timer = setInterval(() => {
|
||
if (existsSync(readyPath)) {
|
||
clearInterval(timer);
|
||
process.exit(42);
|
||
}
|
||
}, 10);
|
||
`;
|
||
let cliChild;
|
||
try {
|
||
const result = await runTauriDev([], {
|
||
resolveDevEndpoint: resolveTestEndpoint,
|
||
preflight: async () => {},
|
||
spawnCli: () => {
|
||
cliChild = spawnChild(
|
||
process.execPath,
|
||
['-e', leaderSource, readyPath, descendantSource],
|
||
{ cwd: process.cwd() },
|
||
);
|
||
return cliChild;
|
||
},
|
||
terminateTree: (child) =>
|
||
terminateChildTree(child, {
|
||
gracefulTimeoutMs: 50,
|
||
forceTimeoutMs: 2000,
|
||
}),
|
||
});
|
||
|
||
expect(result).toBe(42);
|
||
expect(isProcessGroupAlive(cliChild.pid)).toBe(false);
|
||
} finally {
|
||
if (Number.isInteger(cliChild?.pid)) {
|
||
try {
|
||
process.kill(-cliChild.pid, 'SIGKILL');
|
||
} catch {
|
||
// 进程组已经由启动器强制收束。
|
||
}
|
||
}
|
||
rmSync(tempDir, { recursive: true, force: true });
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('AI 游戏创作 Tauri dev 进程环境', () => {
|
||
const posixTest = process.platform === 'win32' ? test.skip : test;
|
||
|
||
// 本机 `~/.cargo/config.toml` 或仓库级 Cargo 配置里的 sccache wrapper 只有在环境变量
|
||
// 非空时才会被覆盖;这里必须显式写入要交给 Tauri Cargo 的 wrapper 决策结果。
|
||
posixTest('本地 dev 不把 sccache 交给 Tauri Cargo', () => {
|
||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||
try {
|
||
const env = buildTauriDevProcessEnv(testEndpoint, {
|
||
RUSTC_WRAPPER: 'sccache',
|
||
CARGO_BUILD_RUSTC_WRAPPER: 'sccache',
|
||
});
|
||
|
||
expect(env.RUSTC_WRAPPER).toBe('/usr/bin/env');
|
||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('/usr/bin/env');
|
||
} finally {
|
||
warn.mockRestore();
|
||
}
|
||
});
|
||
|
||
posixTest('未显式配置 wrapper 时清空两个变量', () => {
|
||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||
try {
|
||
const env = buildTauriDevProcessEnv(testEndpoint, {
|
||
CARGO_TERM_COLOR: 'never',
|
||
});
|
||
|
||
expect(env.RUSTC_WRAPPER).toBe('');
|
||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('');
|
||
expect(env.CARGO_TERM_COLOR).toBe('never');
|
||
} finally {
|
||
warn.mockRestore();
|
||
}
|
||
});
|
||
|
||
posixTest('保留显式自定义 wrapper 且不改写调用方 env', () => {
|
||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||
try {
|
||
const input = { RUSTC_WRAPPER: '/opt/custom/rustc-wrapper' };
|
||
const env = buildTauriDevProcessEnv(testEndpoint, input);
|
||
|
||
expect(env.RUSTC_WRAPPER).toBe('/opt/custom/rustc-wrapper');
|
||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('/opt/custom/rustc-wrapper');
|
||
expect(env.GENARRATIVE_AGC_VITE_PORT).toBe(String(testEndpoint.port));
|
||
expect(input).toEqual({ RUSTC_WRAPPER: '/opt/custom/rustc-wrapper' });
|
||
} finally {
|
||
warn.mockRestore();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('AI 游戏创作 Tauri dev 随包资源准备目标', () => {
|
||
const windowsFeatures = [
|
||
'cocos-editor-execute',
|
||
'unity-editor-execute',
|
||
'godot-editor-execute',
|
||
];
|
||
// buildTauriArguments 注入的动态 devUrl 配置:最终交给 Tauri 的参数里长这样。
|
||
const devConfig =
|
||
'{"build":{"devUrl":"http://127.0.0.1:10005/","beforeDevCommand":""}}';
|
||
|
||
test('显式 --target 决定 staging 目标,未指定或属于应用参数时回落宿主目标', () => {
|
||
const calls: [string, string[]][] = [];
|
||
const hostTarget = 'aarch64-apple-darwin';
|
||
const prepare = (options: {
|
||
target: string;
|
||
features: Iterable<string>;
|
||
}) => {
|
||
calls.push([options.target, [...options.features]]);
|
||
return [];
|
||
};
|
||
|
||
for (const argv of [
|
||
['dev', '--config', devConfig],
|
||
['dev', '--target', 'x86_64-apple-darwin', '--config', devConfig],
|
||
['dev', '--target=x86_64-pc-windows-msvc', '--config', devConfig],
|
||
// 启动器把 `--` 之后的参数交给应用(补成第二段分隔符):不能拿它改随包资源目标。
|
||
[
|
||
'dev',
|
||
'--config',
|
||
devConfig,
|
||
'--',
|
||
'--',
|
||
'--target=x86_64-pc-windows-msvc',
|
||
],
|
||
]) {
|
||
prepareBundledResourcesBeforeTauri(windowsFeatures, argv, {
|
||
hostTarget,
|
||
prepare,
|
||
log: () => {},
|
||
});
|
||
}
|
||
|
||
expect(calls.map(([target]) => target)).toEqual([
|
||
hostTarget,
|
||
'x86_64-apple-darwin',
|
||
'x86_64-pc-windows-msvc',
|
||
hostTarget,
|
||
]);
|
||
expect(calls[0][1]).toEqual(windowsFeatures);
|
||
});
|
||
|
||
test('宿主平台不受声明覆盖时跳过准备,且不为其新增随包支持', () => {
|
||
const prepare = vi.fn();
|
||
const log = vi.fn();
|
||
|
||
prepareBundledResourcesBeforeTauri(
|
||
windowsFeatures,
|
||
['dev', '--config', devConfig],
|
||
{ hostTarget: null, prepare, log },
|
||
);
|
||
|
||
expect(prepare).not.toHaveBeenCalled();
|
||
expect(log.mock.calls.flat().join('\n')).toContain('跳过随包资源准备');
|
||
});
|
||
|
||
async function runCapturingDev(argv: string[]) {
|
||
const prepared: unknown[][] = [];
|
||
const spawned: string[][] = [];
|
||
const child = Object.assign(new EventEmitter(), {
|
||
pid: 1234,
|
||
exitCode: 0,
|
||
signalCode: null,
|
||
kill: vi.fn(),
|
||
});
|
||
const result = await runTauriDev(argv, {
|
||
resolveDevEndpoint: resolveTestEndpoint,
|
||
preflight: async () => {},
|
||
prepareResources: (...args: unknown[]) => {
|
||
prepared.push(args);
|
||
},
|
||
spawnCli: (args: string[]) => {
|
||
spawned.push(args);
|
||
return child;
|
||
},
|
||
waitForCli: async () => ({ type: 'exit', code: 0, signal: null }),
|
||
terminateTree: async () => ({ stopped: true, forced: false }),
|
||
});
|
||
return { result, prepared, spawned };
|
||
}
|
||
|
||
test('显式 -f 优先于环境变量,并同时决定随包资源 feature 与 cargo 参数', async () => {
|
||
vi.stubEnv('AGC_DEV_CARGO_FEATURES', 'template-library-fixtures');
|
||
try {
|
||
const { result, prepared, spawned } = await runCapturingDev([
|
||
'-f',
|
||
'custom-feature',
|
||
]);
|
||
|
||
expect(result).toBe(0);
|
||
expect(prepared.map(([features]) => features)).toEqual([
|
||
['custom-feature'],
|
||
]);
|
||
expect(spawned[0].slice(0, 3)).toEqual(['dev', '-f', 'custom-feature']);
|
||
expect(spawned[0]).not.toContain(
|
||
`--features=${windowsFeatures.join(',')}`,
|
||
);
|
||
// 随包资源准备拿到的是最终交给 Tauri 的参数,不是启动器收到的原始参数。
|
||
expect(prepared[0][1]).toEqual(spawned[0]);
|
||
} finally {
|
||
vi.unstubAllEnvs();
|
||
}
|
||
});
|
||
|
||
test('显式 --target 同时进入随包资源准备与 Tauri 参数', async () => {
|
||
vi.stubEnv('AGC_DEV_CARGO_FEATURES', '');
|
||
try {
|
||
const { result, prepared, spawned } = await runCapturingDev([
|
||
'--target',
|
||
'x86_64-apple-darwin',
|
||
]);
|
||
|
||
expect(result).toBe(0);
|
||
expect(prepared.map(([features]) => features)).toEqual([[]]);
|
||
expect(prepared[0][1]).toEqual([
|
||
'dev',
|
||
'--target',
|
||
'x86_64-apple-darwin',
|
||
'--config',
|
||
devConfig,
|
||
]);
|
||
expect(spawned[0]).toEqual(prepared[0][1]);
|
||
} finally {
|
||
vi.unstubAllEnvs();
|
||
}
|
||
});
|
||
|
||
test('启动器 `--` 之后的应用参数不参与 staging feature 解析', async () => {
|
||
vi.stubEnv('AGC_DEV_CARGO_FEATURES', '');
|
||
try {
|
||
const { result, prepared, spawned } = await runCapturingDev([
|
||
'--',
|
||
'--features=app-tool',
|
||
]);
|
||
|
||
expect(result).toBe(0);
|
||
expect(prepared.map(([features]) => features)).toEqual([[]]);
|
||
expect(spawned[0]).toEqual([
|
||
'dev',
|
||
'--config',
|
||
devConfig,
|
||
'--',
|
||
'--',
|
||
'--features=app-tool',
|
||
]);
|
||
expect(prepared[0][1]).toEqual(spawned[0]);
|
||
} finally {
|
||
vi.unstubAllEnvs();
|
||
}
|
||
});
|
||
|
||
test('空串 AGC_DEV_CARGO_FEATURES 关闭默认 feature,staging 与 cargo 都不带 feature', async () => {
|
||
vi.stubEnv('AGC_DEV_CARGO_FEATURES', '');
|
||
try {
|
||
const { result, prepared, spawned } = await runCapturingDev([]);
|
||
|
||
expect(result).toBe(0);
|
||
expect(prepared.map(([features]) => features)).toEqual([[]]);
|
||
expect(
|
||
spawned[0].some((argument) => argument.startsWith('--features')),
|
||
).toBe(false);
|
||
} finally {
|
||
vi.unstubAllEnvs();
|
||
}
|
||
});
|
||
|
||
test('环境变量只在不与显式 CLI 冲突时生效', () => {
|
||
vi.stubEnv('AGC_DEV_CARGO_FEATURES', 'template-library-fixtures');
|
||
try {
|
||
expect(
|
||
resolveEditorFeatures({
|
||
argv: ['dev', '--config', devConfig],
|
||
target: 'win32',
|
||
}),
|
||
).toEqual(['template-library-fixtures']);
|
||
expect(
|
||
resolveEditorFeatures({
|
||
argv: ['dev', '-f', 'cli-feature', '--config', devConfig],
|
||
target: 'win32',
|
||
}),
|
||
).toEqual(['cli-feature']);
|
||
} finally {
|
||
vi.unstubAllEnvs();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('AI 游戏创作 Tauri dev 目标与 feature 解析', () => {
|
||
test('目标只从构建参数区解析,取值缺失、取到选项或重复都失败关闭', () => {
|
||
expect(readCargoTarget([])).toBeUndefined();
|
||
expect(readCargoTarget(['--no-watch'])).toBeUndefined();
|
||
expect(readCargoTarget(['-t', 'x86_64-apple-darwin'])).toBe(
|
||
'x86_64-apple-darwin',
|
||
);
|
||
expect(readCargoTarget(['--target=x86_64-apple-darwin'])).toBe(
|
||
'x86_64-apple-darwin',
|
||
);
|
||
// runner 参数由 Tauri 追加给 cargo:仍然算构建目标。
|
||
expect(
|
||
readCargoTarget(['build', '--', '--target=aarch64-apple-darwin']),
|
||
).toBe('aarch64-apple-darwin');
|
||
// 第二个 `--` 之后是应用参数。
|
||
expect(
|
||
readCargoTarget(['dev', '--', '--', '--target=aarch64-apple-darwin']),
|
||
).toBeUndefined();
|
||
expect(() => readCargoTarget(['--target'])).toThrow('缺少有效目标');
|
||
expect(() => readCargoTarget(['--target', '--no-watch'])).toThrow(
|
||
'缺少有效目标',
|
||
);
|
||
expect(() => readCargoTarget(['--target', 'a', '-t', 'b'])).toThrow(
|
||
'不能重复指定',
|
||
);
|
||
});
|
||
|
||
test('feature 解析覆盖 -f 拼写、合并多个 flag 并止步于应用参数', () => {
|
||
const cases: [string[], string[]][] = [
|
||
[
|
||
['-f', 'a,b'],
|
||
['a', 'b'],
|
||
],
|
||
[['-fa'], ['a']],
|
||
[['--features=explicit'], ['explicit']],
|
||
// Cargo 会合并多个 feature flag:staging 必须拿到同一集合。
|
||
[
|
||
['--features', 'a', '-f', 'b'],
|
||
['a', 'b'],
|
||
],
|
||
// 显式给空集合即关闭默认,而不是回落默认值。
|
||
[['--features='], []],
|
||
// 第二个 `--` 之后是应用参数。
|
||
[
|
||
['dev', '--config', '{}', '--', '--', '-f', 'app-feature'],
|
||
[
|
||
'cocos-editor-execute',
|
||
'unity-editor-execute',
|
||
'godot-editor-execute',
|
||
],
|
||
],
|
||
];
|
||
|
||
for (const [argv, expected] of cases) {
|
||
expect(resolveEditorFeatures({ argv, target: 'win32', env: {} })).toEqual(
|
||
expected,
|
||
);
|
||
}
|
||
});
|
||
});
|