be89296492
拆分 App 认证、壳层、运行配置与项目摘要模块 拆分 Tauri 项目能力与 Rust 测试领域模块 拆分界面测试与 Agent Runtime 真实 E2E 套件 补充源码扫描和客户端模块化文档约定
155 lines
4.7 KiB
JavaScript
155 lines
4.7 KiB
JavaScript
import { assert } from '../assertions/core.mjs';
|
|
import { createHash, fs, path } from '../dependencies.mjs';
|
|
import { mainAgentId, state } from '../runtime-state.mjs';
|
|
import { decodeUtf8Fatal, splitJsonlBufferLines } from './reporting.mjs';
|
|
|
|
export function collectApiKeys(value, keys = []) {
|
|
if (!value || typeof value !== 'object') return keys;
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) collectApiKeys(item, keys);
|
|
return [...new Set(keys)];
|
|
}
|
|
for (const [key, child] of Object.entries(value)) {
|
|
if (
|
|
/^api_?key$/i.test(key) &&
|
|
typeof child === 'string' &&
|
|
child.length > 0
|
|
) {
|
|
keys.push(child);
|
|
} else {
|
|
collectApiKeys(child, keys);
|
|
}
|
|
}
|
|
return [...new Set(keys)];
|
|
}
|
|
|
|
export function parseAssignedJson(output, names) {
|
|
const matches = [];
|
|
for (const line of output.split(/\r?\n/u)) {
|
|
for (const name of names) {
|
|
if (line.startsWith(`${name}=`)) {
|
|
matches.push({ name, payload: line.slice(name.length + 1) });
|
|
}
|
|
}
|
|
}
|
|
assert(matches.length === 1, 'cli-assigned-json-output-invalid');
|
|
return JSON.parse(matches[0].payload);
|
|
}
|
|
|
|
export async function listFiles(root) {
|
|
const files = [];
|
|
let metadata;
|
|
try {
|
|
metadata = await fs.lstat(root);
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') return files;
|
|
throw error;
|
|
}
|
|
if (metadata.isSymbolicLink()) return files;
|
|
if (metadata.isFile()) return [root];
|
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const file = path.join(root, entry.name);
|
|
if (entry.isSymbolicLink()) continue;
|
|
if (entry.isDirectory()) files.push(...(await listFiles(file)));
|
|
else if (entry.isFile()) files.push(file);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export async function readJson(file) {
|
|
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
}
|
|
|
|
export function validateCommandOutputSidecar(sidecar, audit, file) {
|
|
const relative = relativeProjectPath(file);
|
|
assert(
|
|
sidecar?.schemaVersion === 'game-creator-command-output.v1' &&
|
|
sidecar.outputRef === relative &&
|
|
sidecar.identity?.agentId === mainAgentId &&
|
|
sidecar.identity?.taskId === audit.taskId &&
|
|
sidecar.identity?.sessionId === audit.sessionId &&
|
|
sidecar.identity?.runId === state.initialRunId &&
|
|
sidecar.identity?.actionId === audit.actionId &&
|
|
sidecar.identity?.actionFingerprint === audit.actionFingerprint &&
|
|
sidecar.outputSha256 === audit.outputSha256 &&
|
|
sidecar.totalLines === audit.totalLines &&
|
|
sidecar.captureTruncated === audit.captureTruncated &&
|
|
sidecar.exitCode === audit.exitCode &&
|
|
sidecar.timedOut === audit.timedOut &&
|
|
sidecar.sourceChanged === audit.sourceChanged &&
|
|
typeof sidecar.output === 'string' &&
|
|
createHash('sha256').update(sidecar.output).digest('hex') ===
|
|
sidecar.outputSha256 &&
|
|
(sidecar.output.length === 0
|
|
? sidecar.totalLines === 0
|
|
: sidecar.output.split('\n').length === sidecar.totalLines),
|
|
'command-output-sidecar-identity-invalid',
|
|
);
|
|
}
|
|
|
|
export async function readJsonl(file) {
|
|
const content = await fs.readFile(file);
|
|
const records = [];
|
|
for (const lineRecord of splitJsonlBufferLines(content)) {
|
|
const line = decodeUtf8Fatal(lineRecord.bytes, 'jsonl-invalid-utf8');
|
|
if (line.trim().length > 0) records.push(JSON.parse(line));
|
|
}
|
|
return records;
|
|
}
|
|
|
|
export async function readOptionalJsonl(file) {
|
|
try {
|
|
return await readJsonl(file);
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') return [];
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function resolveProjectRelative(value) {
|
|
const candidate = path.isAbsolute(value)
|
|
? path.resolve(value)
|
|
: path.resolve(state.projectRoot, value);
|
|
assert(
|
|
isPathInside(state.projectRoot, candidate),
|
|
'evidence-path-outside-project',
|
|
);
|
|
return candidate;
|
|
}
|
|
|
|
export function relativeProjectPath(value) {
|
|
const relative = path.relative(state.projectRoot, path.resolve(value));
|
|
assert(
|
|
relative && !relative.startsWith('..') && !path.isAbsolute(relative),
|
|
'relative-evidence-path-invalid',
|
|
);
|
|
return relative.split(path.sep).join('/');
|
|
}
|
|
|
|
export function isPathInside(parent, child) {
|
|
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
return (
|
|
relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
);
|
|
}
|
|
|
|
export function isTerminalRuntime(runtime) {
|
|
return ['completed', 'failed', 'cancelled', 'budget-exhausted'].includes(
|
|
runtime.phase,
|
|
);
|
|
}
|
|
|
|
export function isLiveTask(task) {
|
|
return (
|
|
['pending', 'running', 'waiting-for-confirmation'].includes(task.status) ||
|
|
[
|
|
'queued',
|
|
'running',
|
|
'executing',
|
|
'finalizing',
|
|
'waiting-for-confirmation',
|
|
].includes(task.phase)
|
|
);
|
|
}
|