167b11d52e
固定首批程序实现与质量只读职责,并在计划执行前阻断质量 Agent 项目写入 要求非只读专业 Agent 完成本人修改和对应 revision 验证后再交付 补齐 Provider 瞬态重试、最终回复和 Swarm CLI 终态收敛 升级 Provider action batch v3 并兼容 v2/v1 持久恢复 扩展确定性与真实外部 Provider 塔防验收、前端状态和项目文档
621 lines
22 KiB
JavaScript
621 lines
22 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import {
|
|
createDeterministicLaneDefenseRouter,
|
|
deterministicLaneDefenseInitialHtml,
|
|
deterministicLaneDefenseModel,
|
|
hiddenCanvasCss,
|
|
startDeterministicLaneDefenseProvider,
|
|
visibleCanvasCss,
|
|
} from './deterministic-lane-defense-provider.mjs';
|
|
import { withLoopbackNoProxy } from './llm-transient-fault-proxy.mjs';
|
|
|
|
const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
const repoRoot = path.resolve(appRoot, '../..');
|
|
const realE2eScript = path.join(appRoot, 'scripts/agent-runtime-real-e2e.mjs');
|
|
const suite = 'supervisor-autonomous-playable-lane-defense';
|
|
const wrapperSuite =
|
|
'supervisor-autonomous-playable-lane-defense-deterministic';
|
|
const configFileName = 'game-creator.config.json';
|
|
const configSentinelName = '.deterministic-provider-e2e.json';
|
|
const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1';
|
|
const outputLimit = 32 * 1024 * 1024;
|
|
|
|
function hashValue(value) {
|
|
return createHash('sha256').update(value).digest('hex');
|
|
}
|
|
|
|
function assert(condition, code) {
|
|
if (condition) return;
|
|
const error = new Error(code);
|
|
error.code = code;
|
|
throw error;
|
|
}
|
|
|
|
function parseArguments(args) {
|
|
let keepProject = false;
|
|
let selfTest = false;
|
|
for (const arg of args) {
|
|
if (arg === '--keep-project') keepProject = true;
|
|
else if (arg === '--self-test') selfTest = true;
|
|
else throw new Error('unknown-argument');
|
|
}
|
|
assert(!(keepProject && selfTest), 'self-test-keep-project-conflict');
|
|
return { keepProject, selfTest };
|
|
}
|
|
|
|
function appendBounded(current, chunk) {
|
|
const combined = Buffer.concat([current, chunk]);
|
|
if (combined.length > outputLimit) throw new Error('child-output-too-large');
|
|
return combined;
|
|
}
|
|
|
|
function runChild(args, environment) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(process.execPath, args, {
|
|
cwd: appRoot,
|
|
env: environment,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let stdout = Buffer.alloc(0);
|
|
let stderr = Buffer.alloc(0);
|
|
let outputError = null;
|
|
child.stdout.on('data', (chunk) => {
|
|
try {
|
|
stdout = appendBounded(stdout, chunk);
|
|
} catch (error) {
|
|
outputError = error;
|
|
child.kill('SIGTERM');
|
|
}
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
try {
|
|
stderr = appendBounded(stderr, chunk);
|
|
} catch (error) {
|
|
outputError = error;
|
|
child.kill('SIGTERM');
|
|
}
|
|
});
|
|
child.once('error', (error) =>
|
|
resolve({ code: null, signal: null, error, stdout, stderr }),
|
|
);
|
|
child.once('close', (code, signal) =>
|
|
resolve({ code, signal, error: outputError, stdout, stderr }),
|
|
);
|
|
});
|
|
}
|
|
|
|
function safeChildDiagnostic(result) {
|
|
return {
|
|
exitCode: Number.isInteger(result.code) ? result.code : null,
|
|
signal: typeof result.signal === 'string' ? result.signal : null,
|
|
errorCode:
|
|
typeof result.error?.code === 'string' ? result.error.code : null,
|
|
stdoutBytes: result.stdout.length,
|
|
stdoutSha256: hashValue(result.stdout),
|
|
stderrBytes: result.stderr.length,
|
|
stderrSha256: hashValue(result.stderr),
|
|
};
|
|
}
|
|
|
|
function parseChildReport(result) {
|
|
try {
|
|
const report = JSON.parse(result.stdout.toString('utf8'));
|
|
assert(report && typeof report === 'object', 'child-report-root-invalid');
|
|
return report;
|
|
} catch (error) {
|
|
if (error?.code) throw error;
|
|
const wrapped = new Error('child-report-json-invalid');
|
|
wrapped.code = 'child-report-json-invalid';
|
|
throw wrapped;
|
|
}
|
|
}
|
|
|
|
function responsibilityContractEvidence(stats) {
|
|
const qualityPlanningCount =
|
|
stats?.byAgent?.['quality-review']?.planning ?? null;
|
|
const evidence = {
|
|
delegationContractCount: stats?.delegationContractCount ?? null,
|
|
delegationContractViolationCount:
|
|
stats?.delegationContractViolationCount ?? null,
|
|
codePrototypeDelegationCount: stats?.codePrototypeDelegationCount ?? null,
|
|
codePrototypeExpectedArtifactCount:
|
|
stats?.codePrototypeExpectedArtifactCount ?? null,
|
|
codePrototypeGameIndexArtifactDelegationCount:
|
|
stats?.codePrototypeGameIndexArtifactDelegationCount ?? null,
|
|
codePrototypeProjectMutationCount:
|
|
stats?.byAgent?.['code-prototype']?.projectMutation ?? null,
|
|
qualityReviewDelegationCount: stats?.qualityReviewDelegationCount ?? null,
|
|
qualityReviewReadOnlyDelegationCount:
|
|
stats?.qualityReviewReadOnlyDelegationCount ?? null,
|
|
qualityReviewExpectedArtifactCount:
|
|
stats?.qualityReviewExpectedArtifactCount ?? null,
|
|
qualityReviewProjectMutationCount:
|
|
stats?.byAgent?.['quality-review']?.projectMutation ?? null,
|
|
qualityPlanningCount,
|
|
qualityRevisionReplanCount: stats?.qualityRevisionReplanCount ?? null,
|
|
};
|
|
return {
|
|
...evidence,
|
|
codeOwnsGameIndex:
|
|
evidence.codePrototypeDelegationCount === 2 &&
|
|
evidence.codePrototypeExpectedArtifactCount === 2 &&
|
|
evidence.codePrototypeGameIndexArtifactDelegationCount === 2,
|
|
qualityIsReadOnly:
|
|
evidence.qualityReviewDelegationCount === 1 &&
|
|
evidence.qualityReviewReadOnlyDelegationCount === 1 &&
|
|
evidence.qualityReviewExpectedArtifactCount === 0 &&
|
|
evidence.qualityReviewProjectMutationCount === 0,
|
|
qualityIndependentOfCodeTiming:
|
|
(qualityPlanningCount === 1 || qualityPlanningCount === 2) &&
|
|
evidence.qualityRevisionReplanCount === qualityPlanningCount - 1,
|
|
contractViolationFree:
|
|
evidence.delegationContractCount === 3 &&
|
|
evidence.delegationContractViolationCount === 0,
|
|
};
|
|
}
|
|
|
|
function expectedProviderStats(stats) {
|
|
const responsibility = responsibilityContractEvidence(stats);
|
|
const qualityPlanningCount = responsibility.qualityPlanningCount;
|
|
return (
|
|
(qualityPlanningCount === 1 || qualityPlanningCount === 2) &&
|
|
stats.requestCount === 15 + qualityPlanningCount &&
|
|
stats.planningRequestCount === 15 + qualityPlanningCount &&
|
|
stats.finalReplyRequestCount === 0 &&
|
|
stats.initialDelegationCount === 2 &&
|
|
stats.followupDelegationCount === 1 &&
|
|
stats.runStatusCount === 2 &&
|
|
stats.sourceWriteCount === 1 &&
|
|
stats.sourcePatchCount === 1 &&
|
|
stats.staticSmokeCount === 4 &&
|
|
stats.previewValidationCount === 2 &&
|
|
stats.supervisorDirectMutationAttemptCount === 1 &&
|
|
stats.unexpectedRequestCount === 0 &&
|
|
Object.keys(stats.rejectionCodes ?? {}).length === 0 &&
|
|
stats.byAgent?.['project-supervisor']?.planning === 9 &&
|
|
stats.byAgent?.['project-supervisor']?.finalReply === 0 &&
|
|
stats.byAgent?.['project-supervisor']?.projectMutation === 1 &&
|
|
stats.byAgent?.['code-prototype']?.planning === 6 &&
|
|
stats.byAgent?.['code-prototype']?.finalReply === 0 &&
|
|
stats.byAgent?.['code-prototype']?.projectMutation === 2 &&
|
|
stats.byAgent?.['quality-review']?.finalReply === 0 &&
|
|
responsibility.codeOwnsGameIndex &&
|
|
responsibility.qualityIsReadOnly &&
|
|
responsibility.qualityIndependentOfCodeTiming &&
|
|
responsibility.contractViolationFree
|
|
);
|
|
}
|
|
|
|
const requiredZeroChildEvidenceFields = Object.freeze([
|
|
'activeRunnerKillCount',
|
|
'approveInputCount',
|
|
'answerInputCount',
|
|
'steerInputCount',
|
|
'turnReportWaitingForConfirmationCount',
|
|
'turnReportWaitingForUserInputCount',
|
|
'turnReportReconciliationAgentCount',
|
|
'providerLifecycleFailedCount',
|
|
'openProviderLifecycleCount',
|
|
'pendingActionCount',
|
|
'confirmationSidecarCount',
|
|
'userInputSidecarCount',
|
|
'providerActionBatchSidecarCount',
|
|
'providerRetrySidecarCount',
|
|
'providerHandoffSidecarCount',
|
|
'toolPlanHandoffSidecarCount',
|
|
'finalizationJournalCount',
|
|
'reconciliationResidueCount',
|
|
]);
|
|
|
|
function expectedChildReport(report, options, providerStats) {
|
|
const evidence = report?.evidence;
|
|
const providerRequestCount = providerStats?.requestCount;
|
|
return (
|
|
report?.status === 'PASS' &&
|
|
report?.suite === suite &&
|
|
report?.errorCount === 0 &&
|
|
Array.isArray(report?.blocked) &&
|
|
report.blocked.length === 0 &&
|
|
report?.cleanup?.performed === !options.keepProject &&
|
|
report?.cleanup?.kept === options.keepProject &&
|
|
evidence?.evidenceCompleteness === 'complete' &&
|
|
evidence?.dedicatedZeroInterventionPath === true &&
|
|
evidence?.stdinTaskCount === 1 &&
|
|
evidence?.stdinEndedAfterTask === true &&
|
|
evidence?.turnReportOutcome === 'settled' &&
|
|
evidence?.parentTaskStatus === 'completed' &&
|
|
evidence?.parentRuntimeStatus === 'idle' &&
|
|
evidence?.parentRuntimePhase === 'completed' &&
|
|
evidence?.laneDefensePlaytestPassed === true &&
|
|
evidence?.laneDefenseAssertionCount === 37 &&
|
|
evidence?.laneDefensePassedAssertionCount === 37 &&
|
|
evidence?.browserValidationPassed === true &&
|
|
evidence?.staticSmokePassed === true &&
|
|
evidence?.gameIndexChanged === true &&
|
|
Number.isInteger(evidence?.projectRevisionDelta) &&
|
|
evidence.projectRevisionDelta > 0 &&
|
|
evidence?.finalSupervisorAssistantCount === 1 &&
|
|
evidence?.professionalAssistantCount === 3 &&
|
|
evidence?.providerRequestIdentityCount === providerRequestCount &&
|
|
evidence?.providerLifecycleStartedCount === providerRequestCount &&
|
|
evidence?.providerLifecycleTerminalCount === providerRequestCount &&
|
|
evidence?.providerLifecycleCompletedCount === providerRequestCount &&
|
|
requiredZeroChildEvidenceFields.every((field) => evidence?.[field] === 0)
|
|
);
|
|
}
|
|
|
|
function syntheticPayload(agentId, runId, tools, extraContext = '') {
|
|
return {
|
|
model: deterministicLaneDefenseModel,
|
|
stream: false,
|
|
messages: [
|
|
{
|
|
role: 'user',
|
|
content: `- templateAgentId: ${agentId}\n- runId: ${runId}\n${extraContext}`,
|
|
},
|
|
],
|
|
tools: tools.map((name) => ({ type: 'function', function: { name } })),
|
|
};
|
|
}
|
|
|
|
function responseFunctionNames(response) {
|
|
return response.choices[0].message.tool_calls.map(
|
|
(call) => call.function.name,
|
|
);
|
|
}
|
|
|
|
function responseFunctionCalls(response) {
|
|
return response.choices[0].message.tool_calls.map((call) => ({
|
|
name: call.function.name,
|
|
arguments: JSON.parse(call.function.arguments),
|
|
}));
|
|
}
|
|
|
|
async function runSelfTest() {
|
|
const html = deterministicLaneDefenseInitialHtml();
|
|
assert([...html].length <= 8_000, 'self-test-html-source-budget-invalid');
|
|
assert(
|
|
html.includes(hiddenCanvasCss) &&
|
|
!html.includes(visibleCanvasCss) &&
|
|
html.includes('playable-web-game-state.v1') &&
|
|
html.includes('data-playtest-id="next-level"') &&
|
|
html.includes('Goal: defend the garden and win every wave.') &&
|
|
html.includes('requestAnimationFrame'),
|
|
'self-test-html-contract-invalid',
|
|
);
|
|
const apiKey = `deterministic-self-test-${randomUUID()}`;
|
|
const router = createDeterministicLaneDefenseRouter({ apiKey });
|
|
const allTools = [
|
|
'update_agent_plan',
|
|
'respond_to_user',
|
|
'runtime_tool_agent_delegate',
|
|
'runtime_tool_agent_run_status',
|
|
'runtime_tool_command_run_limited',
|
|
'runtime_tool_file_patch',
|
|
'runtime_tool_file_write',
|
|
'runtime_tool_preview_validate',
|
|
];
|
|
const route = (agentId, runId, tools = allTools, extraContext = '') =>
|
|
router.route({
|
|
authorization: `Bearer ${apiKey}`,
|
|
payload: syntheticPayload(agentId, runId, tools, extraContext),
|
|
});
|
|
const initialDelegationResponse = route('project-supervisor', 'parent-run');
|
|
const initialDelegationCalls = responseFunctionCalls(
|
|
initialDelegationResponse,
|
|
);
|
|
assert(
|
|
initialDelegationCalls.map((call) => call.name).join(',') ===
|
|
'runtime_tool_agent_delegate,runtime_tool_agent_delegate',
|
|
'self-test-initial-delegation-invalid',
|
|
);
|
|
const initialCodeDelegation = initialDelegationCalls[0]?.arguments?.input;
|
|
const initialQualityDelegation = initialDelegationCalls[1]?.arguments?.input;
|
|
assert(
|
|
initialCodeDelegation?.agentId === 'code-prototype' &&
|
|
JSON.stringify(initialCodeDelegation.expectedArtifacts) ===
|
|
JSON.stringify(['game/index.html']) &&
|
|
initialQualityDelegation?.agentId === 'quality-review' &&
|
|
initialQualityDelegation.task.includes('只读验收') &&
|
|
initialQualityDelegation.task.includes('不要修改任何文件') &&
|
|
initialQualityDelegation.task.includes(
|
|
'不要读取或依赖并行 code-prototype',
|
|
) &&
|
|
Array.isArray(initialQualityDelegation.expectedArtifacts) &&
|
|
initialQualityDelegation.expectedArtifacts.length === 0,
|
|
'self-test-initial-delegation-contract-invalid',
|
|
);
|
|
const qualityPlan =
|
|
'计划进度:\n- #1 [in_progress] 核对完整玩法\n- #2 [pending] 回传结论\n工具策略:auto=无';
|
|
const builderPlan =
|
|
'计划进度:\n- #1 [completed] 生成入口\n- #2 [completed] 静态验证\n- #3 [in_progress] 回传结论\n工具策略:auto=无';
|
|
assert(
|
|
responseFunctionNames(
|
|
route('quality-review', 'quality-run', allTools, qualityPlan),
|
|
).join(',') === 'update_agent_plan,respond_to_user',
|
|
'self-test-quality-read-only-invalid',
|
|
);
|
|
route('code-prototype', 'initial-code-run');
|
|
route('code-prototype', 'initial-code-run');
|
|
assert(
|
|
responseFunctionNames(
|
|
route('code-prototype', 'initial-code-run', allTools, builderPlan),
|
|
).join(',') === 'update_agent_plan,respond_to_user',
|
|
'self-test-builder-plan-completion-invalid',
|
|
);
|
|
assert(
|
|
responseFunctionNames(
|
|
route('quality-review', 'quality-run', allTools, qualityPlan),
|
|
).join(',') === 'update_agent_plan,respond_to_user',
|
|
'self-test-quality-stale-replan-invalid',
|
|
);
|
|
route('project-supervisor', 'parent-run');
|
|
route('project-supervisor', 'parent-run');
|
|
route('project-supervisor', 'parent-run');
|
|
assert(
|
|
responseFunctionNames(route('project-supervisor', 'parent-run'))[0] ===
|
|
'runtime_tool_file_patch',
|
|
'self-test-forbidden-parent-mutation-invalid',
|
|
);
|
|
const followupDelegationResponse = route(
|
|
'project-supervisor',
|
|
'parent-run',
|
|
['runtime_tool_agent_delegate'],
|
|
'当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate',
|
|
);
|
|
const followupDelegationCall = responseFunctionCalls(
|
|
followupDelegationResponse,
|
|
)[0];
|
|
assert(
|
|
followupDelegationCall?.name === 'runtime_tool_agent_delegate' &&
|
|
followupDelegationCall.arguments?.input?.agentId === 'code-prototype' &&
|
|
JSON.stringify(
|
|
followupDelegationCall.arguments.input.expectedArtifacts,
|
|
) === JSON.stringify(['game/index.html']),
|
|
'self-test-followup-delegation-invalid',
|
|
);
|
|
route('code-prototype', 'repair-code-run');
|
|
route('code-prototype', 'repair-code-run');
|
|
route('code-prototype', 'repair-code-run', allTools, builderPlan);
|
|
assert(
|
|
responseFunctionNames(route('project-supervisor', 'parent-run'))[0] ===
|
|
'runtime_tool_agent_run_status',
|
|
'self-test-repair-claim-before-verification-invalid',
|
|
);
|
|
assert(
|
|
responseFunctionNames(route('project-supervisor', 'parent-run')).join(
|
|
',',
|
|
) === 'runtime_tool_command_run_limited,runtime_tool_preview_validate',
|
|
'self-test-repair-verification-batch-invalid',
|
|
);
|
|
route('project-supervisor', 'parent-run');
|
|
const stats = router.getStats();
|
|
assert(expectedProviderStats(stats), 'self-test-provider-stats-invalid');
|
|
const responsibilityContract = responsibilityContractEvidence(stats);
|
|
assert(
|
|
responsibilityContract.codeOwnsGameIndex &&
|
|
responsibilityContract.qualityIsReadOnly &&
|
|
responsibilityContract.qualityIndependentOfCodeTiming &&
|
|
responsibilityContract.contractViolationFree,
|
|
'self-test-responsibility-contract-invalid',
|
|
);
|
|
const singlePassQualityStats = structuredClone(stats);
|
|
singlePassQualityStats.requestCount -= 1;
|
|
singlePassQualityStats.planningRequestCount -= 1;
|
|
singlePassQualityStats.qualityRevisionReplanCount = 0;
|
|
singlePassQualityStats.byAgent['quality-review'].planning = 1;
|
|
assert(
|
|
expectedProviderStats(singlePassQualityStats),
|
|
'self-test-quality-single-pass-timing-invalid',
|
|
);
|
|
|
|
const syntheticChildReport = {
|
|
status: 'PASS',
|
|
suite,
|
|
errorCount: 0,
|
|
blocked: [],
|
|
cleanup: { performed: true, kept: false },
|
|
evidence: {
|
|
...Object.fromEntries(
|
|
requiredZeroChildEvidenceFields.map((field) => [field, 0]),
|
|
),
|
|
evidenceCompleteness: 'complete',
|
|
dedicatedZeroInterventionPath: true,
|
|
stdinTaskCount: 1,
|
|
stdinEndedAfterTask: true,
|
|
turnReportOutcome: 'settled',
|
|
parentTaskStatus: 'completed',
|
|
parentRuntimeStatus: 'idle',
|
|
parentRuntimePhase: 'completed',
|
|
laneDefensePlaytestPassed: true,
|
|
laneDefenseAssertionCount: 37,
|
|
laneDefensePassedAssertionCount: 37,
|
|
browserValidationPassed: true,
|
|
staticSmokePassed: true,
|
|
gameIndexChanged: true,
|
|
projectRevisionDelta: 2,
|
|
finalSupervisorAssistantCount: 1,
|
|
professionalAssistantCount: 3,
|
|
providerRequestIdentityCount: stats.requestCount,
|
|
providerLifecycleStartedCount: stats.requestCount,
|
|
providerLifecycleTerminalCount: stats.requestCount,
|
|
providerLifecycleCompletedCount: stats.requestCount,
|
|
},
|
|
};
|
|
const incompletePlaytestReport = structuredClone(syntheticChildReport);
|
|
incompletePlaytestReport.evidence.laneDefensePassedAssertionCount = 36;
|
|
const manualInputReport = structuredClone(syntheticChildReport);
|
|
manualInputReport.evidence.approveInputCount = 1;
|
|
const residualSidecarReport = structuredClone(syntheticChildReport);
|
|
residualSidecarReport.evidence.providerHandoffSidecarCount = 1;
|
|
assert(
|
|
expectedChildReport(syntheticChildReport, { keepProject: false }, stats) &&
|
|
!expectedChildReport(
|
|
incompletePlaytestReport,
|
|
{ keepProject: false },
|
|
stats,
|
|
) &&
|
|
!expectedChildReport(manualInputReport, { keepProject: false }, stats) &&
|
|
!expectedChildReport(
|
|
residualSidecarReport,
|
|
{ keepProject: false },
|
|
stats,
|
|
),
|
|
'self-test-child-hard-gates-invalid',
|
|
);
|
|
|
|
const rootPackage = JSON.parse(
|
|
await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'),
|
|
);
|
|
const shellPackage = JSON.parse(
|
|
await fs.readFile(path.join(appRoot, 'package.json'), 'utf8'),
|
|
);
|
|
assert(
|
|
shellPackage.scripts?.[
|
|
'agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e'
|
|
] === 'node scripts/agent-runtime-deterministic-playable-e2e.mjs' &&
|
|
rootPackage.scripts?.[
|
|
'ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e'
|
|
] ===
|
|
'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --',
|
|
'self-test-package-command-invalid',
|
|
);
|
|
return {
|
|
status: 'PASS',
|
|
suite: `${wrapperSuite}-self-test`,
|
|
providerUsed: false,
|
|
htmlChars: [...html].length,
|
|
providerStats: stats,
|
|
responsibilityContract,
|
|
qualityTimingOrdersValidated: ['before-code', 'after-code'],
|
|
childHardGatesValidated: true,
|
|
packageCommandsRegistered: true,
|
|
};
|
|
}
|
|
|
|
async function runE2e(options) {
|
|
const token = randomUUID();
|
|
const apiKey = `deterministic-runtime-${randomUUID()}`;
|
|
const configDir = await fs.mkdtemp(
|
|
path.join(os.tmpdir(), 'genarrative-deterministic-provider-config-'),
|
|
);
|
|
let provider = null;
|
|
let childResult = null;
|
|
let childReport = null;
|
|
let configRemoved = false;
|
|
let providerStats = null;
|
|
let failureCode = null;
|
|
try {
|
|
if (process.platform !== 'win32') await fs.chmod(configDir, 0o700);
|
|
await fs.writeFile(
|
|
path.join(configDir, configSentinelName),
|
|
`${JSON.stringify({ schemaVersion: configSentinelSchema, token })}\n`,
|
|
{ flag: 'wx', mode: 0o600 },
|
|
);
|
|
provider = await startDeterministicLaneDefenseProvider({ apiKey });
|
|
const config = {
|
|
llm: {
|
|
apiKey,
|
|
baseUrl: provider.baseUrl,
|
|
model: deterministicLaneDefenseModel,
|
|
apiKind: 'openai_chat',
|
|
reasoningEffort: 'low',
|
|
stream: false,
|
|
requestTimeoutMs: 30_000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 100,
|
|
},
|
|
};
|
|
await fs.writeFile(
|
|
path.join(configDir, configFileName),
|
|
`${JSON.stringify(config)}\n`,
|
|
{ flag: 'wx', mode: 0o600 },
|
|
);
|
|
const childArgs = [
|
|
realE2eScript,
|
|
'--suite',
|
|
suite,
|
|
'--config-dir',
|
|
configDir,
|
|
];
|
|
if (options.keepProject) childArgs.push('--keep-project');
|
|
childResult = await runChild(
|
|
childArgs,
|
|
withLoopbackNoProxy({ ...process.env, NO_COLOR: '1' }),
|
|
);
|
|
childReport = parseChildReport(childResult);
|
|
} catch (error) {
|
|
failureCode = error?.code ?? 'deterministic-e2e-unexpected-error';
|
|
} finally {
|
|
if (provider) {
|
|
try {
|
|
await provider.stop();
|
|
providerStats = provider.getStats();
|
|
} catch {
|
|
failureCode ??= 'deterministic-provider-stop-failed';
|
|
}
|
|
}
|
|
try {
|
|
const sentinel = JSON.parse(
|
|
await fs.readFile(path.join(configDir, configSentinelName), 'utf8'),
|
|
);
|
|
assert(
|
|
sentinel.schemaVersion === configSentinelSchema &&
|
|
sentinel.token === token,
|
|
'deterministic-config-sentinel-invalid',
|
|
);
|
|
await fs.rm(configDir, { recursive: true, force: false });
|
|
configRemoved = true;
|
|
} catch (error) {
|
|
failureCode ??= error?.code ?? 'deterministic-config-cleanup-failed';
|
|
}
|
|
}
|
|
|
|
const childPassed =
|
|
childResult?.code === 0 &&
|
|
childResult?.signal === null &&
|
|
!childResult?.error &&
|
|
expectedChildReport(childReport, options, providerStats);
|
|
const providerPassed =
|
|
providerStats?.stopped === true && expectedProviderStats(providerStats);
|
|
const responsibilityContract = responsibilityContractEvidence(providerStats);
|
|
const status =
|
|
!failureCode && childPassed && providerPassed && configRemoved
|
|
? 'PASS'
|
|
: 'FAIL';
|
|
if (status !== 'PASS' && !failureCode) {
|
|
failureCode = !childPassed
|
|
? 'deterministic-child-e2e-failed'
|
|
: !providerPassed
|
|
? 'deterministic-provider-contract-failed'
|
|
: 'deterministic-config-not-cleaned';
|
|
}
|
|
return {
|
|
status,
|
|
suite: wrapperSuite,
|
|
providerMode: 'deterministic-loopback-openai-chat',
|
|
delegatedSuite: suite,
|
|
child: childReport,
|
|
provider: providerStats,
|
|
responsibilityContract,
|
|
cleanup: {
|
|
providerStopped: providerStats?.stopped === true,
|
|
configRemoved,
|
|
projectKept: options.keepProject,
|
|
},
|
|
childDiagnostic: childResult ? safeChildDiagnostic(childResult) : null,
|
|
failureCode,
|
|
};
|
|
}
|
|
|
|
const options = parseArguments(process.argv.slice(2));
|
|
const report = options.selfTest ? await runSelfTest() : await runE2e(options);
|
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
process.exitCode = report.status === 'PASS' ? 0 : 1;
|