mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
test(mobile): add iOS workspace parity
This commit is contained in:
@@ -2463,4 +2463,6 @@ copy.
|
||||
| 2026-07-28 | Complete | RNW package `9e5e807523e8b917fef68f221cc1fd2e1a16dbe07d7077e717238eed17003b52` verifies with 49 assets, 9,330,604 raw bytes, and 2,697,919 gzip bytes. The mobile suite passes 553 files / 3,297 tests with 2 expected skips; all typechecks and repository quality gates pass. This slice's root suite hit an unrelated timeout that passed in isolation, so the prior complete root-suite checkpoint remains authoritative. |
|
||||
| 2026-07-28 | Complete | The unchanged Accounts screen passes iPhone 17 Pro native-versus-hosted parity at 0.050% changed pixels / 0.099 mean channel difference / 0.000544 vertical-title delta against 3% / 4 / 0.005 budgets. The cached-app journey then passes Tasks, Session, Files/Preview, Agent History, Desktop restart/recovery, Source Control, Review, and both isolation probes. The unlabeled toolbar icon remains an open VoiceOver finding. |
|
||||
| 2026-07-28 | Complete | Post-Accounts validation passes 554 mobile files / 3,299 tests with 2 expected skips, mobile and RNW typechecks, mobile lint, full mobile formatting, max-lines, and diff hygiene. The RNW package remains unchanged because this slice adds only the native-versus-hosted driver and evidence. |
|
||||
| 2026-07-28 | Complete | The unchanged base workspace `HostScreen` passes iPhone 17 Pro native-versus-hosted parity at 0.879% changed pixels / 1.876 mean channel difference / 0.000395 vertical landmark delta against 3% / 4 / 0.005 budgets. The complete cached-app journey captures Workspace before Accounts, then passes every existing downstream parity, recovery, review, and isolation checkpoint. |
|
||||
| 2026-07-28 | Complete | Post-Workspace validation passes 555 mobile files / 3,301 tests with 2 expected skips, mobile and RNW typechecks, mobile lint, full mobile formatting, max-lines, and diff hygiene. The RNW package remains unchanged because this slice adds only deterministic parity infrastructure and evidence. |
|
||||
| 2026-07-28 | Next | Complete the remaining parity inventory and cutover cleanup, then execute the physical-device, topology, security, performance, packaged-release, and App Store gates. |
|
||||
|
||||
@@ -62,7 +62,7 @@ but is a component, not a route. It migrates with the session UI.
|
||||
| `mobile/app/about.tsx` | Native application identity/version | Native shell | Keep; add shell and active web build versions where useful |
|
||||
| `mobile/app/h/_layout.tsx` | Host protocol gate and host route stack | Native shell | Keep as host security/recovery boundary; mount one production hybrid workspace route |
|
||||
| `mobile/app/h/[hostId]/edit.tsx` | Paired host display name, endpoint, reconnect | Native shell | Keep because it changes paired connectivity rather than workspace content |
|
||||
| `mobile/app/h/[hostId]/index.tsx` | Worktree list, creation, actions, host workspace entry | Mobile web app | Reuse `HostScreen` presentation unchanged through a web route/transport adapter |
|
||||
| `mobile/app/h/[hostId]/index.tsx` | Worktree list, creation, actions, host workspace entry | Mobile web app | Complete on iOS Simulator: native and hosted mount the same `HostScreen` and pass strict screenshot parity |
|
||||
| `mobile/app/h/[hostId]/accounts.tsx` | Host agent-account usage and selection | Mobile web app | Complete on iOS Simulator: the same screen uses typed native/web host-account adapters and passes strict screenshot parity |
|
||||
| `mobile/app/h/[hostId]/tasks.tsx` | Host task providers, task details, mutations, workspace creation | Mobile web app | Complete on iOS Simulator: same route/presentation with strict native/web operations |
|
||||
| `mobile/app/h/[hostId]/session/[worktreeId].tsx` | Sessions, tabs, terminal, browser, native chat, files, attachments, dictation | Mobile web app | Reuse current session presentation; native-chat/agent-state slice is adapter-complete and awaiting live parity evidence |
|
||||
@@ -502,6 +502,12 @@ existing unlabeled non-embedded toolbar icon position; its missing
|
||||
accessibility label remains part of the open VoiceOver review rather than a
|
||||
presentation change in this migration.
|
||||
|
||||
The fixture now starts by capturing the unchanged base workspace screen in
|
||||
native and hosted modes. Workspace measures 0.879% changed pixels / 1.876 mean
|
||||
channel difference / 0.000395 vertical landmark delta, within the 3% / 4 /
|
||||
0.005 budgets. This proves the shared `HostScreen` itself before the same
|
||||
fixture navigates into Accounts and the downstream route matrix.
|
||||
|
||||
The interrupted-transcript versus hook-status mismatch and a real structured
|
||||
prompt response pass Host 37 Simulator replay. Current package `4b7df7d4…`
|
||||
also carries the network-denied local Mermaid engine and its WebKit-compatible
|
||||
|
||||
@@ -1866,6 +1866,13 @@ changed pixels, 0.099 mean channel difference, and 0.000544 vertical-title
|
||||
delta against the 3% / 4 / 0.005 budgets. This extends source-sharing evidence;
|
||||
it does not replace or restyle the account presentation.
|
||||
|
||||
The base host-workspace route now has equivalent evidence. The exact-app gate
|
||||
captures the unchanged shared `HostScreen` before any feature navigation and
|
||||
passes at 0.879% changed pixels, 1.876 mean channel difference, and 0.000395
|
||||
vertical landmark delta. The route remains the existing worktree list,
|
||||
creation, action, and host-entry presentation rendered through its native/web
|
||||
operations boundary.
|
||||
|
||||
The host-only router now imports the existing
|
||||
`mobile/app/h/[hostId]/session/[worktreeId].tsx` route directly; a
|
||||
source-identity test prevents that wrapper from growing copied presentation.
|
||||
|
||||
@@ -73,6 +73,12 @@ gap for the broader VoiceOver review. The complete cached-app journey passes
|
||||
with Accounts inserted before Tasks, Session, Files/Preview, Agent History,
|
||||
Desktop restart/recovery, Source Control, Review, and both isolation probes.
|
||||
|
||||
The base workspace screen now has the same deterministic proof. Native and
|
||||
hosted mount the unchanged `HostScreen` and pass at 0.879% changed pixels,
|
||||
1.876 mean channel difference, and 0.000395 vertical landmark delta against the
|
||||
3% / 4 / 0.005 budgets. The complete journey captures this screen before
|
||||
Accounts and the rest of the route matrix.
|
||||
|
||||
The migration is rebased onto `origin/main` at `0404f27b3`. Current post-rebase
|
||||
validation passes 552 mobile files / 3,291 tests with 2 expected skips and
|
||||
3,770 root files / 39,212 tests with 62 expected skips. All project typechecks,
|
||||
@@ -81,7 +87,7 @@ formatting, localization, and the max-lines ratchet pass.
|
||||
The independently verified React Native Web package is
|
||||
`9e5e807523e8b917fef68f221cc1fd2e1a16dbe07d7077e717238eed17003b52`:
|
||||
49 assets, 9,330,604 raw bytes, and 2,697,919 gzip bytes. The current mobile
|
||||
suite passes 554 files / 3,299 tests with 2 expected skips; all project
|
||||
suite passes 555 files / 3,301 tests with 2 expected skips; all project
|
||||
typechecks and repository-wide quality gates pass. A fresh root-suite attempt
|
||||
hit an unrelated 30-second timeout in
|
||||
`project-view-wrapper-source-context-boundary.test.ts` and was interrupted
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { dismissEmulatorDeveloperMenuIfPresent } from './emulator-developer-menu-dismissal.mjs'
|
||||
import { waitForHostedIosAccessibilityControl } from './hosted-ios-emulator-accessibility.mjs'
|
||||
import { assertHostedIosScreenshotParity } from './hosted-ios-screenshot-parity.mjs'
|
||||
import { readHostedWebViewTextPoint } from './hosted-webview-cdp-session.mjs'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
export async function captureNativeWorkspaceBaseline({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
timeoutMs
|
||||
}) {
|
||||
await dismissEmulatorDeveloperMenuIfPresent(emulator)
|
||||
const filterPoint = await waitForHostedIosAccessibilityControl(emulator, 'Filter', timeoutMs)
|
||||
await delay(500)
|
||||
const screenshot = path.join(runtimeDirectory, 'native-workspace-portrait.png')
|
||||
await captureSimulatorScreenshot(deviceUdid, screenshot)
|
||||
return { filterPoint, screenshot }
|
||||
}
|
||||
|
||||
export async function captureHostedWorkspaceParity({
|
||||
deviceUdid,
|
||||
document,
|
||||
nativeBaseline,
|
||||
runtimeDirectory,
|
||||
timeoutMs
|
||||
}) {
|
||||
const filterPoint = await readHostedWebViewTextPoint(document, 'Filter')
|
||||
const screenshot = path.join(runtimeDirectory, 'hosted-workspace-portrait.png')
|
||||
const deadline = Date.now() + timeoutMs
|
||||
let lastError = new Error('Workspace did not reach screenshot parity')
|
||||
while (Date.now() < deadline) {
|
||||
await delay(500)
|
||||
await captureSimulatorScreenshot(deviceUdid, screenshot)
|
||||
try {
|
||||
const screenshotParity = await assertHostedIosScreenshotParity({
|
||||
hostedLandmark: filterPoint,
|
||||
hostedScreenshot: screenshot,
|
||||
nativeLandmark: nativeBaseline.filterPoint,
|
||||
nativeScreenshot: nativeBaseline.screenshot
|
||||
})
|
||||
return workspaceParityEvidence(nativeBaseline, {
|
||||
filterPoint,
|
||||
screenshot,
|
||||
screenshotParity
|
||||
})
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
}
|
||||
throw lastError
|
||||
}
|
||||
|
||||
export function workspaceParityEvidence(nativeCapture, hostedCapture) {
|
||||
return {
|
||||
nativeScreenshot: path.basename(nativeCapture.screenshot),
|
||||
hostedScreenshot: path.basename(hostedCapture.screenshot),
|
||||
nativeFilterPoint: nativeCapture.filterPoint,
|
||||
hostedFilterPoint: hostedCapture.filterPoint,
|
||||
screenshotParity: hostedCapture.screenshotParity
|
||||
}
|
||||
}
|
||||
|
||||
async function captureSimulatorScreenshot(deviceUdid, outputPath) {
|
||||
await execFileAsync('xcrun', ['simctl', 'io', deviceUdid, 'screenshot', outputPath])
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
captureHostedFilesPreviewParity,
|
||||
captureNativeFilesPreviewBaselines
|
||||
} from './hosted-ios-files-preview-parity.mjs'
|
||||
import {
|
||||
captureHostedWorkspaceParity,
|
||||
captureNativeWorkspaceBaseline
|
||||
} from './hosted-ios-workspace-parity.mjs'
|
||||
import { verifyHostedAgentHistoryJourney } from './hosted-ios-agent-history-journey.mjs'
|
||||
import { openHostedIosHybridRoute } from './hosted-ios-hybrid-route-handoff.mjs'
|
||||
import { verifyHostedNativeTerminalSettingsHandoff } from './hosted-ios-native-settings-handoff.mjs'
|
||||
@@ -107,6 +111,20 @@ async function main() {
|
||||
const nativeOnboarding = await evidenceStep('native onboarding', () =>
|
||||
completeHostedIosNativeOnboarding(emulator, expectedWorkspace, options.timeoutMs)
|
||||
)
|
||||
const nativeWorkspace =
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
options.nativeSettingsOnly ||
|
||||
options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('native workspace baseline', () =>
|
||||
captureNativeWorkspaceBaseline({
|
||||
deviceUdid,
|
||||
emulator,
|
||||
runtimeDirectory,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
)
|
||||
const nativeAccounts =
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
@@ -179,6 +197,21 @@ async function main() {
|
||||
expectedText: 'Orca Desktop',
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
const hostedWorkspace =
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
options.nativeSettingsOnly ||
|
||||
options.sourceControlOnly
|
||||
? null
|
||||
: await evidenceStep('hosted workspace parity', () =>
|
||||
captureHostedWorkspaceParity({
|
||||
deviceUdid,
|
||||
document: workspaceDocument,
|
||||
nativeBaseline: nativeWorkspace,
|
||||
runtimeDirectory,
|
||||
timeoutMs: options.timeoutMs
|
||||
})
|
||||
)
|
||||
const hostedAccounts =
|
||||
options.securityOnly ||
|
||||
options.filesPreviewOnly ||
|
||||
@@ -340,6 +373,7 @@ async function main() {
|
||||
networkIsolation,
|
||||
navigationIsolation,
|
||||
nativeOnboarding,
|
||||
workspaceParity: hostedWorkspace,
|
||||
accountsParity: hostedAccounts?.evidence ?? null,
|
||||
agentHistory: historyEvidence,
|
||||
coreRouteParity: hostedCoreRoutes?.evidence ?? null,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
captureScreenshot: vi.fn(),
|
||||
compareScreenshots: vi.fn(),
|
||||
dismissDeveloperMenu: vi.fn(),
|
||||
readTextPoint: vi.fn(),
|
||||
waitForControl: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFile: mocks.captureScreenshot
|
||||
}))
|
||||
vi.mock('../../scripts/emulator-developer-menu-dismissal.mjs', () => ({
|
||||
dismissEmulatorDeveloperMenuIfPresent: mocks.dismissDeveloperMenu
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-ios-emulator-accessibility.mjs', () => ({
|
||||
waitForHostedIosAccessibilityControl: mocks.waitForControl
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-ios-screenshot-parity.mjs', () => ({
|
||||
assertHostedIosScreenshotParity: mocks.compareScreenshots
|
||||
}))
|
||||
vi.mock('../../scripts/hosted-webview-cdp-session.mjs', () => ({
|
||||
readHostedWebViewTextPoint: mocks.readTextPoint
|
||||
}))
|
||||
|
||||
import {
|
||||
captureHostedWorkspaceParity,
|
||||
captureNativeWorkspaceBaseline
|
||||
} from '../../scripts/hosted-ios-workspace-parity.mjs'
|
||||
|
||||
describe('hosted iOS workspace parity', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.captureScreenshot.mockImplementation((_command, _args, callback) =>
|
||||
callback(null, '', '')
|
||||
)
|
||||
mocks.compareScreenshots.mockResolvedValue({ changedPixelRatio: 0.01 })
|
||||
mocks.readTextPoint.mockResolvedValue({ x: 0.2, y: 0.1 })
|
||||
mocks.waitForControl.mockResolvedValue({ x: 0.2, y: 0.1 })
|
||||
})
|
||||
|
||||
it('captures the unchanged native workspace screen', async () => {
|
||||
const baseline = await captureNativeWorkspaceBaseline({
|
||||
deviceUdid: 'simulator',
|
||||
emulator: { deviceUdid: 'simulator' },
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(mocks.waitForControl).toHaveBeenCalledWith({ deviceUdid: 'simulator' }, 'Filter', 30_000)
|
||||
expect(baseline).toEqual({
|
||||
filterPoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-workspace-portrait.png'
|
||||
})
|
||||
})
|
||||
|
||||
it('compares the unchanged hosted workspace screen', async () => {
|
||||
const evidence = await captureHostedWorkspaceParity({
|
||||
deviceUdid: 'simulator',
|
||||
document: { href: 'orca-mobile-web://build/h/host' },
|
||||
nativeBaseline: {
|
||||
filterPoint: { x: 0.2, y: 0.1 },
|
||||
screenshot: '/tmp/parity/native-workspace-portrait.png'
|
||||
},
|
||||
runtimeDirectory: '/tmp/parity',
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
|
||||
expect(mocks.readTextPoint).toHaveBeenCalledWith(expect.anything(), 'Filter')
|
||||
expect(mocks.compareScreenshots).toHaveBeenCalledOnce()
|
||||
expect(evidence).toEqual({
|
||||
nativeScreenshot: 'native-workspace-portrait.png',
|
||||
hostedScreenshot: 'hosted-workspace-portrait.png',
|
||||
nativeFilterPoint: { x: 0.2, y: 0.1 },
|
||||
hostedFilterPoint: { x: 0.2, y: 0.1 },
|
||||
screenshotParity: { changedPixelRatio: 0.01 }
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user