8d710738e2
- 配置向导删除指向已删 npm 脚本的 `npm run test:chat` 调用与失去含义的 `--configure-only` 开关 - 删除只服务该分支的 askYesNo 与 npmCommand,保持 TTY 前置检查语义不变 - App 内用户文案 `请先用 /project 设置本地项目。` 改为 `请先打开本地项目。` - chatPromptPolish 头部注释去掉已退役的 `/` 命令表述 - 删除 interaction.json 中只服务已删交互内核的 7 个 prompt 键(execute_description、resume_description、project_location_description、protocol、system、user、user_with_context) - harness/process.mjs 的账号 fixture 注释改为描述现役隔离 Runner 机制
218 lines
6.5 KiB
JavaScript
218 lines
6.5 KiB
JavaScript
import {
|
|
assert,
|
|
codedError,
|
|
hashValue,
|
|
throwIfShutdownRequested,
|
|
} from '../assertions/core.mjs';
|
|
import { fs, path, spawn, withLoopbackNoProxy } from '../dependencies.mjs';
|
|
import {
|
|
activeCommandChildren,
|
|
appRoot,
|
|
commandOutputLimit,
|
|
manifestPath,
|
|
state,
|
|
} from '../runtime-state.mjs';
|
|
import { isSupervisorSwarmTransientRetrySuite } from '../suites/supervisor-swarm.mjs';
|
|
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
|
|
|
export async function prepareCliBinary() {
|
|
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
|
|
await runProcess(
|
|
cargo,
|
|
['build', '--quiet', '--manifest-path', manifestPath],
|
|
{
|
|
cwd: appRoot,
|
|
timeoutMs: 15 * 60 * 1000,
|
|
},
|
|
);
|
|
const metadata = await runProcess(
|
|
cargo,
|
|
[
|
|
'metadata',
|
|
'--format-version',
|
|
'1',
|
|
'--no-deps',
|
|
'--manifest-path',
|
|
manifestPath,
|
|
],
|
|
{ cwd: appRoot, timeoutMs: 120_000 },
|
|
);
|
|
const parsed = JSON.parse(metadata.stdout);
|
|
const executable = path.join(
|
|
parsed.target_directory,
|
|
'debug',
|
|
`genarrative-ai-game-creator-shell${process.platform === 'win32' ? '.exe' : ''}`,
|
|
);
|
|
const binary = await fs.stat(executable).catch(() => null);
|
|
assert(binary?.isFile(), 'cli-binary-missing');
|
|
return executable;
|
|
}
|
|
|
|
export async function runCli(args, options = {}) {
|
|
assert(Boolean(state.cliBinary), 'cli-binary-not-ready');
|
|
assert(Boolean(state.runtimeConfigDir), 'runtime-config-dir-not-ready');
|
|
if (
|
|
isIsolatedRunnerSuite() &&
|
|
state.options?.configDir &&
|
|
path.resolve(state.runtimeConfigDir) ===
|
|
path.resolve(state.options.configDir)
|
|
) {
|
|
state.isolatedRunner.sourceConfigCliCallCount += 1;
|
|
}
|
|
return runProcess(
|
|
state.cliBinary,
|
|
[...args, '--config-dir', state.runtimeConfigDir],
|
|
{
|
|
cwd: appRoot,
|
|
timeoutMs: options.timeoutMs ?? 60_000,
|
|
stdin: options.stdin,
|
|
allowNonZero: options.allowNonZero ?? false,
|
|
env: buildCliChildEnvironment(),
|
|
},
|
|
);
|
|
}
|
|
|
|
export function buildCliChildEnvironment() {
|
|
const environment = {
|
|
...process.env,
|
|
NO_COLOR: '1',
|
|
RUST_BACKTRACE: '0',
|
|
};
|
|
// The isolated runner copies its account fixture into the sibling isolated
|
|
// AppData directory. Set the path explicitly here so
|
|
// every CLI and the Runner it launches use the isolated copy, even if the
|
|
// parent harness environment was restored or changed after setup.
|
|
if (state.isolatedRunner.platformSessionFixturePath) {
|
|
environment.GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE =
|
|
state.isolatedRunner.platformSessionFixturePath;
|
|
}
|
|
return isSupervisorSwarmTransientRetrySuite()
|
|
? withLoopbackNoProxy(environment)
|
|
: environment;
|
|
}
|
|
|
|
export function safeProcessFailureDiagnostic({
|
|
code = null,
|
|
signal = null,
|
|
error = null,
|
|
stdout = '',
|
|
stderr = '',
|
|
} = {}) {
|
|
const stdoutText = Buffer.isBuffer(stdout)
|
|
? stdout.toString('utf8')
|
|
: String(stdout);
|
|
const stderrText = Buffer.isBuffer(stderr)
|
|
? stderr.toString('utf8')
|
|
: String(stderr);
|
|
const combined = `${error?.message ?? ''}\n${stdoutText}\n${stderrText}`;
|
|
const processErrorCode = /^[A-Z0-9_]+$/u.test(error?.code ?? '')
|
|
? error.code
|
|
: 'none';
|
|
const closeSignal = /^[A-Z0-9]+$/u.test(signal ?? '') ? signal : 'none';
|
|
let failureKind = 'closed';
|
|
if (
|
|
processErrorCode === 'ENOSPC' ||
|
|
/(?:\bENOSPC\b|No space left on device|os error 28)/iu.test(combined)
|
|
) {
|
|
failureKind = 'enospc';
|
|
} else if (processErrorCode !== 'none') {
|
|
failureKind = 'process-error';
|
|
} else if (closeSignal !== 'none') {
|
|
failureKind = 'signal';
|
|
} else if (Number.isInteger(code) && code !== 0) {
|
|
failureKind = 'nonzero-exit';
|
|
}
|
|
return {
|
|
failureKind,
|
|
exitCode: Number.isInteger(code) ? String(code) : 'none',
|
|
signal: closeSignal,
|
|
processErrorCode,
|
|
stderrChars: [...stderrText].length,
|
|
stderrSha256: hashValue(stderrText) ?? 'none',
|
|
};
|
|
}
|
|
|
|
export function codedProcessError(code, details) {
|
|
const error = codedError(code, details?.error);
|
|
error.safeFailureDiagnostic = safeProcessFailureDiagnostic(details);
|
|
return error;
|
|
}
|
|
|
|
export async function runProcess(
|
|
program,
|
|
args,
|
|
{ cwd, timeoutMs, stdin, allowNonZero = false, env = null },
|
|
) {
|
|
throwIfShutdownRequested();
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(program, args, {
|
|
cwd,
|
|
env: env ?? { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' },
|
|
stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
});
|
|
activeCommandChildren.add(child);
|
|
let stdout = Buffer.alloc(0);
|
|
let stderr = Buffer.alloc(0);
|
|
let timedOut = false;
|
|
const timer = setTimeout(() => {
|
|
timedOut = true;
|
|
child.kill('SIGKILL');
|
|
}, timeoutMs);
|
|
child.stdout.on('data', (chunk) => {
|
|
state.transcriptScanner?.scan('stdout', chunk);
|
|
state.projectPathTranscriptScanner?.scan('stdout', chunk);
|
|
state.formalConfigPathTranscriptScanner?.scan('stdout', chunk);
|
|
stdout = appendBounded(stdout, chunk, commandOutputLimit);
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
state.transcriptScanner?.scan('stderr', chunk);
|
|
state.projectPathTranscriptScanner?.scan('stderr', chunk);
|
|
state.formalConfigPathTranscriptScanner?.scan('stderr', chunk);
|
|
stderr = appendBounded(stderr, chunk, commandOutputLimit);
|
|
});
|
|
child.on('error', (error) => {
|
|
clearTimeout(timer);
|
|
activeCommandChildren.delete(child);
|
|
reject(
|
|
codedProcessError('process-spawn-failed', {
|
|
error,
|
|
stdout,
|
|
stderr,
|
|
}),
|
|
);
|
|
});
|
|
child.on('close', (code, signal) => {
|
|
clearTimeout(timer);
|
|
activeCommandChildren.delete(child);
|
|
const result = {
|
|
stdout: stdout.toString('utf8'),
|
|
stderr: stderr.toString('utf8'),
|
|
code,
|
|
signal,
|
|
};
|
|
if (timedOut) {
|
|
reject(codedProcessError('process-timeout', result));
|
|
} else if (state.shutdownSignal && !state.cleanupInProgress) {
|
|
reject(
|
|
codedProcessError(
|
|
`interrupted-${state.shutdownSignal.toLowerCase()}`,
|
|
result,
|
|
),
|
|
);
|
|
} else if (code !== 0 && !allowNonZero) {
|
|
reject(codedProcessError('cli-command-failed', result));
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
});
|
|
if (stdin !== undefined) child.stdin.end(stdin);
|
|
});
|
|
}
|
|
|
|
export function appendBounded(current, chunk, limit) {
|
|
const combined = Buffer.concat([current, chunk]);
|
|
return combined.length <= limit
|
|
? combined
|
|
: combined.subarray(combined.length - limit);
|
|
}
|