Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
);
name = AgentDeviceRunnerUITests;
packageProductDependencies = (
A1B2C3D4E5F60718293A4B5D /* AgentDeviceSnapshotPresentation */,
);
productName = AgentDeviceRunnerUITests;
productReference = 20EA2EDD2F2CFC7C001CF0EF /* AgentDeviceRunnerUITests.xctest */;
Expand Down Expand Up @@ -146,6 +147,9 @@
);
mainGroup = 20EA2EBD2F2CFC7B001CF0EF;
minimizedProjectReferenceProxies = 1;
packageReferences = (
A1B2C3D4E5F60718293A4B5C /* XCLocalSwiftPackageReference "snapshot-presentation" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 20EA2EC72F2CFC7B001CF0EF /* Products */;
projectDirPath = "";
Expand All @@ -157,6 +161,21 @@
};
/* End PBXProject section */

/* Begin XCLocalSwiftPackageReference section */
A1B2C3D4E5F60718293A4B5C /* XCLocalSwiftPackageReference "snapshot-presentation" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../../snapshot-presentation;
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
A1B2C3D4E5F60718293A4B5D /* AgentDeviceSnapshotPresentation */ = {
isa = XCSwiftPackageProductDependency;
package = A1B2C3D4E5F60718293A4B5C /* XCLocalSwiftPackageReference "snapshot-presentation" */;
productName = AgentDeviceSnapshotPresentation;
};
/* End XCSwiftPackageProductDependency section */

