From bb07cbec147296cff35ca6cc281d01d3766ac613 Mon Sep 17 00:00:00 2001 From: "propr-dev[bot]" <1316198+propr-dev[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:05:03 +0000 Subject: [PATCH 1/3] fix(ai): Resolve issue #2109 - Desktop packaged Connect: stabilize macOS browser Implemented by ProPR AI using gpt-5.6-sol model. Implementation completed successfully. --- .../packaged-connect-lifecycle.test.mjs | 2 + apps/desktop/src/main.ts | 17 ++++++-- .../src/packaged-approval-session.test.ts | 25 +++++++++++ apps/desktop/src/packaged-approval-session.ts | 42 +++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs b/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs index c045e8365..fafce1f78 100644 --- a/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs +++ b/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs @@ -380,6 +380,8 @@ describe('packaged Connect bounded child lifecycle', () => { assert.ok(manual >= 0 && browser >= 0 && credential >= 0 && reprobeReady >= 0 && activation >= 0 && publication >= 0 && react >= 0); assert.match(main, /await stages\.waitFor\('CREDENTIAL_COMMITTED'\)[\s\S]*?await stages\.waitFor\('AUTHENTICATED_REPROBE_READY'\)[\s\S]*?await stages\.waitFor\('ACTIVATION_COMMITTED'\)[\s\S]*?await stages\.waitFor\('ACTIVATION_PUBLISHED'\)[\s\S]*?await stages\.waitFor\('REACT_CONNECTED'\)/u); + assert.match(main, /Packaged pairing expiry classification failed[\s\S]*?await waitForApprovalIdle\(\)[\s\S]*?JOURNEY_NEGATIVE_CANCEL[\s\S]*?Packaged pairing cancellation classification failed[\s\S]*?await waitForApprovalIdle\(\)/u); + assert.match(main, /await waitForApprovalIdle\(\);\s+reportPackagedConnectJourneyStage\(phase === 'pair'\s+\? 'JOURNEY_PAIR_COMPLETE'/u); assert.match(main, /if \(packagedSmokeTest && !transportSmoke && !connectJourney\)/u); assert.match(main, /if \(packagedSmokeTest && !connectJourney\) \{/u); assert.doesNotMatch(main, /JOURNEY_PAIR_RENDERER|JOURNEY_REPROBE_RENDERER/u); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 8ce0d7a82..6c0c3dc2f 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -30,6 +30,7 @@ import { openApprovedDesktopPairingUrl } from './pairing-browser'; import { clearPackagedApprovalStorage, createPackagedApprovalNavigation, + createPackagedApprovalTaskTracker, packagedApprovalPartition, } from './packaged-approval-session'; import { createDesktopShutdownCoordinator } from './shutdown'; @@ -601,6 +602,7 @@ const runPackagedConnectJourneySmoke = async ( window: BrowserWindow, profiles: ProfileStore, credentials: DesktopCredentialService, + waitForApprovalIdle: () => Promise, endpoint: string, phase: 'pair' | 'reprobe', stages: PackagedJourneyStageTracker, @@ -644,6 +646,7 @@ const runPackagedConnectJourneySmoke = async ( } }, ); + await waitForApprovalIdle(); reportPackagedConnectJourneyStage('JOURNEY_NEGATIVE_CANCEL'); await setMode('cancel'); const cancelledPairing = credentials.pair({ @@ -659,6 +662,7 @@ const runPackagedConnectJourneySmoke = async ( } }, ); + await waitForApprovalIdle(); reportPackagedConnectJourneyStage('JOURNEY_NEGATIVE_STATE'); const failedProfiles = await profiles.list(); if (failedProfiles.profiles.some(profile => profile.id.startsWith('negative-'))) { @@ -769,6 +773,7 @@ const runPackagedConnectJourneySmoke = async ( || transportEvidence.authenticatedSockets < requiredAuthenticatedRequests) { throw new Error('Packaged Connect authenticated transport proof timed out'); } + await waitForApprovalIdle(); reportPackagedConnectJourneyStage(phase === 'pair' ? 'JOURNEY_PAIR_COMPLETE' : 'JOURNEY_REPROBE_COMPLETE'); @@ -1187,11 +1192,14 @@ if (!hasSingleInstanceLock) { return status; }, }); + const packagedJourneyApprovals = connectSmoke?.journeyEndpoint + ? createPackagedApprovalTaskTracker(openPackagedJourneyApproval) + : null; const credentials = new DesktopCredentialService({ profiles, fetch: session.defaultSession.fetch.bind(session.defaultSession) as typeof globalThis.fetch, - openPairingBrowser: connectSmoke?.journeyEndpoint - ? openPackagedJourneyApproval + openPairingBrowser: packagedJourneyApprovals + ? packagedJourneyApprovals.open : request => openApprovedDesktopPairingUrl(request, shell), clientName: `ProPR Desktop (${process.platform})`, reportRevocationFailure: diagnostic => { @@ -1270,11 +1278,14 @@ if (!hasSingleInstanceLock) { reportPackagedConnectJourneyStage('JOURNEY_DISCOVERY_RENDERER'); const readyFields = await runPackagedConnectDiscoverySmoke(mainWindow); if (connectSmoke.journeyEndpoint && connectSmoke.journeyPhase) { - if (!journeyStages) throw new Error('Packaged Connect journey stage tracker was unavailable'); + if (!journeyStages || !packagedJourneyApprovals) { + throw new Error('Packaged Connect journey stage tracker was unavailable'); + } await runPackagedConnectJourneySmoke( mainWindow, profiles, credentials, + packagedJourneyApprovals.waitForIdle, connectSmoke.journeyEndpoint, connectSmoke.journeyPhase, journeyStages, diff --git a/apps/desktop/src/packaged-approval-session.test.ts b/apps/desktop/src/packaged-approval-session.test.ts index 152420a51..749db7434 100644 --- a/apps/desktop/src/packaged-approval-session.test.ts +++ b/apps/desktop/src/packaged-approval-session.test.ts @@ -5,6 +5,7 @@ import type { BrowserWindow, Session } from 'electron'; import { clearPackagedApprovalStorage, createPackagedApprovalNavigation, + createPackagedApprovalTaskTracker, packagedApprovalPartition, } from './packaged-approval-session'; @@ -204,6 +205,30 @@ describe('packaged pairing approval isolated session', () => { await controller.cleanup(); }); + it('drains delayed approval work exactly once before the next pairing case', async () => { + const requested: string[] = []; + const releases: Array<() => void> = []; + const tracker = createPackagedApprovalTaskTracker(async request => { + requested.push(request); + await new Promise(resolve => { releases.push(resolve); }); + }); + + for (const request of ['expiry', 'cancel', 'success']) { + const opened = tracker.open(request); + await new Promise(resolve => setImmediate(resolve)); + let idle = false; + const drained = tracker.waitForIdle().then(() => { idle = true; }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(idle, false); + assert.equal(requested.filter(value => value === request).length, 1); + releases.shift()?.(); + await Promise.all([opened, drained]); + assert.equal(idle, true); + } + + assert.deepEqual(requested, ['expiry', 'cancel', 'success']); + }); + it('cancels an incidental resource without invalidating the exact main-frame approval', async () => { const value = harness(); const originalLoad = value.window.load; diff --git a/apps/desktop/src/packaged-approval-session.ts b/apps/desktop/src/packaged-approval-session.ts index e53b377cc..8dae0324d 100644 --- a/apps/desktop/src/packaged-approval-session.ts +++ b/apps/desktop/src/packaged-approval-session.ts @@ -29,6 +29,11 @@ export interface PackagedApprovalNavigation { cleanup(): Promise; } +export interface PackagedApprovalTaskTracker { + open(request: Request): Promise; + waitForIdle(): Promise; +} + interface PackagedApprovalNavigationOptions { approvalUrl: string; approvalSession: Session; @@ -44,6 +49,43 @@ function cleanupRejected(): Error { return new Error(APPROVAL_CLEANUP_REJECTED); } +/** + * Keep acceptance-only browser work owned after pairing expiry/cancellation. + * The pairing protocol deliberately stops awaiting a browser callback once its + * lifetime ends, but a packaged smoke must still observe the one HTTP approval + * that it started before advancing the shared fixture or publishing READY. + */ +export const createPackagedApprovalTaskTracker = ( + open: (request: Request) => Promise, +): PackagedApprovalTaskTracker => { + const active = new Set>(); + let rejectedTask = false; + + return { + open(request) { + const task = Promise.resolve().then(() => open(request)); + active.add(task); + void task.then( + () => active.delete(task), + () => { + rejectedTask = true; + active.delete(task); + }, + ); + return task; + }, + async waitForIdle() { + while (active.size > 0) { + await Promise.all(Array.from(active, task => task.then( + () => undefined, + () => undefined, + ))); + } + if (rejectedTask) throw rejected(); + }, + }; +}; + export const clearPackagedApprovalStorage = async ( approvalSession: Pick, timeoutMs = APPROVAL_CLEANUP_TIMEOUT_MS, From f5af3000859ba6e65e54dc2314d4258f3b025f4a Mon Sep 17 00:00:00 2001 From: "propr-dev[bot]" <1316198+propr-dev[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:49:44 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat(ai):=20Implementation=20and=20focused?= =?UTF-8?q?=20validation=20are=20complete.=20I=E2=80=99m=20doing=20one=20f?= =?UTF-8?q?inal=20full=20desktop-suite=20pass=20on=20the=20exact=20final?= =?UTF-8?q?=20tree,=20then=20I=E2=80=99ll=20report=20the=20platform=20limi?= =?UTF-8?q?tation:=20this=20workspace=20has=20no=20packaged=20Darwin/Linux?= =?UTF-8?q?=20artifacts,=20so=20the=20fresh=20x64/ARM64=20packaged=20matri?= =?UTF-8?q?x=20must=20be=20exercised=20by=20CI.=20No=20visual=20preview=20?= =?UTF-8?q?is=20warranted=20because=20all=20changes=20are=20harness/tracke?= =?UTF-8?q?r=20behavior=20and=20diagnostics.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementation and focused validation are complete. I’m doing one final full desktop-suite pass on the exact final tree, then I’ll report the platform limitation: this workspace has no packaged Darwin/Linux artifacts, so the fresh x64/ARM64 packaged matrix must be exercised by CI. No visual preview is warranted because all changes are harness/tracker behavior and diagnostics. PR: #2110 Comment by: @integry (ID: 5544223426) Model: gpt-5.6-sol --- .../scripts/packaged-connect-evidence.mjs | 18 +++++++- .../packaged-connect-evidence.test.mjs | 15 ++++++- .../packaged-connect-lifecycle.test.mjs | 12 +++++- .../scripts/smoke-packaged-connect.mjs | 43 +++++++++++++++++-- apps/desktop/src/main.ts | 4 ++ .../src/packaged-approval-session.test.ts | 10 ++++- apps/desktop/src/packaged-approval-session.ts | 13 ++++++ 7 files changed, 104 insertions(+), 11 deletions(-) diff --git a/apps/desktop/scripts/packaged-connect-evidence.mjs b/apps/desktop/scripts/packaged-connect-evidence.mjs index 9a30cc787..5d6ab13f5 100644 --- a/apps/desktop/scripts/packaged-connect-evidence.mjs +++ b/apps/desktop/scripts/packaged-connect-evidence.mjs @@ -4,10 +4,17 @@ export const PACKAGED_CONNECT_EXPECTED_DISCOVERY_COUNT = 10; export const PACKAGED_CONNECT_EVIDENCE_FAILURE_CODES = Object.freeze([ 'DISCOVERY_COUNT_MISMATCH', 'DISCOVERY_AUTHORIZATION_PRESENT', - 'PAIRING_START_COUNT_MISMATCH', + 'PAIRING_START_MISSING', + 'PAIRING_START_DUPLICATE', 'PAIRING_BROWSER_COUNT_MISMATCH', 'PAIRING_POLL_COUNT_MISMATCH', 'PAIRING_ACTIVATION_COUNT_MISMATCH', + 'PAIRING_METHOD_MISMATCH', + 'PAIRING_BROWSER_CREDENTIAL_PRESENT', + 'PAIRING_INTENT_SEQUENCE_MISMATCH', + 'PAIRING_LIFECYCLE_ISOLATION_FAILED', + 'PAIRING_REQUEST_AFTER_TERMINAL', + 'DELAYED_APPROVAL_READINESS_MISSING', 'BOOTSTRAP_AUTHORIZATION_PRESENT', 'AUTHENTICATED_REST_COUNT_MISMATCH', 'AUTHENTICATED_SOCKET_COUNT_MISMATCH', @@ -41,10 +48,17 @@ const failureChecks = Object.freeze([ ['DISCOVERY_COUNT_MISMATCH', evidence => evidence.discoveryCount !== PACKAGED_CONNECT_EXPECTED_DISCOVERY_COUNT], ['DISCOVERY_AUTHORIZATION_PRESENT', evidence => evidence.discoveryAuthorizationPresent], - ['PAIRING_START_COUNT_MISMATCH', evidence => evidence.pairingStartCount !== 3], + ['PAIRING_START_MISSING', evidence => evidence.pairingStartCount < 3], + ['PAIRING_START_DUPLICATE', evidence => evidence.pairingStartCount > 3], ['PAIRING_BROWSER_COUNT_MISMATCH', evidence => evidence.pairingBrowserCount !== 3], ['PAIRING_POLL_COUNT_MISMATCH', evidence => evidence.pairingPollCount !== 1], ['PAIRING_ACTIVATION_COUNT_MISMATCH', evidence => evidence.pairingActivationCount !== 1], + ['PAIRING_METHOD_MISMATCH', evidence => !evidence.pairingMethodBoundaryValid], + ['PAIRING_BROWSER_CREDENTIAL_PRESENT', evidence => evidence.pairingBrowserCredentialPresent], + ['PAIRING_INTENT_SEQUENCE_MISMATCH', evidence => !evidence.pairingIntentSequenceValid], + ['PAIRING_LIFECYCLE_ISOLATION_FAILED', evidence => !evidence.pairingLifecycleIsolated], + ['PAIRING_REQUEST_AFTER_TERMINAL', evidence => evidence.pairingRequestAfterTerminal], + ['DELAYED_APPROVAL_READINESS_MISSING', evidence => !evidence.delayedApprovalReadinessProven], ['BOOTSTRAP_AUTHORIZATION_PRESENT', evidence => evidence.bootstrapAuthorizationPresent], ['AUTHENTICATED_REST_COUNT_MISMATCH', evidence => evidence.authenticatedRestCount < 2], ['AUTHENTICATED_SOCKET_COUNT_MISMATCH', evidence => evidence.authenticatedSocketCount < 2], diff --git a/apps/desktop/scripts/packaged-connect-evidence.test.mjs b/apps/desktop/scripts/packaged-connect-evidence.test.mjs index 6afa7aeb7..3883cf3a7 100644 --- a/apps/desktop/scripts/packaged-connect-evidence.test.mjs +++ b/apps/desktop/scripts/packaged-connect-evidence.test.mjs @@ -15,6 +15,12 @@ const passingEvidence = () => ({ pairingBrowserCount: 3, pairingPollCount: 1, pairingActivationCount: 1, + pairingMethodBoundaryValid: true, + pairingBrowserCredentialPresent: false, + pairingIntentSequenceValid: true, + pairingLifecycleIsolated: true, + pairingRequestAfterTerminal: false, + delayedApprovalReadinessProven: true, bootstrapAuthorizationPresent: false, authenticatedRestCount: 2, authenticatedSocketCount: 2, @@ -31,10 +37,17 @@ const passingEvidence = () => ({ const failingEvidence = Object.freeze({ DISCOVERY_COUNT_MISMATCH: { discoveryCount: 8 }, DISCOVERY_AUTHORIZATION_PRESENT: { discoveryAuthorizationPresent: true }, - PAIRING_START_COUNT_MISMATCH: { pairingStartCount: 2 }, + PAIRING_START_MISSING: { pairingStartCount: 2 }, + PAIRING_START_DUPLICATE: { pairingStartCount: 4 }, PAIRING_BROWSER_COUNT_MISMATCH: { pairingBrowserCount: 2 }, PAIRING_POLL_COUNT_MISMATCH: { pairingPollCount: 2 }, PAIRING_ACTIVATION_COUNT_MISMATCH: { pairingActivationCount: 2 }, + PAIRING_METHOD_MISMATCH: { pairingMethodBoundaryValid: false }, + PAIRING_BROWSER_CREDENTIAL_PRESENT: { pairingBrowserCredentialPresent: true }, + PAIRING_INTENT_SEQUENCE_MISMATCH: { pairingIntentSequenceValid: false }, + PAIRING_LIFECYCLE_ISOLATION_FAILED: { pairingLifecycleIsolated: false }, + PAIRING_REQUEST_AFTER_TERMINAL: { pairingRequestAfterTerminal: true }, + DELAYED_APPROVAL_READINESS_MISSING: { delayedApprovalReadinessProven: false }, BOOTSTRAP_AUTHORIZATION_PRESENT: { bootstrapAuthorizationPresent: true }, AUTHENTICATED_REST_COUNT_MISMATCH: { authenticatedRestCount: 1 }, AUTHENTICATED_SOCKET_COUNT_MISMATCH: { authenticatedSocketCount: 1 }, diff --git a/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs b/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs index fafce1f78..37bcad22c 100644 --- a/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs +++ b/apps/desktop/scripts/packaged-connect-lifecycle.test.mjs @@ -109,11 +109,19 @@ describe('packaged Connect bounded child lifecycle', () => { assert.match(accounting, /pairingActivationCount: pairingActivations\.length/u); const evaluator = await readFile(new URL('./packaged-connect-evidence.mjs', import.meta.url), 'utf8'); - assert.match(evaluator, /evidence\.pairingStartCount !== 3/u); + assert.match(evaluator, /evidence\.pairingStartCount < 3/u); + assert.match(evaluator, /evidence\.pairingStartCount > 3/u); assert.match(evaluator, /evidence\.pairingBrowserCount !== 3/u); assert.match(evaluator, /evidence\.pairingPollCount !== 1/u); assert.match(evaluator, /evidence\.pairingActivationCount !== 1/u); + assert.match(harness, /request\.method === 'POST'[\s\S]*?request\.url === '\/api\/desktop\/pairings'/u); + assert.match(harness, /request\.method === 'GET'[\s\S]*?\/\\\/browser\$\//u); + assert.match(harness, /pairingBrowserCredentialPresent: pairingBrowsers\.some/u); assert.doesNotMatch(evaluator, /pairingPollCount < 3/u); + assert.match(harness, /const approvalReadinessDelayMs = process\.platform === 'darwin' \? 300 : 0/u); + assert.match(harness, /pairingIntentSequenceValid: hasExactModes\(pairingStarts\) && hasExactModes\(pairingBrowsers\)/u); + assert.match(harness, /pairingRequestAfterTerminal: bootstrap\.length !== pairingRequestCountAtPairTerminal/u); + assert.match(harness, /delayedApprovalReadinessProven: process\.platform !== 'darwin'/u); }); test('accepts an exact ready proof followed by a clean exit', async () => { @@ -380,7 +388,7 @@ describe('packaged Connect bounded child lifecycle', () => { assert.ok(manual >= 0 && browser >= 0 && credential >= 0 && reprobeReady >= 0 && activation >= 0 && publication >= 0 && react >= 0); assert.match(main, /await stages\.waitFor\('CREDENTIAL_COMMITTED'\)[\s\S]*?await stages\.waitFor\('AUTHENTICATED_REPROBE_READY'\)[\s\S]*?await stages\.waitFor\('ACTIVATION_COMMITTED'\)[\s\S]*?await stages\.waitFor\('ACTIVATION_PUBLISHED'\)[\s\S]*?await stages\.waitFor\('REACT_CONNECTED'\)/u); - assert.match(main, /Packaged pairing expiry classification failed[\s\S]*?await waitForApprovalIdle\(\)[\s\S]*?JOURNEY_NEGATIVE_CANCEL[\s\S]*?Packaged pairing cancellation classification failed[\s\S]*?await waitForApprovalIdle\(\)/u); + assert.match(main, /Packaged pairing expiry classification failed[\s\S]*?await waitForApprovalIdle\(\)[\s\S]*?JOURNEY_NEGATIVE_CANCEL[\s\S]*?const approvalReady = waitForNextApproval\(\)[\s\S]*?await approvalReady[\s\S]*?Packaged pairing cancellation classification failed[\s\S]*?await waitForApprovalIdle\(\)/u); assert.match(main, /await waitForApprovalIdle\(\);\s+reportPackagedConnectJourneyStage\(phase === 'pair'\s+\? 'JOURNEY_PAIR_COMPLETE'/u); assert.match(main, /if \(packagedSmokeTest && !transportSmoke && !connectJourney\)/u); assert.match(main, /if \(packagedSmokeTest && !connectJourney\) \{/u); diff --git a/apps/desktop/scripts/smoke-packaged-connect.mjs b/apps/desktop/scripts/smoke-packaged-connect.mjs index 243d63e24..379c2fba1 100644 --- a/apps/desktop/scripts/smoke-packaged-connect.mjs +++ b/apps/desktop/scripts/smoke-packaged-connect.mjs @@ -98,6 +98,8 @@ const createPackagedJourneyFixture = async () => { let active = false; let binding; let mode = 'success'; + let modeGeneration = 0; + const approvalReadinessDelayMs = process.platform === 'darwin' ? 300 : 0; const cors = { 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Headers': 'Authorization, Content-Type, X-ProPR-Desktop-Transport-Scope', @@ -130,7 +132,11 @@ const createPackagedJourneyFixture = async () => { authorization: request.headers.authorization ?? null, origin: request.headers.origin ?? null, transportScope: request.headers[DESKTOP_TRANSPORT_SCOPE_HEADER.toLowerCase()] ?? null, + credentialHeadersPresent: Object.keys(request.headers).some(name => + ['authorization', 'cookie', 'proxy-authorization'].includes(name.toLowerCase())), socketIo: false, + fixtureMode: mode, + fixtureModeGeneration: modeGeneration, }; requests.push(record); if (request.method === 'OPTIONS') { @@ -145,6 +151,7 @@ const createPackagedJourneyFixture = async () => { throw new Error('invalid fixture mode'); } mode = requestedMode; + modeGeneration += 1; approved = false; binding = undefined; response.writeHead(204, cors); @@ -208,6 +215,12 @@ const createPackagedJourneyFixture = async () => { return; } if (request.method === 'GET' && request.url === `/api/desktop/pairings/${pairingId}/browser`) { + if (approvalReadinessDelayMs > 0) { + await new Promise(resolve => setTimeout(resolve, approvalReadinessDelayMs)); + record.approvalReadinessDelayed = true; + } + record.fixtureModeStable = record.fixtureMode === mode + && record.fixtureModeGeneration === modeGeneration; approved = true; response.writeHead(200, { 'Cache-Control': 'no-store', 'Content-Type': 'text/html' }); response.end('Desktop approved

Approved

'); @@ -600,6 +613,10 @@ try { }); outcome = await runPhase('pair'); if (outcome.ok && journeyFixture) { + const pairingRequestCountAtPairTerminal = journeyFixture.requests.filter(request => + request.method !== 'OPTIONS' + && (request.url === '/api/desktop/pairings' + || /^\/api\/desktop\/pairings\/[^/]+\/(?:browser|poll|activate)$/u.test(request.url ?? ''))).length; outcome = await runPhase('reprobe'); if (outcome.ok) { const applicationRequests = journeyFixture.requests.filter(request => request.method !== 'OPTIONS'); @@ -608,10 +625,17 @@ try { request.url === '/api/desktop/pairings' || /^\/api\/desktop\/pairings\/[^/]+\/(?:poll|activate)$/u.test(request.url ?? '') || /\/browser$/u.test(request.url ?? '')); - const pairingStarts = bootstrap.filter(request => request.url === '/api/desktop/pairings'); - const pairingBrowsers = bootstrap.filter(request => /\/browser$/u.test(request.url ?? '')); - const pairingPolls = bootstrap.filter(request => /\/poll$/u.test(request.url ?? '')); - const pairingActivations = bootstrap.filter(request => /\/activate$/u.test(request.url ?? '')); + const pairingStarts = bootstrap.filter(request => request.method === 'POST' + && request.url === '/api/desktop/pairings'); + const pairingBrowsers = bootstrap.filter(request => request.method === 'GET' + && /\/browser$/u.test(request.url ?? '')); + const pairingPolls = bootstrap.filter(request => request.method === 'POST' + && /\/poll$/u.test(request.url ?? '')); + const pairingActivations = bootstrap.filter(request => request.method === 'POST' + && /\/activate$/u.test(request.url ?? '')); + const intendedPairingModes = ['expiry', 'cancel', 'success']; + const hasExactModes = requests => requests.length === intendedPairingModes.length + && requests.every((request, index) => request.fixtureMode === intendedPairingModes[index]); const authenticatedRest = applicationRequests.filter(request => request.socketIo === false && request.url === '/api/auth/user' @@ -631,6 +655,17 @@ try { pairingBrowserCount: pairingBrowsers.length, pairingPollCount: pairingPolls.length, pairingActivationCount: pairingActivations.length, + pairingMethodBoundaryValid: bootstrap.length === pairingStarts.length + + pairingBrowsers.length + pairingPolls.length + pairingActivations.length, + pairingBrowserCredentialPresent: pairingBrowsers.some(request => + request.credentialHeadersPresent === true), + pairingIntentSequenceValid: hasExactModes(pairingStarts) && hasExactModes(pairingBrowsers), + pairingLifecycleIsolated: pairingBrowsers.every(request => request.fixtureModeStable === true) + && pairingPolls.every(request => request.fixtureMode === 'success') + && pairingActivations.every(request => request.fixtureMode === 'success'), + pairingRequestAfterTerminal: bootstrap.length !== pairingRequestCountAtPairTerminal, + delayedApprovalReadinessProven: process.platform !== 'darwin' + || pairingBrowsers.every(request => request.approvalReadinessDelayed === true), bootstrapAuthorizationPresent: bootstrap.some(request => request.authorization !== null), authenticatedRestCount: authenticatedRest.length, authenticatedSocketCount: socketEvidence.authenticatedSocketCount, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 6c0c3dc2f..22d8745f2 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -602,6 +602,7 @@ const runPackagedConnectJourneySmoke = async ( window: BrowserWindow, profiles: ProfileStore, credentials: DesktopCredentialService, + waitForNextApproval: () => Promise, waitForApprovalIdle: () => Promise, endpoint: string, phase: 'pair' | 'reprobe', @@ -649,9 +650,11 @@ const runPackagedConnectJourneySmoke = async ( await waitForApprovalIdle(); reportPackagedConnectJourneyStage('JOURNEY_NEGATIVE_CANCEL'); await setMode('cancel'); + const approvalReady = waitForNextApproval(); const cancelledPairing = credentials.pair({ id: 'negative-cancel', label: 'Packaged cancel', apiBaseUrl: endpoint, }); + await approvalReady; await new Promise(resolve => setTimeout(resolve, 50)); credentials.cancelPairing('negative-cancel'); await cancelledPairing.then( @@ -1285,6 +1288,7 @@ if (!hasSingleInstanceLock) { mainWindow, profiles, credentials, + packagedJourneyApprovals.waitForNextOpen, packagedJourneyApprovals.waitForIdle, connectSmoke.journeyEndpoint, connectSmoke.journeyPhase, diff --git a/apps/desktop/src/packaged-approval-session.test.ts b/apps/desktop/src/packaged-approval-session.test.ts index 749db7434..07de6bc20 100644 --- a/apps/desktop/src/packaged-approval-session.test.ts +++ b/apps/desktop/src/packaged-approval-session.test.ts @@ -205,7 +205,7 @@ describe('packaged pairing approval isolated session', () => { await controller.cleanup(); }); - it('drains delayed approval work exactly once before the next pairing case', async () => { + it('reports owned approval readiness and drains delayed work before the next pairing case', async () => { const requested: string[] = []; const releases: Array<() => void> = []; const tracker = createPackagedApprovalTaskTracker(async request => { @@ -214,8 +214,14 @@ describe('packaged pairing approval isolated session', () => { }); for (const request of ['expiry', 'cancel', 'success']) { - const opened = tracker.open(request); + let ready = false; + const readiness = tracker.waitForNextOpen().then(() => { ready = true; }); + const concurrentReadiness = tracker.waitForNextOpen(); await new Promise(resolve => setImmediate(resolve)); + assert.equal(ready, false); + const opened = tracker.open(request); + await Promise.all([readiness, concurrentReadiness]); + assert.equal(ready, true); let idle = false; const drained = tracker.waitForIdle().then(() => { idle = true; }); await new Promise(resolve => setImmediate(resolve)); diff --git a/apps/desktop/src/packaged-approval-session.ts b/apps/desktop/src/packaged-approval-session.ts index 8dae0324d..230d0283c 100644 --- a/apps/desktop/src/packaged-approval-session.ts +++ b/apps/desktop/src/packaged-approval-session.ts @@ -31,6 +31,7 @@ export interface PackagedApprovalNavigation { export interface PackagedApprovalTaskTracker { open(request: Request): Promise; + waitForNextOpen(): Promise; waitForIdle(): Promise; } @@ -59,12 +60,20 @@ export const createPackagedApprovalTaskTracker = ( open: (request: Request) => Promise, ): PackagedApprovalTaskTracker => { const active = new Set>(); + const nextOpenWaiters = new Set<{ after: number; resolve(): void }>(); + let openGeneration = 0; let rejectedTask = false; return { open(request) { const task = Promise.resolve().then(() => open(request)); active.add(task); + openGeneration += 1; + for (const waiter of nextOpenWaiters) { + if (waiter.after >= openGeneration) continue; + nextOpenWaiters.delete(waiter); + waiter.resolve(); + } void task.then( () => active.delete(task), () => { @@ -74,6 +83,10 @@ export const createPackagedApprovalTaskTracker = ( ); return task; }, + waitForNextOpen() { + const after = openGeneration; + return new Promise(resolve => nextOpenWaiters.add({ after, resolve })); + }, async waitForIdle() { while (active.size > 0) { await Promise.all(Array.from(active, task => task.then( From 24ffd83242dffbf5f903b201bbdcbaeba46200c7 Mon Sep 17 00:00:00 2001 From: "propr-dev[bot]" <1316198+propr-dev[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:02:56 +0000 Subject: [PATCH 3/3] feat(ai): Updated the stale structural assertion in [packaged-connect-platform.test.mjs](/tmp/git-processor/worktrees/integry/propr/pr-2110-followup-2026-09-04T18-00-19/apps/desktop/scripts/packaged-connect-platform.test.mjs:117). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the stale structural assertion in [packaged-connect-platform.test.mjs](/tmp/git-processor/worktrees/integry/propr/pr-2110-followup-2026-09-04T18-00-19/apps/desktop/scripts/packaged-connect-platform.test.mjs:117). It now strictly verifies: `pair` completes → successful journey-fixture block begins → terminal request snapshot is captured → `reprobe` runs. No runtime or Windows changes were made. Darwin ARM64 behavior remains untouched for fresh CI classification. Validation: 41 tests passed across the platform and lifecycle suites; `git diff --check` passed. No visual preview was needed for this test-only change. PR: #2110 Comment by: @integry (ID: 5544503194) Model: gpt-5.6-sol --- apps/desktop/scripts/packaged-connect-platform.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/scripts/packaged-connect-platform.test.mjs b/apps/desktop/scripts/packaged-connect-platform.test.mjs index a37981688..d97b4d454 100644 --- a/apps/desktop/scripts/packaged-connect-platform.test.mjs +++ b/apps/desktop/scripts/packaged-connect-platform.test.mjs @@ -114,7 +114,7 @@ describe('packaged Connect target-native credential setup', () => { const stable = darwinRunner.indexOf('node "$signature_verifier" stable'); assert.ok(establish >= 0 && establish < smoke && smoke < stable); assert.match(packagedConnectSmoke, /const runPhase = async phase => await runPackagedConnectLifecycle\([\s\S]*?spawn: spawnLifecycleProcess/u); - assert.match(packagedConnectSmoke, /outcome = await runPhase\('pair'\);[\s\S]*?if \(outcome\.ok && journeyFixture\) \{\s*outcome = await runPhase\('reprobe'\);/u); + assert.match(packagedConnectSmoke, /outcome = await runPhase\('pair'\);\s*if \(outcome\.ok && journeyFixture\) \{\s*const pairingRequestCountAtPairTerminal = journeyFixture\.requests\.filter\(request =>[^{};]+\)\.length;\s*outcome = await runPhase\('reprobe'\);/u); assert.match(workflow, /target: darwin-x64\s+runner: macos-15-intel\s+platform: darwin\s+arch: x64/u); assert.match(workflow, /target: darwin-arm64\s+runner: macos-15\s+platform: darwin\s+arch: arm64/u); });