Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
__pycache__/
*.pyc

# Swift native-review build output
tools/native-review/swift/.build/

# lefthook-generated hook scripts (machine-specific)
.hooks/

Expand Down
8 changes: 8 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -973,3 +973,11 @@ benchmark *ARGS:
# Stop the benchmark Docker stack (state and channels are kept)
benchmark-down:
docker compose --project-name buzz-benchmark down

# Validate macOS native-review tooling and report required OS permissions.
native-review-doctor:
./tools/native-review/bin/review-native doctor

# Run one declarative journey against the isolated local desktop fixture.
native-review-desktop JOURNEY="tools/native-review/desktop/tooltip-fresh-dwell.yaml":
./tools/native-review/bin/review-native run "{{JOURNEY}}"
50 changes: 50 additions & 0 deletions desktop/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Toaster } from "@/shared/ui/sonner";
import { TooltipProvider } from "@/shared/ui/tooltip";
import { recoverLocalStorageQuotaOnStartup } from "@/shared/lib/localStorageQuota";
import { startLocalStorageSweep } from "@/shared/lib/localStorageSweep";
import { installNativeReviewSemanticProbe } from "@/testing/nativeReviewSemanticProbe";

type E2eWindow = Window & {
__BUZZ_E2E__?: unknown;
Expand All @@ -28,6 +29,54 @@ const E2E_DEFAULT_PUBKEY = "deadbeef".repeat(8);
const E2E_COMMUNITY_ID = "e2e-default-community";
const ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX = "buzz-onboarding-complete.v1:";
const DEV_STATE_RESET_PARAM = "resetDevState";
const NATIVE_REVIEW_PARAM = "nativeReview";

function configureNativeReviewFixtureFromUrl() {
const buildEnabled = import.meta.env.VITE_NATIVE_REVIEW === "1";
if (!import.meta.env.DEV && !buildEnabled) return;
const url = new URL(window.location.href);
const enabled =
url.searchParams.get(NATIVE_REVIEW_PARAM) === "1" || buildEnabled;
if (!enabled) return;

const relayUrl =
url.searchParams.get("reviewRelay") ??
import.meta.env.VITE_NATIVE_REVIEW_RELAY;
const pubkey =
url.searchParams.get("reviewPubkey") ??
import.meta.env.VITE_NATIVE_REVIEW_PUBKEY;
if (
!relayUrl ||
!pubkey ||
!/^(ws|http):\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\/?$/.test(
relayUrl,
)
) {
throw new Error(
"native review bootstrap requires a loopback relay and pubkey",
);
}
const communityId = "native-review-local";
const community = {
addedAt: new Date().toISOString(),
id: communityId,
name: "Native Review",
pubkey,
relayUrl,
};
window.localStorage.setItem("buzz-communities", JSON.stringify([community]));
window.localStorage.setItem("buzz-active-community-id", communityId);
window.localStorage.setItem(
`buzz-machine-onboarding-complete.v2:${pubkey}`,
"true",
);
window.localStorage.setItem(`buzz-onboarding-complete.v1:${pubkey}`, "true");
window.localStorage.setItem(
`buzz-community-onboarding-complete.v1:${encodeURIComponent(relayUrl)}:${pubkey}`,
"true",
);
installNativeReviewSemanticProbe();
}

function resetDevWebviewStateFromUrl() {
if (!import.meta.env.DEV) {
Expand Down Expand Up @@ -121,6 +170,7 @@ async function installE2eBridgeIfConfigured() {

async function bootstrap() {
resetDevWebviewStateFromUrl();
configureNativeReviewFixtureFromUrl();
configureDevE2eBridgeFromUrl();
recoverLocalStorageQuotaOnStartup();
startLocalStorageSweep();
Expand Down
108 changes: 108 additions & 0 deletions desktop/src/testing/nativeReviewSemanticProbe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
type SemanticNode = {
id?: string;
role?: string;
name?: string;
enabled: boolean;
focused: boolean;
frame: { x: number; y: number; width: number; height: number };
viewport: { width: number; height: number };
};

const IMPLICIT_ROLES: Partial<Record<string, string>> = {
A: "link",
BUTTON: "button",
INPUT: "text-field",
TEXTAREA: "text-area",
};

function accessibleName(element: HTMLElement): string | undefined {
const labelledBy = element.getAttribute("aria-labelledby");
const labelledText = labelledBy
?.split(/\s+/)
.map((id) => document.getElementById(id)?.textContent?.trim())
.filter(Boolean)
.join(" ");
return (
element.getAttribute("aria-label")?.trim() ||
labelledText ||
element.getAttribute("title")?.trim() ||
(element.getAttribute("role") === "tooltip"
? element.textContent?.trim()
: undefined) ||
undefined
);
}

function snapshot(): SemanticNode[] {
const nodes: SemanticNode[] = [];
for (const candidate of document.querySelectorAll<HTMLElement>(
"[data-testid], [role], button, textarea, input, a[href]",
)) {
const rect = candidate.getBoundingClientRect();
const style = window.getComputedStyle(candidate);
if (
rect.width <= 0 ||
rect.height <= 0 ||
style.display === "none" ||
style.visibility === "hidden"
) {
continue;
}
const id = candidate.dataset.testid;
const role =
candidate.getAttribute("role") ?? IMPLICIT_ROLES[candidate.tagName];
const name = accessibleName(candidate);
if (!id && !role && !name) continue;
nodes.push({
...(id ? { id } : {}),
...(role ? { role } : {}),
...(name ? { name } : {}),
enabled:
!candidate.hasAttribute("disabled") &&
candidate.getAttribute("aria-disabled") !== "true",
focused: candidate === document.activeElement,
frame: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
});
}
return nodes;
}

export function installNativeReviewSemanticProbe(): void {
let scheduled = false;
const publish = () => {
scheduled = false;
const payload = JSON.stringify(snapshot());
if (
!navigator.sendBeacon(
import.meta.env.VITE_NATIVE_REVIEW_PROBE_URL,
payload,
)
) {
console.error("native review semantic probe beacon was rejected");
}
};
const schedule = () => {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(publish);
};
new MutationObserver(schedule).observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
});
window.addEventListener("focusin", schedule);
window.addEventListener("focusout", schedule);
window.addEventListener("resize", schedule);
window.addEventListener("scroll", schedule, true);
schedule();
}
16 changes: 16 additions & 0 deletions scripts/setup-desktop-test-data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ BOB_PUBKEY="bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
CHARLIE_PUBKEY="554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
TYLER_PUBKEY="e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
AGENT_PUBKEY="db0b028cd36f4d3e36c8300cce87252c1f7fc9495ffecc53f393fcac341ffd36"
REVIEW_PUBKEY="${BUZZ_REVIEW_PUBKEY:-}"
if [[ -n "$REVIEW_PUBKEY" && ! "$REVIEW_PUBKEY" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "BUZZ_REVIEW_PUBKEY must be exactly 64 hexadecimal characters." >&2
exit 1
fi

if command -v psql >/dev/null 2>&1; then
run_psql() { PGPASSWORD="$DB_PASS" psql -h"$DB_HOST" -p"$DB_PORT" -U"$DB_USER" -d"$DB_NAME" -qtA "$@"; }
Expand Down Expand Up @@ -129,4 +134,15 @@ ON CONFLICT DO NOTHING
;
"

if [[ -n "$REVIEW_PUBKEY" ]]; then
run_sql "
INSERT INTO channel_members
(community_id, channel_id, pubkey, role, invited_by)
VALUES
('${COMMUNITY_ID}', '${UUID_GENERAL}', decode('${REVIEW_PUBKEY}','hex'), 'member', decode('${SYSTEM_PUBKEY}','hex'))
ON CONFLICT DO NOTHING
;
"
fi

echo "Desktop e2e data ready."
43 changes: 43 additions & 0 deletions tools/native-review/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Buzz native review harness

This macOS-only MVP drives the real Tauri/WKWebView app with Accessibility and
CGEvent, captures its window with Core Graphics and AVFoundation, and writes an
exact-SHA run receipt. It is a targeted review lane, not a replacement for
`just ci` or Playwright.

## Safety contract

- Only loopback `ws://`/`http://` relays are accepted.
- Every run gets an ephemeral Nostr key, run-specific dev bundle ID, keyring
service, HOME, WebKit/app-data state, and artifact directory.
- The launcher environment is allowlisted before the ephemeral key is added;
inherited tokens and production keys never enter the reviewed process.
- Production bundle IDs, keyring services, and remote relays fail closed.
- This protects reviewer state from accidents. It is **not** containment for
hostile code; use a dedicated macOS user or disposable VM for untrusted PRs.

## Commands

```bash
just native-review-doctor
just native-review-desktop tools/native-review/desktop/tooltip-fresh-dwell.yaml
python3 -m unittest discover -s tools/native-review/tests -p 'test_*.py'
```

The desktop command expects the isolated `buzz-harness` relay on port 3030
(`scripts/start-isolated-test-relay.sh`). Doctor reports Accessibility and
Screen Recording separately and the run refuses to proceed unless both are
already granted to the invoking terminal/agent.

Runs are written under
`test-results/native-review/<sha>/<flow>/<run-id>/`. A failed locator,
postcondition, recording, evidence capture, or cleanup produces a failed partial
receipt. `tests/fixtures/broken-tooltip.yaml` is the deliberate fail-loud
mutation.

## MVP limits

Only macOS desktop, the local review-channel fixture, role/name/identifier AX
locators, click/hover/key/wait actions, window screenshots/video, and step AX
snapshots are implemented. iOS Simulator and repeated base/head performance
comparison remain later phases.
8 changes: 8 additions & 0 deletions tools/native-review/bin/review-native
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# The repository pins Rust/Node tooling through Hermit; native review must build
# with the same toolchain as the artifact it records.
source "$REPO_ROOT/bin/activate-hermit"
exec python3 "$SCRIPT_DIR/review_native.py" "$@"
61 changes: 61 additions & 0 deletions tools/native-review/desktop/tooltip-fresh-dwell.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
schema_version: 1
flow: tooltip_fresh_dwell
platforms: [macos]
fixture: local_review_channel
record:
video: window
screenshots: false
accessibility: false
steps:
- name: activate_buzz
act: {type: activate}
expect:
exists: {role: window}
timeout_ms: 15000
- name: await_interactive_app
act: {type: wait, duration_ms: 100}
expect:
not_exists: {id: app-loading-gate}
timeout_ms: 60000
- name: reach_seeded_channel
locate:
- {id: channel-welcome-everyone}
- {role: button, name: welcome-everyone}
act: {type: click}
expect:
exists: {id: message-insert-mention}
timeout_ms: 15000
- name: settle_pointer_before_fresh_dwell
locate:
- {id: message-composer}
- {role: text-area, name: Message}
act: {type: move_pointer, duration_ms: 80}
expect:
not_exists: {role: tooltip, name: "Mention someone"}
- name: clear_tooltip_skip_delay
act: {type: wait, duration_ms: 800}
expect:
not_exists: {role: tooltip, name: "Mention someone"}
- name: transit_over_mention
locate:
- {id: message-insert-mention}
- {role: button, name: "Mention someone"}
act: {type: move_pointer}
expect: {enabled: true}
- name: complete_dwell
act: {type: wait, duration_ms: 550}
expect:
exists: {role: tooltip, name: "Mention someone"}
timeout_ms: 1000
measure: tooltip_open_latency
- name: leave_trigger
locate:
- {id: message-composer}
- {role: text-area, name: Message}
act: {type: move_pointer, duration_ms: 80}
expect:
not_exists: {role: tooltip, name: "Mention someone"}
timeout_ms: 1000
cleanup:
terminate_app: true
remove_state: true
Loading