-
-
Notifications
You must be signed in to change notification settings - Fork 314
test(ios-snapshot): cover runner-presented interactive pipeline #3009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,12 @@ import fs from 'node:fs'; | |||||||||||||||||||
| import os from 'node:os'; | ||||||||||||||||||||
| import path from 'node:path'; | ||||||||||||||||||||
| import type { IosSnapshotAcquisition } from '@agent-device/contracts/ios-snapshot'; | ||||||||||||||||||||
| import { createIosSnapshotRequest, deriveIosCaptureHint } from '../ios-snapshot-planning.ts'; | ||||||||||||||||||||
| import { IosSnapshotEngineError, presentIosSnapshot } from './index.ts'; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| buildIosSnapshotPresentationKey, | ||||||||||||||||||||
| createIosSnapshotRequest, | ||||||||||||||||||||
| deriveIosCaptureHint, | ||||||||||||||||||||
| } from '../ios-snapshot-planning.ts'; | ||||||||||||||||||||
| import { IosSnapshotEngineError, presentIosSnapshot, publishIosSnapshot } from './index.ts'; | ||||||||||||||||||||
| import type { RawSnapshotNode, Rect } from '@agent-device/kernel/snapshot'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const REPO_ROOT = path.resolve(import.meta.dirname, '..', '..', '..', '..'); | ||||||||||||||||||||
|
|
@@ -16,19 +20,26 @@ let swiftHarnessExecutable: string | undefined; | |||||||||||||||||||
|
|
||||||||||||||||||||
| type DifferentialCase = Readonly<{ | ||||||||||||||||||||
| name: string; | ||||||||||||||||||||
| route: 'acquired' | 'runner-presented'; | ||||||||||||||||||||
| projection: 'regular' | 'raw'; | ||||||||||||||||||||
| interactiveOnly: false; | ||||||||||||||||||||
| interactiveOnly: boolean; | ||||||||||||||||||||
| depth: number | null; | ||||||||||||||||||||
| scope: string | null; | ||||||||||||||||||||
| foldPolicy: 'cursor-projected' | 'plain-viewport'; | ||||||||||||||||||||
| viewport: Rect; | ||||||||||||||||||||
| nodes: readonly RawSnapshotNode[]; | ||||||||||||||||||||
| requiredLabels?: readonly string[]; | ||||||||||||||||||||
| absentLabels?: readonly string[]; | ||||||||||||||||||||
| clippedLabel?: Readonly<{ label: string; rect: Rect }>; | ||||||||||||||||||||
| }>; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| type DifferentialOutcome = Readonly<{ | ||||||||||||||||||||
| name?: string; | ||||||||||||||||||||
| outcome: 'success' | 'failure'; | ||||||||||||||||||||
| nodes: readonly CanonicalNode[]; | ||||||||||||||||||||
| rawNodes?: readonly RawSnapshotNode[]; | ||||||||||||||||||||
| qualityNodes?: readonly RawSnapshotNode[]; | ||||||||||||||||||||
| canonicalQualityNodes?: readonly CanonicalNode[]; | ||||||||||||||||||||
| error?: Readonly<{ code: string; reason: string }>; | ||||||||||||||||||||
| }>; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -71,7 +82,12 @@ export function compareDifferentialCases( | |||||||||||||||||||
| const swift = swiftCases.find((entry) => entry.name === testCase.name); | ||||||||||||||||||||
| const typescript = runTypeScriptCase(testCase); | ||||||||||||||||||||
| const normalizedSwift = swift ? withoutName(swift) : undefined; | ||||||||||||||||||||
| if (!normalizedSwift || JSON.stringify(normalizedSwift) !== JSON.stringify(typescript)) { | ||||||||||||||||||||
| if ( | ||||||||||||||||||||
| !normalizedSwift || | ||||||||||||||||||||
| (testCase.route === 'acquired' | ||||||||||||||||||||
| ? JSON.stringify(normalizedSwift) !== JSON.stringify(typescript) | ||||||||||||||||||||
| : !runnerPresentationAgrees(testCase, swift!, typescript)) | ||||||||||||||||||||
| ) { | ||||||||||||||||||||
| return { case: testCase, swift: normalizedSwift, typescript }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -93,13 +109,16 @@ function runSwiftCases(cases: readonly DifferentialCase[]): DifferentialOutcome[ | |||||||||||||||||||
| name: string; | ||||||||||||||||||||
| outcome: 'success' | 'failure'; | ||||||||||||||||||||
| nodes?: RawSnapshotNode[]; | ||||||||||||||||||||
| qualityNodes?: RawSnapshotNode[]; | ||||||||||||||||||||
| error?: { code: string; reason: string }; | ||||||||||||||||||||
| }>; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| return parsed.cases.map((entry) => ({ | ||||||||||||||||||||
| name: entry.name, | ||||||||||||||||||||
| outcome: entry.outcome, | ||||||||||||||||||||
| nodes: canonicalNodes(entry.nodes ?? []), | ||||||||||||||||||||
| rawNodes: entry.nodes ?? [], | ||||||||||||||||||||
| ...(entry.qualityNodes ? { qualityNodes: entry.qualityNodes } : {}), | ||||||||||||||||||||
| ...(entry.error ? { error: entry.error } : {}), | ||||||||||||||||||||
| })); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -135,7 +154,13 @@ function prepareDifferentialAcquisition(testCase: DifferentialCase): Differentia | |||||||||||||||||||
|
|
||||||||||||||||||||
| /* c8 ignore start */ | ||||||||||||||||||||
| function withoutName(outcome: DifferentialOutcome): Omit<DifferentialOutcome, 'name'> { | ||||||||||||||||||||
| const { name: _name, ...normalized } = outcome; | ||||||||||||||||||||
| const { | ||||||||||||||||||||
| name: _name, | ||||||||||||||||||||
| rawNodes: _rawNodes, | ||||||||||||||||||||
| qualityNodes: _qualityNodes, | ||||||||||||||||||||
| canonicalQualityNodes: _canonicalQualityNodes, | ||||||||||||||||||||
| ...normalized | ||||||||||||||||||||
| } = outcome; | ||||||||||||||||||||
| return normalized; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| /* c8 ignore stop */ | ||||||||||||||||||||
|
|
@@ -162,7 +187,13 @@ export function runTypeScriptCase(testCase: DifferentialCase): DifferentialOutco | |||||||||||||||||||
| const result = presentIosSnapshot({ stage: 'acquired', acquisition }, request, { | ||||||||||||||||||||
| foldPolicy: testCase.foldPolicy, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| return { outcome: 'success', nodes: canonicalNodes(result.nodes) }; | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| outcome: 'success', | ||||||||||||||||||||
| nodes: canonicalNodes(result.nodes), | ||||||||||||||||||||
| ...(testCase.route === 'runner-presented' && result.qualityNodes | ||||||||||||||||||||
| ? { canonicalQualityNodes: canonicalNodes(result.qualityNodes) } | ||||||||||||||||||||
| : {}), | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||
| if (!(error instanceof IosSnapshotEngineError)) throw error; | ||||||||||||||||||||
| return { | ||||||||||||||||||||
|
|
@@ -173,6 +204,161 @@ export function runTypeScriptCase(testCase: DifferentialCase): DifferentialOutco | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function runnerPresentationAgrees( | ||||||||||||||||||||
| testCase: DifferentialCase, | ||||||||||||||||||||
| swift: DifferentialOutcome, | ||||||||||||||||||||
| acquired: DifferentialOutcome, | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| if (swift.outcome !== acquired.outcome) return false; | ||||||||||||||||||||
| if (swift.outcome === 'failure') | ||||||||||||||||||||
| return JSON.stringify(swift.error) === JSON.stringify(acquired.error); | ||||||||||||||||||||
| const { presented, published } = runRunnerComposition(testCase, swift); | ||||||||||||||||||||
| return runnerOutputAgrees(testCase, swift, acquired, presented, published); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function runRunnerComposition( | ||||||||||||||||||||
| testCase: DifferentialCase, | ||||||||||||||||||||
| swift: DifferentialOutcome, | ||||||||||||||||||||
| ): Readonly<{ | ||||||||||||||||||||
| presented: ReturnType<typeof presentIosSnapshot>; | ||||||||||||||||||||
| published: readonly CanonicalNode[]; | ||||||||||||||||||||
| }> { | ||||||||||||||||||||
| const request = createIosSnapshotRequest(testCase); | ||||||||||||||||||||
| const input = { | ||||||||||||||||||||
| stage: 'presented' as const, | ||||||||||||||||||||
| presentation: { | ||||||||||||||||||||
| producer: 'apple-runner' as const, | ||||||||||||||||||||
| intent: 'full' as const, | ||||||||||||||||||||
| payload: { nodes: swift.rawNodes ?? [], truncated: false }, | ||||||||||||||||||||
| ...(swift.qualityNodes | ||||||||||||||||||||
| ? { qualityPayload: { nodes: swift.qualityNodes, truncated: false, scope: null } } | ||||||||||||||||||||
| : {}), | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| validation: { | ||||||||||||||||||||
| presentationKey: buildIosSnapshotPresentationKey(request), | ||||||||||||||||||||
| viewport: { kind: 'reported' as const, rect: testCase.viewport }, | ||||||||||||||||||||
| hittability: { kind: 'available' as const }, | ||||||||||||||||||||
| lineage: { targetId: 'differential-target', generation: 'differential-generation' }, | ||||||||||||||||||||
| residue: [], | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| const presented = presentIosSnapshot(input, request, { foldPolicy: testCase.foldPolicy }); | ||||||||||||||||||||
| const published = canonicalNodes( | ||||||||||||||||||||
| publishIosSnapshot(input, request, { foldPolicy: testCase.foldPolicy }).payload.nodes, | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents |
||||||||||||||||||||
| ); | ||||||||||||||||||||
| return { presented, published }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function runnerOutputAgrees( | ||||||||||||||||||||
| testCase: DifferentialCase, | ||||||||||||||||||||
| swift: DifferentialOutcome, | ||||||||||||||||||||
| acquired: DifferentialOutcome, | ||||||||||||||||||||
| presented: ReturnType<typeof presentIosSnapshot>, | ||||||||||||||||||||
| published: readonly CanonicalNode[], | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| return ( | ||||||||||||||||||||
| qualityMembershipAgrees(testCase, presented.qualityNodes, acquired.canonicalQualityNodes) && | ||||||||||||||||||||
| representativesAreValid(swift.rawNodes ?? [], presented) && | ||||||||||||||||||||
| capturedMembershipAgrees(testCase, swift.rawNodes ?? [], presented, published) && | ||||||||||||||||||||
| semanticMembershipAgrees(published, acquired.nodes) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function capturedMembershipAgrees( | ||||||||||||||||||||
| testCase: DifferentialCase, | ||||||||||||||||||||
| sources: readonly RawSnapshotNode[], | ||||||||||||||||||||
| presented: ReturnType<typeof presentIosSnapshot>, | ||||||||||||||||||||
| published: readonly CanonicalNode[], | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| return ( | ||||||||||||||||||||
| requiredLabelsHaveRepresentatives( | ||||||||||||||||||||
| testCase.requiredLabels ?? [], | ||||||||||||||||||||
| sources, | ||||||||||||||||||||
| presented, | ||||||||||||||||||||
| published, | ||||||||||||||||||||
| ) && | ||||||||||||||||||||
| (testCase.absentLabels ?? []).every( | ||||||||||||||||||||
| (label) => !published.some((node) => node.label === label), | ||||||||||||||||||||
| ) && | ||||||||||||||||||||
| clippedLabelAgrees(testCase.clippedLabel, published) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function semanticMembershipAgrees( | ||||||||||||||||||||
| left: readonly CanonicalNode[], | ||||||||||||||||||||
| right: readonly CanonicalNode[], | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| return JSON.stringify(semanticMembership(left)) === JSON.stringify(semanticMembership(right)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function qualityMembershipAgrees( | ||||||||||||||||||||
| testCase: DifferentialCase, | ||||||||||||||||||||
| actual: readonly RawSnapshotNode[] | undefined, | ||||||||||||||||||||
| expected: readonly CanonicalNode[] | undefined, | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| return ( | ||||||||||||||||||||
| testCase.scope === null || | ||||||||||||||||||||
| JSON.stringify(semanticMembership(canonicalNodes(actual ?? []))) === | ||||||||||||||||||||
| JSON.stringify(semanticMembership(expected ?? [])) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+300
to
+304
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| function representativesAreValid( | ||||||||||||||||||||
| sources: readonly RawSnapshotNode[], | ||||||||||||||||||||
| presentation: ReturnType<typeof presentIosSnapshot>, | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| const outputIndexes = new Set(presentation.nodes.map((node) => node.index)); | ||||||||||||||||||||
| if (sources.some((node) => !presentation.presentedIndexesBySourceIndex.has(node.index))) | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
| return [...presentation.presentedIndexesBySourceIndex.values()].every((indexes) => | ||||||||||||||||||||
| indexes.every((index) => outputIndexes.has(index)), | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function requiredLabelsHaveRepresentatives( | ||||||||||||||||||||
| requiredLabels: readonly string[], | ||||||||||||||||||||
| sources: readonly RawSnapshotNode[], | ||||||||||||||||||||
| presentation: ReturnType<typeof presentIosSnapshot>, | ||||||||||||||||||||
| published: readonly CanonicalNode[], | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| for (const label of requiredLabels) { | ||||||||||||||||||||
| const matchingSources = sources.filter((node) => node.label === label); | ||||||||||||||||||||
| if ( | ||||||||||||||||||||
| matchingSources.length === 0 || | ||||||||||||||||||||
| !matchingSources.some( | ||||||||||||||||||||
| (source) => (presentation.presentedIndexesBySourceIndex.get(source.index) ?? []).length > 0, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
| if (!published.some((node) => node.label === label)) return false; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function clippedLabelAgrees( | ||||||||||||||||||||
| expected: DifferentialCase['clippedLabel'], | ||||||||||||||||||||
| published: readonly CanonicalNode[], | ||||||||||||||||||||
| ): boolean { | ||||||||||||||||||||
| return ( | ||||||||||||||||||||
| !expected || | ||||||||||||||||||||
| published.some( | ||||||||||||||||||||
| (node) => | ||||||||||||||||||||
| node.label === expected.label && | ||||||||||||||||||||
| JSON.stringify(node.rect) === JSON.stringify(expected.rect), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function semanticMembership(nodes: readonly CanonicalNode[]): string[] { | ||||||||||||||||||||
| return [ | ||||||||||||||||||||
| ...new Set( | ||||||||||||||||||||
| nodes | ||||||||||||||||||||
| .filter((node) => node.label !== null) | ||||||||||||||||||||
| .map((node) => JSON.stringify([node.type, node.label])), | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This comparator drops Prompt for AI agents |
||||||||||||||||||||
| ), | ||||||||||||||||||||
| ].sort(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function canonicalNodes(nodes: readonly RawSnapshotNode[]): CanonicalNode[] { | ||||||||||||||||||||
| return nodes.map((node) => ({ | ||||||||||||||||||||
| index: node.index, | ||||||||||||||||||||
|
|
||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The failure arm of
runnerPresentationAgreesreturns after comparing Swift's error to the acquired TypeScript error, beforerunRunnerCompositionever runs. For a runner-presented corpus that fails identically on both languages, thestage: 'presented'adapter — the pipeline this PR is meant to exercise — is never invoked, so an acquired/presented failure-code mismatch (e.g., the presented adapter throwingprojection-mismatchwhile the acquired adapter reports the same typed error) passes silently. Run the presented composition in this arm too and assert its typed error agrees, instead of returning on the error comparison alone.Prompt for AI agents