test(pi): separate submit rejection proof

This commit is contained in:
2026-08-25 15:00:52 +08:00
parent 5f0ef761ea
commit e3debc9b33
2 changed files with 35 additions and 17 deletions

View File

@@ -98,8 +98,11 @@
persistent preparation error. Existing runtime/session recovery state remains
reserved for preparation, hydration, worker, and session failures.
- Extended the final packaged proof with a Works-shaped endpoint role canary and
a real Renderer reject/edit/retry/settle flow. The extension and proxy proof
provider modes are protected by a focused wiring regression.
a real Renderer reject/edit/retry/settle flow. The endpoint rejects leading
`developer`, while an E2E-only one-shot Main service rejection exercises the
distinct definite-POST branch before the retry reaches the real Pi runtime.
The extension and proxy proof provider modes are protected by a focused
wiring regression.
- Packaging and final artifact/product proof remain pending.
## Verification
@@ -115,6 +118,12 @@
- `corepack pnpm run build:vite` — Pass; Renderer/Main/Preload/utility built,
with existing chunk/dynamic-import warnings.
- `corepack pnpm run test:electron:windows` — Pass: 2 files / 4 tests.
- The first packaged proof attempt correctly showed that a Provider HTTP 400
after Pi prompt preflight is an accepted-run failure, not a definite POST
rejection; it did not restore the draft and the proof failed. The proof was
corrected to keep role rejection and definite submission injection separate.
- After that correction, focused, typecheck, lint, full unit, `build:vite`, and
Windows Electron E2E were rerun and passed with the same counts/warnings.
## Follow-ups

View File

@@ -6,6 +6,7 @@ import path from 'node:path';
import { promisify } from 'node:util';
import type { CodingProductComposition } from '../../api/coding-product-services';
import { CodingConversationServiceError } from '../conversation-service';
import { getRecentLogs } from '../../utils/logger';
import { createCodingConversationStore } from '../../coding-projects/conversation-store';
import { createCodingProjectAgent } from '../../coding-projects/project-config';
@@ -57,7 +58,7 @@ type ProviderRequest = {
toolNames: string[];
hasToolResult: boolean;
firstMessageRole: string | null;
outcome: 'accepted' | 'controlled_rejection' | 'developer_role_rejection';
outcome: 'accepted' | 'developer_role_rejection';
};
type HeldProviderResponse = {
@@ -104,6 +105,8 @@ type ProxyCompositionRun = {
projectId: string;
projectPath: string;
hostToken: string;
originalAcceptPrompt: CodingProductComposition['conversations']['acceptPrompt'];
submissionInjection: { definiteRejections: number };
activeStatus?: PiReleaseProxyCompositionStatus;
};
@@ -448,7 +451,6 @@ async function startLocalProofProvider(mode: ProofProviderMode): Promise<LocalPr
const held = new Set<HeldProviderResponse>();
let closed = false;
let closeFlight: Promise<void> | null = null;
let controlledSubmitRejectionIssued = false;
const server: Server = createServer(async (request, response) => {
response.once('error', () => undefined);
try {
@@ -486,16 +488,6 @@ async function startLocalProofProvider(mode: ProofProviderMode): Promise<LocalPr
rejectProofRequest(response, 'developer role is unsupported by the controlled endpoint');
return;
}
if (mode === 'submit-rejection'
&& role === 'parent'
&& !toolResult
&& !controlledSubmitRejectionIssued) {
controlledSubmitRejectionIssued = true;
providerRequest.outcome = 'controlled_rejection';
rejectProofRequest(response, 'controlled definite model rejection');
return;
}
if (mode === 'resilience') {
if (role === 'child') {
if (toolResult) respondWithText(response, model, 'RESILIENCE_CHILD_COMPLETE');
@@ -914,9 +906,7 @@ async function proxyCompositionStatus(
systemRoleRequests: run.provider.requests.filter(({ firstMessageRole }) => (
firstMessageRole === 'system'
)).length,
controlledDefiniteRejections: run.provider.requests.filter(({ outcome }) => (
outcome === 'controlled_rejection'
)).length,
controlledDefiniteRejections: run.submissionInjection.definiteRejections,
};
const parent = processInspection.processes
.filter(({ role }) => role === 'parent')
@@ -1342,6 +1332,7 @@ export async function runFinalAsarExtensionProof(): Promise<PiReleaseExtensionPr
}
async function cleanupProxyCompositionRun(run: ProxyCompositionRun): Promise<void> {
run.composition.conversations.acceptPrompt = run.originalAcceptPrompt;
run.provider.releaseAll();
await run.composition.projects.removeProject(run.projectId).catch(() => undefined);
await run.provider.close().catch(() => undefined);
@@ -1400,12 +1391,30 @@ export async function startFinalAsarProxyCompositionProof(input: {
prompt: 'Follow the controlled loopback Provider and complete its subagent request.',
skillIds: [],
});
const originalAcceptPrompt = input.composition.conversations.acceptPrompt
.bind(input.composition.conversations);
const submissionInjection = { definiteRejections: 0 };
let rejectNextSubmission = true;
input.composition.conversations.acceptPrompt = async (promptInput) => {
if (rejectNextSubmission) {
rejectNextSubmission = false;
submissionInjection.definiteRejections += 1;
throw new CodingConversationServiceError(
503,
'CODING_MODEL_UNAVAILABLE',
'Controlled packaged submission rejection',
);
}
return await originalAcceptPrompt(promptInput);
};
proxyCompositionRun = {
composition: input.composition,
provider,
projectId,
projectPath: input.projectPath,
hostToken,
originalAcceptPrompt,
submissionInjection,
};
return {
projectId,