import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { observeNativeUi } from "./NativeUiObservation.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; const target: BinaryTarget = { path: "/fixture/App", sha256: "a".repeat(64), kind: "executable", format: "mach-o", architecture: "arm64", availableArchitectures: ["arm64"], }; const scope = { pid: 123, window_id: 456, screenshot: false, }; const snapshot = { window: { pid: 123, window_id: 456, executable: target.path, launch_time: 1, title: "Fixture", }, nodes: [], screenshot: null, gaps: [], truncated: false, }; const validPng = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jv4sAAAAASUVORK5CYII=", "base64", ); /** Screenshot payload as the native helper reports one PNG buffer. */ const screenshotOf = (bytes: Buffer) => ({ mime_type: "image/png", base64: bytes.toString("base64"), sha256: createHash("sha256").update(bytes).digest("hex"), width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20), }); /** Observe the fixture snapshot carrying one screenshot payload. */ const observeWithScreenshot = (screenshot: Record) => observeNativeUi( target, "observe_native_ui", { ...scope, screenshot: true }, { environment: {}, invoke: async () => ({ ok: true, result: { ...snapshot, screenshot } }), }, ); it.each([ { offset: 8, value: 12 }, { offset: 16, value: 0x8000_0000 }, { offset: 20, value: 0x8000_0000 }, ])( "rejects invalid IHDR metadata with a matching digest", async ({ offset, value }) => { const bytes = Buffer.from(validPng); bytes.writeUInt32BE(value, offset); const result = await observeWithScreenshot(screenshotOf(bytes)); expect(result.ok).toBe(false); }, ); describe("native UI screenshot validation", () => { it("rejects malformed or mismatched screenshot bytes from the helper", async () => { const result = await observeWithScreenshot({ ...screenshotOf(validPng), sha256: "0".repeat(64), }); expect(result.ok).toBe(false); }); it.each([ { base64: "%%%=" }, { base64: Buffer.from("not a PNG").toString("base64") }, { width: 2 }, ])( "rejects invalid screenshot field variants from the helper", async (change) => { const result = await observeWithScreenshot({ ...screenshotOf(validPng), ...change, }); expect(result.ok).toBe(false); }, ); it("rejects a PNG whose IHDR type only matches after ASCII folding", async () => { const bytes = Buffer.from(validPng); const type = bytes.subarray(12, 16); type[0] = (type[0] ?? 0) | 0x80; expect(type.toString("ascii")).toBe("IHDR"); const result = await observeWithScreenshot(screenshotOf(bytes)); expect(result.ok).toBe(false); }); it("accepts a self-consistent PNG screenshot from the helper", async () => { const result = await observeWithScreenshot(screenshotOf(validPng)); expect(result.ok).toBe(true); }); it("preserves an unknown AX child count and its truncation gap", async () => { const partial = { ...snapshot, nodes: [ { path: [], role: "AXWindow", title: "Fixture", value: null, actions: [], children_count: null, }, ], truncated: true, gaps: ["AX child count unavailable at path []: AXError -25204"], }; const result = await observeNativeUi( target, "observe_native_ui", { ...scope, accessibility: true }, { environment: {}, invoke: async () => ({ ok: true, result: partial }) }, ); expect(result.ok).toBe(true); if (result.ok) expect(result.value.initial).toMatchObject({ truncated: true, gaps: ["AX child count unavailable at path []: AXError -25204"], nodes: [{ children_count: null }], }); }); }); describe("native UI capture selection and budgets", () => { it("accepts observations and scenarios without compatibility flags", async () => { let calls = 0; const invoke = async () => { calls++; return { ok: true, result: snapshot }; }; const observed = await observeNativeUi( target, "observe_native_ui", { pid: 123, window_id: 456, screenshot: false, accessibility: true }, { environment: {}, invoke }, ); expect(observed.ok).toBe(true); const captured = await observeNativeUi( target, "capture_native_ui_scenario", { pid: 123, window_id: 456, screenshot: false, accessibility: true, steps: [{ kind: "click", path: [] }], }, { environment: {}, invoke }, ); expect(captured.ok).toBe(true); expect(calls).toBe(3); }); it("returns caller-selected large observations and complete action lists", async () => { const nodeLimits: unknown[] = []; const steps = Array.from({ length: 17 }, () => ({ kind: "wait" as const, milliseconds: 0, })); const result = await observeNativeUi( target, "capture_native_ui_scenario", { pid: 123, window_id: 456, screenshot: false, accessibility: true, max_nodes: 2_001, steps, }, { environment: {}, invoke: async (parameters) => { nodeLimits.push(parameters.max_nodes); return { ok: true, result: snapshot }; }, }, ); expect(result.ok).toBe(true); expect(nodeLimits).toEqual(Array.from({ length: 18 }, () => 2_001)); if (result.ok) { expect(result.value.steps).toHaveLength(17); expect( result.value.steps.every(({ outcome }) => outcome === "completed"), ).toBe(true); } }); it("accepts caller-selected waits beyond the old aggregate cutoff", async () => { const controller = new AbortController(); const result = await observeNativeUi( target, "capture_native_ui_scenario", { pid: 123, window_id: 456, screenshot: false, accessibility: true, steps: Array.from({ length: 4 }, () => ({ kind: "wait" as const, milliseconds: 10_001, })), }, { environment: {}, signal: controller.signal, invoke: async () => { controller.abort(); return { ok: true, result: snapshot }; }, }, ); expect(result.ok).toBe(true); if (result.ok) expect(result.value.steps[0]).toMatchObject({ kind: "wait", outcome: "cancelled", }); }); it("reports aggregate output exhaustion after individually valid captures", async () => { const largeSnapshot = { ...snapshot, window: { ...snapshot.window, title: "x".repeat(17 * 1024 * 1024) }, }; const result = await observeNativeUi( target, "capture_native_ui_scenario", { ...scope, steps: [{ kind: "wait", milliseconds: 0 }], }, { environment: {}, invoke: async () => ({ ok: true, result: largeSnapshot }), }, ); expect(result.ok).toBe(true); if (result.ok) expect(result.value.steps[0]).toMatchObject({ outcome: "failed", reason: expect.stringContaining("64 MiB output budget"), }); }); }); describe("native UI target and cancellation failures", () => { it.each([ "accessibility-denied", "screen-recording-denied", "ambiguous-window", ])("preserves actionable %s without fallback", async (code) => { const result = await observeNativeUi(target, "observe_native_ui", scope, { environment: {}, invoke: async () => ({ ok: false, code, message: "Grant permission or select an unambiguous window", }), }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error.message).toContain(code); }); it("preserves the underlying helper failure reason in diagnostics", async () => { const result = await observeNativeUi(target, "observe_native_ui", scope, { environment: {}, invoke: async () => { throw new Error( "Native helper returned invalid JSON: Unexpected token", ); }, }); expect(result.ok).toBe(false); if (result.ok) return; expect(result.error).toMatchObject({ diagnostics: { reason: "Native helper returned invalid JSON: Unexpected token", remediation: "Native helper failed, timed out, or returned malformed capture data; install compatible Xcode command-line tools and inspect local OS permissions", }, }); }); it("stops on a failed action and returns ordered before/capture-gap evidence", async () => { const calls: Readonly>[] = []; const result = await observeNativeUi( target, "capture_native_ui_scenario", { ...scope, steps: [ { kind: "scroll", path: [1], direction: "increment" }, { kind: "click", path: [2] }, ], }, { environment: {}, invoke: async (parameters) => { calls.push(parameters); return calls.length === 1 ? { ok: true, result: snapshot } : { ok: false, code: "action-failed", message: "AXIncrement unsupported", }; }, }, ); expect(calls).toHaveLength(2); expect(calls[1]).toMatchObject({ launch_time: 1, action: { kind: "scroll", path: [1] }, }); expect(result.ok).toBe(true); if (result.ok) expect(result.value.steps).toMatchObject([ { index: 0, outcome: "failed", before: snapshot, after: null }, ]); }); it("rejects target replacement and stops subsequent actions after cancellation", async () => { const controller = new AbortController(); let calls = 0; const result = await observeNativeUi( target, "capture_native_ui_scenario", { ...scope, steps: [ { kind: "wait", milliseconds: 1000 }, { kind: "click", path: [] }, ], }, { environment: {}, signal: controller.signal, invoke: async () => { calls++; controller.abort(); return { ok: true, result: snapshot }; }, }, ); expect(calls).toBe(1); expect(result.ok).toBe(true); if (result.ok) expect(result.value.steps[0]?.outcome).toBe("cancelled"); const replaced = await observeNativeUi(target, "observe_native_ui", scope, { environment: {}, invoke: async () => ({ ok: true, result: { ...snapshot, window: { ...snapshot.window, pid: 999 } }, }), }); expect(replaced.ok).toBe(false); }); });