/* Begin PBXResourcesBuildPhase section */
20EA2EC42F2CFC7B001CF0EF /* Resources */ = {
isa = PBXResourcesBuildPhase;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
import Foundation

enum SnapshotScopeSelection: Equatable {
case unscoped
case matched(Int)
case missing
}

/// Cross-runtime snapshot scope specification.
///
/// A non-empty scope selects the first node in presentation preorder whose label, identifier, or
/// value contains the trimmed query case-insensitively and whose subtree contributes to the
/// requested projection. Missing matches publish an empty projection.
enum SnapshotScopePolicy {
static func select<Node>(
fromPreorder nodes: [Node],
scope: String?,
depth: (Node) -> Int,
semanticValues: (Node) -> [String?],
subtreeContributes: (Range<Int>) -> Bool
) -> SnapshotScopeSelection {
guard let query = normalized(scope) else { return .unscoped }
for (index, node) in nodes.enumerated() {
guard semanticValues(node).contains(where: { value in
value?.lowercased().contains(query) == true
}) else { continue }
let range = subtreeRange(from: index, in: nodes, depth: depth)
if subtreeContributes(range) {
return .matched(index)
}
}
return .missing
}

static func isActive(_ scope: String?) -> Bool {
normalized(scope) != nil
}

private static func normalized(_ scope: String?) -> String? {
guard let query = scope?.trimmingCharacters(in: .whitespacesAndNewlines), !query.isEmpty else {
return nil
}
return query.lowercased()
}

static func subtreeRange<Node>(
from start: Int,
in nodes: [Node],
depth: (Node) -> Int
) -> Range<Int> {
let rootDepth = depth(nodes[start])
var end = start + 1
while end < nodes.count, depth(nodes[end]) > rootDepth {
end += 1
}
return start..<end
}
}
import AgentDeviceSnapshotPresentation

#if AGENT_DEVICE_RUNNER_UNIT_TESTS
private struct SnapshotScopeFixture: Decodable {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import AgentDeviceSnapshotPresentation

extension RunnerTests {
private static let privateAXSnapshotMaxNodes = 5_000
Expand Down Expand Up @@ -769,7 +770,7 @@ extension RunnerTests {
options: PresentationOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
policy: .cursorProjected
)
let labels = (capture.payload.nodes ?? []).compactMap { $0.label }
let labels = capture.nodes.compactMap { $0.label }
XCTAssertEqual(
labels,
["Blue Sky", "Callstack", "Welcome back", "Email", "Password", "Sign in", "Forgot password?"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import AgentDeviceSnapshotPresentation

#if AGENT_DEVICE_RUNNER_UNIT_TESTS && os(iOS)
import ObjectiveC.runtime
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import XCTest
import AgentDeviceSnapshotPresentation

enum RunnerCommandLifecycleState: String {
case notAccepted
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import AgentDeviceSnapshotPresentation

#if os(macOS)
import CoreGraphics
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import AgentDeviceSnapshotPresentation

// MARK: - Wire Models

enum CommandType: String, Codable {
Expand Down Expand Up @@ -277,24 +279,3 @@ struct ErrorPayload: Codable {
let message: String
var hint: String?
}

struct SnapshotRect: Codable {
let x: Double
let y: Double
let width: Double
let height: Double
}

struct PresentationOptions {
let interactiveOnly: Bool
let depth: Int?
let scope: String?
let raw: Bool
/// Internal daemon ask: capture with this backend first regardless of channel
/// health ("private-ax"). Same-backend evidence probes (tap-outcome
/// corroboration) must be captured the way their baseline was.
var preferredBackend: String? = nil
/// Read UIAccessibilityCustomActions for merged leaves. Opt-in: each element
/// costs its own AX round trip (see RunnerAXSnapshotBridge).
var customActions: Bool = false
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import AgentDeviceSnapshotPresentation

/// Reported facts for one private-AX element, read once so every consumer describes a node the
/// same way.
Expand Down Expand Up @@ -143,7 +144,7 @@ extension RunnerTests {
options: PresentationOptions(
interactiveOnly: interactiveOnly, depth: nil, scope: nil, raw: false),
policy: .cursorProjected
).payload.nodes ?? []
).nodes
}

func testPrivateAXRegularPresentationProjectsToViewportAndKeepsScrollHint() throws {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import AgentDeviceSnapshotPresentation

extension RunnerTests {
private static let axSnapshotErrorCode = "IOS_AX_SNAPSHOT_FAILED"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import AgentDeviceSnapshotPresentation

// MARK: - Snapshot capture plans (ADR 0004)
//
Expand Down Expand Up @@ -30,21 +31,6 @@ struct SnapshotQuality: Codable {
var timing: SnapshotCaptureTiming? = nil
}

/// How much of the merged-element set the custom-action pass actually read. An
/// unread element renders exactly like one with no actions, so a partial pass
/// has to say so — otherwise absence reads as proof of absence.
struct SnapshotCustomActionCoverage: Codable {
let read: Int
let candidates: Int
/// Elements whose action list was clipped by the per-element output caps. A
/// clipped list looks complete, so it is disclosed on the same principle as
/// an unread element.
let truncated: Int
/// The pass stopped early because an earlier read is still hung. Distinct from
/// a budget stop: the remedy is waiting, not scrolling.
let blocked: Bool
}

enum SnapshotXCTestChannelPlanState: Equatable {
case normal
case deferredToIndependentBackend
Expand Down Expand Up @@ -72,19 +58,26 @@ enum SnapshotCaptureTerminalPolicy {

struct SnapshotBackendCapture {
let payload: DataPayload
/// Set by the private AX backend when the ladder accepted a shallower depth than requested.
let effectiveDepth: Int?
/// Set by the private AX backend when the capture asked for custom actions.
var customActions: SnapshotCustomActionCoverage? = nil
/// Broad presentation used only by the quality classifier when a scope narrows publication.
/// A legitimate missing scope is an empty healthy projection, not backend failure evidence.
var qualityPayload: DataPayload? = nil
/// Set by the capture plan after measuring acquisition and presentation separately. Direct
/// presentation fixtures do not claim a plan timing.
var timing: SnapshotCaptureTiming? = nil
}

extension RunnerTests {
static func makeSnapshotBackendCapture(
from result: AgentDeviceSnapshotPresentation.SnapshotPresentationResult
) -> SnapshotBackendCapture {
SnapshotBackendCapture(
payload: DataPayload(nodes: result.nodes, truncated: result.truncated),
effectiveDepth: result.effectiveDepth,
customActions: result.customActions,
qualityPayload: result.qualityNodes.map {
DataPayload(nodes: $0, truncated: result.truncated)
}
)
}

static let sparseRecoveryTruncatedNodeThreshold = 8
/// Umbrella wall-clock budget for one capture plan. Individual backends bound themselves,
/// but chained recovery tiers must never stack past the 30s main-thread watchdog: when the
Expand Down Expand Up @@ -496,14 +489,14 @@ extension RunnerTests {
let presented: SnapshotBackendCapture
do {
presented = try timer.measure(.presentation) {
guard let capture = try SnapshotPresentation.present(acquisition, options: options) else {
guard let result = try SnapshotPresentation.present(acquisition, options: options) else {
throw Self.snapshotProjectionMismatchFailure(
kind,
requested: hint.projection,
acquired: acquisition.hint.projection
)
}
return capture
return Self.makeSnapshotBackendCapture(from: result)
}
} catch let failure as SnapshotPresentationFailure {
return SnapshotBackendAttempt(
Expand Down Expand Up @@ -891,7 +884,7 @@ extension RunnerTests {
scope: nil,
raw: true
)
let capture = SnapshotPresentation.presentRaw(
let result = SnapshotPresentation.presentRaw(
SnapshotAcquisition(
hint: SnapshotPresentation.captureHint(for: options),
nodes: [],
Expand All @@ -901,6 +894,7 @@ extension RunnerTests {
),
options: options
)
let capture = Self.makeSnapshotBackendCapture(from: result)

let payload = stampedSnapshotPayload(
capture,
Expand Down
Loading
Loading