test(pi): hold packaged rpc confirmation past timeout
This commit is contained in:
1 parent
4b75d201b8
commit
26d616c9b6
8 files changed
+105
No files matched your search
@@ -204,6 +204,15 @@ Follow-up plan from cumulative HEAD `621ebb17810394f6f7b97154cb01217bc9112857`:
|
|||||||
runtime failure. The proof was corrected to arm prompt and compact delay
|
runtime failure. The proof was corrected to arm prompt and compact delay
|
||||||
explicitly through E2E-only Main seams, removing message-shape dependence;
|
explicitly through E2E-only Main seams, removing message-shape dependence;
|
||||||
this was a proof-wiring failure, not a product fallback or relaxed gate.
|
this was a proof-wiring failure, not a product fallback or relaxed gate.
|
||||||
|
- The explicit Provider arm then proved the loopback HTTP response remained
|
||||||
|
open for 12 seconds, but locked Pi acknowledged the local prompt RPC before
|
||||||
|
the first Provider response on this controlled path. The proof therefore
|
||||||
|
separates the two facts instead of claiming causality: the Provider response
|
||||||
|
remains delayed for 12 seconds, and an E2E-only one-shot Pi RPC response hold
|
||||||
|
independently crosses the 10-second client threshold. The hold is armed only
|
||||||
|
through the existing Main fault-injection surface, is absent from Renderer
|
||||||
|
and product configuration, and preserves the real packaged Pi command,
|
||||||
|
session, Provider request, events, and cleanup path.
|
||||||
|
|
||||||
- `pnpm install --frozen-lockfile` — passed with package-manager-pinned pnpm
|
- `pnpm install --frozen-lockfile` — passed with package-manager-pinned pnpm
|
||||||
`10.33.4` and locked Pi `0.84.2`.
|
`10.33.4` and locked Pi `0.84.2`.
|
||||||
|
|||||||
@@ -1898,6 +1898,11 @@ export function armFinalAsarResilienceCompactDelay(): { delayMs: number } {
|
|||||||
const run = resilienceCompositionRun;
|
const run = resilienceCompositionRun;
|
||||||
if (!run) throw new Error('PI resilience proof is not running');
|
if (!run) throw new Error('PI resilience proof is not running');
|
||||||
run.provider.armDelayedCompaction();
|
run.provider.armDelayedCompaction();
|
||||||
|
resilienceRuntime(run).delayNextTopLevelConfirmationForProof(
|
||||||
|
run.targetConversationId,
|
||||||
|
'compact',
|
||||||
|
PROOF_MUTATION_CONFIRMATION_DELAY_MS,
|
||||||
|
);
|
||||||
return { delayMs: PROOF_MUTATION_CONFIRMATION_DELAY_MS };
|
return { delayMs: PROOF_MUTATION_CONFIRMATION_DELAY_MS };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1905,6 +1910,11 @@ export function armFinalAsarResiliencePromptDelay(): { delayMs: number } {
|
|||||||
const run = resilienceCompositionRun;
|
const run = resilienceCompositionRun;
|
||||||
if (!run) throw new Error('PI resilience proof is not running');
|
if (!run) throw new Error('PI resilience proof is not running');
|
||||||
run.provider.armDelayedPrompt();
|
run.provider.armDelayedPrompt();
|
||||||
|
resilienceRuntime(run).delayNextTopLevelConfirmationForProof(
|
||||||
|
run.targetConversationId,
|
||||||
|
'prompt',
|
||||||
|
PROOF_MUTATION_CONFIRMATION_DELAY_MS,
|
||||||
|
);
|
||||||
return { delayMs: PROOF_MUTATION_CONFIRMATION_DELAY_MS };
|
return { delayMs: PROOF_MUTATION_CONFIRMATION_DELAY_MS };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export class PiRpcClient {
|
|||||||
private readonly pending = new Map<string, PendingRequest>();
|
private readonly pending = new Map<string, PendingRequest>();
|
||||||
private readonly retiredIds = new Set<string>();
|
private readonly retiredIds = new Set<string>();
|
||||||
private readonly listeners = new Set<(event: PiRpcEvent) => void>();
|
private readonly listeners = new Set<(event: PiRpcEvent) => void>();
|
||||||
|
private readonly proofResponseDelays = new Map<string, number>();
|
||||||
private sequence = 0;
|
private sequence = 0;
|
||||||
private invalidated: PiProcessError | null = null;
|
private invalidated: PiProcessError | null = null;
|
||||||
|
|
||||||
@@ -111,6 +112,13 @@ export class PiRpcClient {
|
|||||||
return () => this.listeners.delete(listener);
|
return () => this.listeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delayNextResponseForProof(commandType: string, delayMs: number): void {
|
||||||
|
if (!commandType || !Number.isSafeInteger(delayMs) || delayMs <= 0) {
|
||||||
|
throw new Error('Proof response delay requires a command type and positive safe delay');
|
||||||
|
}
|
||||||
|
this.proofResponseDelays.set(commandType, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
async request<T = unknown>(
|
async request<T = unknown>(
|
||||||
command: PiRpcCommand,
|
command: PiRpcCommand,
|
||||||
options: PiRpcRequestOptions = {},
|
options: PiRpcRequestOptions = {},
|
||||||
@@ -161,6 +169,22 @@ export class PiRpcClient {
|
|||||||
if (!pending) {
|
if (!pending) {
|
||||||
throw new PiProcessError('PI_RPC_PROTOCOL_ERROR', `Pi RPC response used unknown id ${value.id}`);
|
throw new PiProcessError('PI_RPC_PROTOCOL_ERROR', `Pi RPC response used unknown id ${value.id}`);
|
||||||
}
|
}
|
||||||
|
const proofDelayMs = this.proofResponseDelays.get(pending.commandType);
|
||||||
|
if (proofDelayMs !== undefined) {
|
||||||
|
this.proofResponseDelays.delete(pending.commandType);
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
this.accept(value);
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
this.onEventListenerError?.(error);
|
||||||
|
} catch {
|
||||||
|
// Proof diagnostics must not affect transport state.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, proofDelayMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
pending.cancel();
|
pending.cancel();
|
||||||
this.pending.delete(value.id);
|
this.pending.delete(value.id);
|
||||||
if (value.success) {
|
if (value.success) {
|
||||||
|
|||||||
@@ -1086,6 +1086,14 @@ export class PiConversationRuntime implements CodingConversationRuntime {
|
|||||||
return await this.pool.injectFailureForProof(conversationId, failure);
|
return await this.pool.injectFailureForProof(conversationId, failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delayNextTopLevelConfirmationForProof(
|
||||||
|
conversationId: string,
|
||||||
|
commandType: 'prompt' | 'compact',
|
||||||
|
delayMs: number,
|
||||||
|
): void {
|
||||||
|
this.pool.delayNextTopLevelConfirmationForProof(conversationId, commandType, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
getResilienceProofDiagnostics(): {
|
getResilienceProofDiagnostics(): {
|
||||||
pool: ReturnType<PiWorkerPool['getResilienceProofDiagnostics']>;
|
pool: ReturnType<PiWorkerPool['getResilienceProofDiagnostics']>;
|
||||||
subagents: ReturnType<PiSubagentScheduler['getDiagnostics']> | null;
|
subagents: ReturnType<PiSubagentScheduler['getDiagnostics']> | null;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface PiConversationWorker {
|
|||||||
subscribeInvalidation(listener: (error: PiProcessError) => void): () => void;
|
subscribeInvalidation(listener: (error: PiProcessError) => void): () => void;
|
||||||
stop(reason: PiWorkerStopReason): Promise<PiWorkerStopResult>;
|
stop(reason: PiWorkerStopReason): Promise<PiWorkerStopResult>;
|
||||||
injectFailureForProof?(failure: PiWorkerProofFailure): Promise<void>;
|
injectFailureForProof?(failure: PiWorkerProofFailure): Promise<void>;
|
||||||
|
delayNextResponseForProof?(commandType: string, delayMs: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PiWorkerSessionBinding {
|
export interface PiWorkerSessionBinding {
|
||||||
@@ -677,6 +678,21 @@ export class PiWorkerPool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delayNextTopLevelConfirmationForProof(
|
||||||
|
conversationId: string,
|
||||||
|
commandType: string,
|
||||||
|
delayMs: number,
|
||||||
|
): void {
|
||||||
|
const record = this.workers.get(conversationId);
|
||||||
|
if (!record || record.state === 'crashed') {
|
||||||
|
throw new Error('Conversation worker is not available for proof response delay');
|
||||||
|
}
|
||||||
|
if (!record.worker.delayNextResponseForProof) {
|
||||||
|
throw new Error('Conversation worker does not support proof response delay');
|
||||||
|
}
|
||||||
|
record.worker.delayNextResponseForProof(commandType, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
private async performShutdown(): Promise<void> {
|
private async performShutdown(): Promise<void> {
|
||||||
this.shuttingDown = true;
|
this.shuttingDown = true;
|
||||||
this.shutdownController.abort();
|
this.shutdownController.abort();
|
||||||
|
|||||||
@@ -412,6 +412,11 @@ export class PiWorkerProcess {
|
|||||||
return this.rpc.request<T>(command, options);
|
return this.rpc.request<T>(command, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delayNextResponseForProof(commandType: string, delayMs: number): void {
|
||||||
|
if (!this.rpc) throw new Error('Pi worker has not started');
|
||||||
|
this.rpc.delayNextResponseForProof(commandType, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
send(command: PiRpcCommand): Promise<void> {
|
send(command: PiRpcCommand): Promise<void> {
|
||||||
if (!this.rpc) {
|
if (!this.rpc) {
|
||||||
return Promise.reject(new PiProcessError(
|
return Promise.reject(new PiProcessError(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe('Pi packaged release proof wiring', () => {
|
|||||||
expect(scriptSource).toContain('setMainWindowVisible(electronApplication, false)');
|
expect(scriptSource).toContain('setMainWindowVisible(electronApplication, false)');
|
||||||
expect(scriptSource).toContain("evaluateProof(electronApplication, 'resilience.dispose-target')");
|
expect(scriptSource).toContain("evaluateProof(electronApplication, 'resilience.dispose-target')");
|
||||||
expect(proofSource).toContain('const PROOF_MUTATION_CONFIRMATION_DELAY_MS = 12_000;');
|
expect(proofSource).toContain('const PROOF_MUTATION_CONFIRMATION_DELAY_MS = 12_000;');
|
||||||
|
expect(proofSource).toContain('delayNextTopLevelConfirmationForProof');
|
||||||
expect(mainSource).toContain("'resilience.arm-compact-delay'");
|
expect(mainSource).toContain("'resilience.arm-compact-delay'");
|
||||||
expect(mainSource).toContain("'resilience.arm-prompt-delay'");
|
expect(mainSource).toContain("'resilience.arm-prompt-delay'");
|
||||||
expect(scriptSource).toContain("evaluateProof(electronApplication, 'resilience.arm-compact-delay')");
|
expect(scriptSource).toContain("evaluateProof(electronApplication, 'resilience.arm-compact-delay')");
|
||||||
|
|||||||
@@ -102,6 +102,38 @@ describe('strict Pi LF JSONL framing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Pi RPC client', () => {
|
describe('Pi RPC client', () => {
|
||||||
|
it('can deterministically hold one proof response past the mutation timeout', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
let written = '';
|
||||||
|
const writable = new Writable({
|
||||||
|
write(chunk, _encoding, callback) {
|
||||||
|
written += chunk.toString();
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const lateResults: Array<{ success: boolean }> = [];
|
||||||
|
const client = new PiRpcClient(writable, { generation: 1, defaultTimeoutMs: 10_000 });
|
||||||
|
client.delayNextResponseForProof('prompt', 12_000);
|
||||||
|
const requested = client.request(
|
||||||
|
{ type: 'prompt', message: 'proof response hold' },
|
||||||
|
{
|
||||||
|
retainAfterTimeout: true,
|
||||||
|
onLateResult: (result) => lateResults.push({ success: result.response?.success === true }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
void requested.catch(() => undefined);
|
||||||
|
await Promise.resolve();
|
||||||
|
const command = JSON.parse(written) as { id: string };
|
||||||
|
client.accept({ type: 'response', id: command.id, success: true });
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(10_000);
|
||||||
|
await expect(requested).rejects.toMatchObject({ code: 'PI_RPC_TIMEOUT' });
|
||||||
|
expect(client.pendingCount).toBe(1);
|
||||||
|
await vi.advanceTimersByTimeAsync(2_000);
|
||||||
|
expect(client.pendingCount).toBe(0);
|
||||||
|
expect(lateResults).toEqual([{ success: true }]);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps a mutation correlated after the 10 second confirmation timeout', async () => {
|
it('keeps a mutation correlated after the 10 second confirmation timeout', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
let written = '';
|
let written = '';
|
||||||
|
|||||||
Reference in new issue
Block a user