fix(pi): validate user-entry conversation forks

This commit is contained in:
2026-08-25 17:48:31 +08:00
parent 941b015330
commit 8ba2a6055c
10 changed files with 767 additions and 13 deletions

View File

@@ -164,6 +164,16 @@ function assertProxyProof(proof) {
|| proof?.firstConversation?.parentResponseProjected !== true
|| proof?.firstConversation?.definiteRejectionObserved !== true
|| proof?.firstConversation?.retryAccepted !== true
|| proof?.fork?.conversationCount !== 2
|| proof?.fork?.sameAgent !== true
|| proof?.fork?.targetBindingEstablished !== true
|| proof?.fork?.distinctSessionBinding !== true
|| proof?.fork?.workerStatus !== 'ready'
|| proof?.fork?.runStatus !== 'idle'
|| proof?.fork?.sourceEntryRole !== 'user'
|| proof?.fork?.targetHydratedBeforeSourceEntry !== true
|| proof?.fork?.sourceUnchanged !== true
|| proof?.fork?.promptReplayObserved !== false
|| proof?.providerCompatibility?.developerRoleRequests !== 0
|| proof?.providerCompatibility?.controlledDefiniteRejections !== 1
|| proof?.currentHostTokenUsedBy?.join(',') !== 'parent,child'
@@ -460,6 +470,43 @@ export async function runPackagedProductProof(options) {
}
if (!await composer.isEnabled()) throw new Error('Packaged proxy composer is not editable after the turn');
const settledProxy = await evaluateProof(electronApplication, 'proxy.status');
if (settledProxy?.proxy?.conversationCount !== 1
|| settledProxy?.proxy?.activeProviderRequests?.parent !== 0
|| settledProxy?.proxy?.activeProviderRequests?.child !== 0) {
throw new Error(`Packaged proxy source did not settle before fork: ${JSON.stringify(settledProxy?.proxy)}`);
}
const timeline = page.getByTestId('coding-conversation-timeline');
const forkActions = timeline.getByRole('button', { name: '从这里创建新对话分支' });
await forkActions.waitFor({ state: 'visible', timeout: 30_000 });
if (await forkActions.count() !== 1) {
throw new Error(`Packaged proxy timeline exposed ${await forkActions.count()} fork actions for one user and one assistant entry`);
}
const sourceHeader = await page.getByTestId('coding-conversation-header').innerText();
await forkActions.click({ timeout: 30_000 });
await page.waitForFunction((previous) => {
const header = document.querySelector('[data-testid="coding-conversation-header"]');
return Boolean(header?.textContent?.includes('(fork)') && header.textContent !== previous);
}, sourceHeader, { timeout: 30_000 });
const forkedHeader = await page.getByTestId('coding-conversation-header').innerText();
const forkedTitle = forkedHeader.split('\n', 1)[0]?.trim();
if (!forkedTitle?.includes('(fork)')) {
throw new Error(`Packaged proxy fork target was not selected: ${forkedHeader}`);
}
const agentConversations = page.getByRole('group', { name: 'Packaged proxy proof agent 的对话' });
await agentConversations.getByRole('button', { name: forkedTitle, exact: true })
.waitFor({ state: 'visible', timeout: 30_000 });
const forkedBody = await page.locator('body').innerText();
if (forkedBody.includes('本地编程运行时暂时不可用')
|| forkedBody.includes('正在重新连接本地 Agent')) {
throw new Error(`Packaged user-entry fork exposed a runtime banner: ${forkedBody}`);
}
if (await timeline.getByText(retryPrompt, { exact: true }).count() !== 0
|| await timeline.getByText('REAL_PARENT_COMPLETE', { exact: true }).count() !== 0
|| await timeline.getByRole('button', { name: '从这里创建新对话分支' }).count() !== 0) {
throw new Error('Packaged fork target did not hydrate at the path before the selected user entry');
}
const proxyFinish = await evaluateProof(electronApplication, 'proxy.finish');
proxyActive = false;
assertPackagedMain(proxyFinish);
@@ -653,6 +700,12 @@ export async function runPackagedProductProof(options) {
submissionErrorSafe,
retryAfterEditEnabled,
retrySubmitted: true,
assistantForkActionHidden: true,
userForkActionAvailable: true,
forkTargetSelected: true,
sameAgentOwnership: true,
sourceUnchanged: proxyFinish.proxy?.fork?.sourceUnchanged === true,
uncertainPromptReplayed: proxyFinish.proxy?.fork?.promptReplayObserved === true,
runtimeUnavailableBanner: false,
permanentRecovering: false,
},