peachsim 0.1.527 → 0.1.570
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -26
- package/cli/app-fonts.ts +1 -1
- package/cli/app-splash.ts +1 -1
- package/cli/bin.ts +1 -1
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +57 -5
- package/cli/cloud-client.ts +273 -6
- package/cli/cloud-dispatch.ts +22 -2
- package/cli/cloud-session.ts +60 -4
- package/cli/cloud-test-tunnel.ts +354 -0
- package/cli/commands/box/bundle-plane.ts +2 -2
- package/cli/commands/box/checkout-plane.ts +1 -1
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +8 -8
- package/cli/commands/compat.ts +1 -1
- package/cli/commands/control.ts +22 -3
- package/cli/commands/detox.ts +1 -1
- package/cli/commands/device.ts +1 -1
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +60 -3
- package/cli/commands/inspect/actions.ts +1 -1
- package/cli/commands/inspect/core.ts +6 -7
- package/cli/commands/inspect/find.ts +2 -1
- package/cli/commands/inspect/resolve-target.ts +5 -3
- package/cli/commands/inspect/shared.ts +1 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-selector.ts +15 -4
- package/cli/commands/inspect.ts +19 -14
- package/cli/commands/maestro.ts +464 -43
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +205 -8
- package/cli/commands/preview.ts +2 -2
- package/cli/commands/record.ts +2 -2
- package/cli/commands/report-issue.ts +2 -2
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +2 -2
- package/cli/commands/test.ts +440 -115
- package/cli/commands/three-mode.ts +1 -1
- package/cli/commands/upload.ts +5 -5
- package/cli/commands/what-happened.ts +1 -1
- package/cli/desktop-companion.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +2 -2
- package/cli/flow-file.ts +7 -0
- package/cli/flow-live-status.ts +65 -8
- package/cli/help-core.ts +3 -3
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/maestro-report.ts +178 -0
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +185 -136
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +1 -1
- package/cli/run-registry.ts +8 -0
- package/cli/send-to-box.ts +4 -4
- package/cli/ws-bridge.ts +3 -5
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +2 -2
- package/detox/index.ts +71 -29
- package/detox/jest-preset.cjs +3 -3
- package/detox/jest-setup-after-env.cjs +1 -1
- package/dist-lib/agent-daemon-client.cjs +2 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +2 -2
- package/dist-lib/attached-projects.cjs +2 -2
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +94 -3
- package/dist-lib/capture-contract.mjs +93 -3
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud.cjs +2 -2
- package/dist-lib/cloud.mjs +2 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +106 -25
- package/dist-lib/detox/jest-preset.cjs +3 -3
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +137 -61
- package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
- package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +209 -16
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +2 -2
- package/dist-lib/menu.mjs +2 -2
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +3 -3
- package/dist-lib/metro-production-bundle.mjs +3 -3
- package/dist-lib/metro.cjs +8 -8
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +2 -2
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +31 -15
- package/dist-lib/sdk.cjs +3 -2
- package/dist-lib/sdk.mjs +3 -2
- package/dist-lib/skills.cjs +362 -163
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +583 -16
- package/dist-lib/vite.cjs +29 -14
- package/package.json +9 -9
- package/scripts/dev-server-scanner.ts +41 -14
- package/scripts/optional-demo-registry.ts +3 -3
- package/skills/contrast/SKILL.md +9 -9
- package/skills/rnx-debug/SKILL.md +14 -4
- package/skills/rnx-perf/SKILL.md +5 -5
- package/skills/rnx-setup/SKILL.md +11 -11
- package/skills/rnx-test/SKILL.md +11 -11
- package/skills/rnx-visual/SKILL.md +6 -6
- package/src/agent-sessions.ts +3 -3
- package/src/attached-projects.ts +1 -1
- package/src/bridge-contract-input.ts +1 -1
- package/src/bridge-contract.ts +23 -5
- package/src/capture-contract.ts +145 -3
- package/src/config.ts +2 -2
- package/src/connect.ts +2 -2
- package/src/home-paths.ts +3 -3
- package/src/host/bridge-host.ts +2 -2
- package/src/host/fetch-proxy-handler.ts +4 -3
- package/src/host/fetch-proxy-overrides.ts +41 -0
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +3 -0
- package/src/metro-plugin.ts +4 -4
- package/src/metro-production-bundle.ts +2 -2
- package/src/native-seam-manifest.ts +6 -6
- package/src/public-brand.ts +1 -1
- package/src/rolldown-plugin-swift.ts +8 -5
- package/src/runtime-assets.ts +8 -6
- package/src/screenshots/frame-compose.ts +24 -56
- package/src/screenshots/org-deck.ts +1 -1
- package/src/screenshots/registry.ts +4 -4
- package/src/screenshots/schema.ts +1 -1
- package/src/sdk.ts +2 -2
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/compat-check.ts +1 -1
- package/src/vite-plugin-one.ts +1 -1
- package/src/vite-plugin-swift.ts +11 -6
- package/src/vite-plugin.ts +30 -17
- package/src/worklets-babel.ts +2 -2
|
@@ -9,7 +9,7 @@ import { dirname, extname, resolve } from 'node:path'
|
|
|
9
9
|
import {
|
|
10
10
|
isThreeModeSetting,
|
|
11
11
|
THREE_MODE_SETTING_IDS,
|
|
12
|
-
} from '
|
|
12
|
+
} from 'peach-engine/screenshots/tokens'
|
|
13
13
|
import { rnxExit } from '../run-rnx'
|
|
14
14
|
import {
|
|
15
15
|
callInBridgeWrite,
|
package/cli/commands/upload.ts
CHANGED
|
@@ -9,18 +9,18 @@ import { createHash } from 'crypto'
|
|
|
9
9
|
import { readFileSync } from 'fs'
|
|
10
10
|
import path from 'path'
|
|
11
11
|
import { gzipSync, gunzipSync } from 'zlib'
|
|
12
|
-
import { shouldCompress } from '
|
|
13
|
-
import { brotliBundle } from '
|
|
12
|
+
import { shouldCompress } from 'peach-engine/preview/compress'
|
|
13
|
+
import { brotliBundle } from 'peach-engine/preview/compress-node'
|
|
14
14
|
import {
|
|
15
15
|
runPresignedUpload,
|
|
16
16
|
type InitRequestBody,
|
|
17
|
-
} from '
|
|
17
|
+
} from 'peach-engine/preview/presigned-upload'
|
|
18
18
|
import {
|
|
19
19
|
TRACE_CAPTURE_POSTROLL_MS,
|
|
20
20
|
TRACE_CAPTURE_PREROLL_MS,
|
|
21
21
|
isTraceTimelineEvent,
|
|
22
22
|
parseTraceTimelineJsonl,
|
|
23
|
-
} from '
|
|
23
|
+
} from 'peach-engine/preview/trace'
|
|
24
24
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
25
25
|
import { fetchAppFontFiles } from '../app-fonts'
|
|
26
26
|
import { fetchAppSplashImages } from '../app-splash'
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
evalInBridge,
|
|
35
35
|
parseBridgeCliArgs,
|
|
36
36
|
} from '../ws-bridge'
|
|
37
|
-
import type { SootSimTimelineEvent } from '@
|
|
37
|
+
import type { SootSimTimelineEvent } from '@peach/globals'
|
|
38
38
|
|
|
39
39
|
interface UploadOptions {
|
|
40
40
|
port?: number
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
inspectTimelineSummary,
|
|
26
26
|
NOISY_TIMELINE_KINDS,
|
|
27
27
|
} from './inspect/core'
|
|
28
|
-
import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@
|
|
28
|
+
import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@peach/globals'
|
|
29
29
|
|
|
30
30
|
interface WhatHappenedOptions {
|
|
31
31
|
port?: number
|
package/cli/desktop-companion.ts
CHANGED
|
@@ -12,7 +12,7 @@ function getSootsimRoot(): string | null {
|
|
|
12
12
|
if (_sootsimRoot !== undefined) return _sootsimRoot
|
|
13
13
|
try {
|
|
14
14
|
_sootsimRoot = dirname(
|
|
15
|
-
fileURLToPath(import.meta.resolve('
|
|
15
|
+
fileURLToPath(import.meta.resolve('peach-engine/package.json')),
|
|
16
16
|
)
|
|
17
17
|
} catch {
|
|
18
18
|
_sootsimRoot = null
|
|
@@ -54,7 +54,7 @@ function findDevElectron(): DesktopCompanionInstall | null {
|
|
|
54
54
|
if (!existsSync(engineMain)) return null
|
|
55
55
|
|
|
56
56
|
// walk up from engine dir looking for a node_modules/.bin/electron.
|
|
57
|
-
// the repo layout is packages/
|
|
57
|
+
// the repo layout is packages/peach-engine next to a root node_modules,
|
|
58
58
|
// but also works when installed as a dependency.
|
|
59
59
|
const electronBin = findElectronBin(engineDir)
|
|
60
60
|
if (!electronBin) return null
|
|
@@ -194,7 +194,7 @@ export function findDesktopCompanion(): DesktopCompanionInstall | null {
|
|
|
194
194
|
if (!existsSync(engineMain)) {
|
|
195
195
|
console.error(
|
|
196
196
|
` dist-electron/main.cjs missing in ${engineDir}.\n` +
|
|
197
|
-
` run: bun run --cwd packages/
|
|
197
|
+
` run: bun run --cwd packages/peach-engine build:electron-main\n` +
|
|
198
198
|
` (or keep it fresh with: bun run watch:sootsim:electron-main)`,
|
|
199
199
|
)
|
|
200
200
|
} else {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readBrowserCompositeCaptureRequest,
|
|
3
3
|
resolveBrowserCompositeCdpClip,
|
|
4
|
-
} from '../../../
|
|
5
|
-
import { createBrowserPointerDispatcher } from '../../../
|
|
4
|
+
} from '../../../peach-engine/src/capture/browser-composite'
|
|
5
|
+
import { createBrowserPointerDispatcher } from '../../../peach-engine/src/render-worker/browser-pointer'
|
|
6
6
|
// the detached browser host, as CJS source.
|
|
7
7
|
//
|
|
8
8
|
// it resolves playwright from the absolute path the driver hands it, launches
|
package/cli/flow-file.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface FlowFrontmatter {
|
|
|
16
16
|
env?: Record<string, string>
|
|
17
17
|
// maestro-compatible: workspace tag filtering (config.yml includeTags/excludeTags)
|
|
18
18
|
tags?: string[]
|
|
19
|
+
// maestro-compatible: display name for reports. defaults the junit
|
|
20
|
+
// testcase id, name, and classname when properties does not override them.
|
|
21
|
+
name?: string
|
|
22
|
+
// maestro-compatible: custom junit <property> entries. junitId and
|
|
23
|
+
// junitClassname are reserved: they set the testcase attributes and are
|
|
24
|
+
// not emitted as properties.
|
|
25
|
+
properties?: Record<string, string>
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
// parse flow file. accepts three shapes:
|
package/cli/flow-live-status.ts
CHANGED
|
@@ -44,17 +44,21 @@ export class FlowLiveStatusReporter {
|
|
|
44
44
|
private getSimId?: () => string | undefined,
|
|
45
45
|
) {}
|
|
46
46
|
|
|
47
|
-
private async send(
|
|
47
|
+
private async send(
|
|
48
|
+
payload: Record<string, unknown>,
|
|
49
|
+
simId: string | undefined,
|
|
50
|
+
): Promise<unknown> {
|
|
48
51
|
const reply: unknown = await this.bridge.send({
|
|
49
52
|
type: 'flowStatus',
|
|
50
53
|
simId,
|
|
51
54
|
status: { runId: this.runId, flowName: this.flowName, ...payload },
|
|
52
55
|
})
|
|
53
56
|
this.paused = replyPaused(reply)
|
|
57
|
+
return reply
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
private async push(payload: Record<string, unknown>): Promise<
|
|
57
|
-
if (this.disabled) return
|
|
60
|
+
private async push(payload: Record<string, unknown>): Promise<unknown> {
|
|
61
|
+
if (this.disabled) return undefined
|
|
58
62
|
try {
|
|
59
63
|
const simId = this.getSimId?.()
|
|
60
64
|
if (
|
|
@@ -77,19 +81,68 @@ export class FlowLiveStatusReporter {
|
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
this.lastSimId = simId
|
|
80
|
-
await this.send(payload, simId)
|
|
84
|
+
return await this.send(payload, simId)
|
|
81
85
|
} catch (error) {
|
|
82
86
|
this.disabled = true
|
|
83
87
|
console.log(
|
|
84
88
|
`[flow] live status unavailable (${error instanceof Error ? error.message.slice(0, 120) : error}) — continuing without the devtools rail`,
|
|
85
89
|
)
|
|
90
|
+
return undefined
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
async plan(steps: PlanStep[]): Promise<void> {
|
|
94
|
+
async plan(steps: PlanStep[], options: { dryRun?: boolean } = {}): Promise<void> {
|
|
90
95
|
this.plannedSteps = steps.map((step) => ({ ...step }))
|
|
91
96
|
this.completedSteps.clear()
|
|
92
|
-
await this.push({
|
|
97
|
+
await this.push({
|
|
98
|
+
phase: 'plan',
|
|
99
|
+
state: options.dryRun ? 'idle' : 'running',
|
|
100
|
+
dryRun: options.dryRun,
|
|
101
|
+
steps,
|
|
102
|
+
source: this.flowSource,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async waitForStart(): Promise<void> {
|
|
107
|
+
if (this.disabled) return
|
|
108
|
+
while (!this.disabled) {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
110
|
+
const reply = await this.push({ phase: 'heartbeat', state: 'idle' })
|
|
111
|
+
if (
|
|
112
|
+
reply &&
|
|
113
|
+
typeof reply === 'object' &&
|
|
114
|
+
'control' in reply &&
|
|
115
|
+
typeof (reply as { control?: unknown }).control === 'object'
|
|
116
|
+
) {
|
|
117
|
+
const ctrl = (reply as { control: { start?: boolean } }).control
|
|
118
|
+
if (ctrl?.start === true) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async waitForResetOrRerun(): Promise<'reset' | 'start' | 'exit'> {
|
|
126
|
+
if (this.disabled) return 'exit'
|
|
127
|
+
while (!this.disabled) {
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
129
|
+
const reply = await this.push({ phase: 'heartbeat' })
|
|
130
|
+
if (
|
|
131
|
+
reply &&
|
|
132
|
+
typeof reply === 'object' &&
|
|
133
|
+
'control' in reply &&
|
|
134
|
+
typeof (reply as { control?: unknown }).control === 'object'
|
|
135
|
+
) {
|
|
136
|
+
const ctrl = (reply as { control: { reset?: boolean; start?: boolean } }).control
|
|
137
|
+
if (ctrl?.reset === true) {
|
|
138
|
+
return 'reset'
|
|
139
|
+
}
|
|
140
|
+
if (ctrl?.start === true) {
|
|
141
|
+
return 'start'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return 'exit'
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
async step(
|
|
@@ -114,10 +167,14 @@ export class FlowLiveStatusReporter {
|
|
|
114
167
|
// steps only, so a pause never interrupts a step mid-gesture.
|
|
115
168
|
async waitWhilePaused(): Promise<void> {
|
|
116
169
|
if (this.disabled) return
|
|
117
|
-
|
|
170
|
+
const reply = await this.push({ phase: 'heartbeat' })
|
|
171
|
+
if (replyPaused(reply)) {
|
|
172
|
+
this.paused = true
|
|
173
|
+
}
|
|
174
|
+
while (this.paused && !this.disabled) {
|
|
118
175
|
console.log('[flow] paused from devtools — waiting…')
|
|
119
176
|
while (this.paused && !this.disabled) {
|
|
120
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
121
178
|
await this.push({ phase: 'heartbeat', state: 'paused' })
|
|
122
179
|
}
|
|
123
180
|
if (!this.disabled) console.log('[flow] resumed')
|
package/cli/help-core.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { cliCommandMetas } from '@
|
|
1
|
+
import { cliCommandMetas } from '@peach/skills/cli'
|
|
2
2
|
import {
|
|
3
3
|
renderCliCommandHelp,
|
|
4
4
|
renderCliGroupHelp,
|
|
5
5
|
renderCliIndexHelp,
|
|
6
|
-
} from '@
|
|
7
|
-
import { formatFlagHelp } from '
|
|
6
|
+
} from '@peach/skills/cli/renderers'
|
|
7
|
+
import { formatFlagHelp } from 'peach-engine/settings/cli-flags'
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
10
10
|
DEFAULT_SOOTSIM_BRIDGE_URL,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// maestro-compatible run reports: junit xml, per-flow commands.json, and the
|
|
2
|
+
// session/flow maestro.log text. pure builders over playback receipts — no
|
|
3
|
+
// bridge, no filesystem — so the workspace tests cover them without a sim.
|
|
4
|
+
//
|
|
5
|
+
// the shapes mirror maestro's own outputs (test-reports-and-artifacts): one
|
|
6
|
+
// <testsuite name="Test Suite"> per invocation, flow-header `properties:`
|
|
7
|
+
// as <property> children, junitId/junitClassname reserved for the testcase
|
|
8
|
+
// attributes, commands.json carrying one entry per executed step.
|
|
9
|
+
|
|
10
|
+
import type { SootSimFlowTraceStep } from './bridge-flow-runner'
|
|
11
|
+
|
|
12
|
+
export function escapeXml(value: string): string {
|
|
13
|
+
return value
|
|
14
|
+
.replace(/&/g, '&')
|
|
15
|
+
.replace(/</g, '<')
|
|
16
|
+
.replace(/>/g, '>')
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/'/g, ''')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JUnitCaseInput {
|
|
22
|
+
// flow path as printed in the run (relative to the invocation cwd)
|
|
23
|
+
file: string
|
|
24
|
+
name: string
|
|
25
|
+
id: string
|
|
26
|
+
classname: string
|
|
27
|
+
// custom flow-header properties with the reserved junit keys removed
|
|
28
|
+
properties: Record<string, string>
|
|
29
|
+
tags: string[]
|
|
30
|
+
timeSeconds: number
|
|
31
|
+
failure: { message: string; body: string } | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface JUnitSuiteInput {
|
|
35
|
+
device: string
|
|
36
|
+
cases: JUnitCaseInput[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatJUnitTime(seconds: number): string {
|
|
40
|
+
return (Number.isFinite(seconds) && seconds >= 0 ? seconds : 0).toFixed(3)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildJUnitXml(suite: JUnitSuiteInput): string {
|
|
44
|
+
const failures = suite.cases.filter((c) => c.failure !== null).length
|
|
45
|
+
const totalTime = suite.cases.reduce(
|
|
46
|
+
(sum, c) => sum + (Number.isFinite(c.timeSeconds) ? c.timeSeconds : 0),
|
|
47
|
+
0,
|
|
48
|
+
)
|
|
49
|
+
const lines = [
|
|
50
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
51
|
+
'<testsuites>',
|
|
52
|
+
` <testsuite name="Test Suite" device="${escapeXml(suite.device)}" tests="${suite.cases.length}" failures="${failures}" time="${formatJUnitTime(totalTime)}">`,
|
|
53
|
+
]
|
|
54
|
+
for (const c of suite.cases) {
|
|
55
|
+
lines.push(
|
|
56
|
+
` <testcase id="${escapeXml(c.id)}" name="${escapeXml(c.name)}" classname="${escapeXml(c.classname)}" file="${escapeXml(c.file)}" time="${formatJUnitTime(c.timeSeconds)}">`,
|
|
57
|
+
)
|
|
58
|
+
const properties: Array<{ name: string; value: string }> = []
|
|
59
|
+
if (c.tags.length > 0) {
|
|
60
|
+
properties.push({ name: 'tags', value: c.tags.join(',') })
|
|
61
|
+
}
|
|
62
|
+
for (const [name, value] of Object.entries(c.properties)) {
|
|
63
|
+
properties.push({ name, value })
|
|
64
|
+
}
|
|
65
|
+
if (properties.length > 0) {
|
|
66
|
+
lines.push(' <properties>')
|
|
67
|
+
for (const p of properties) {
|
|
68
|
+
lines.push(
|
|
69
|
+
` <property name="${escapeXml(p.name)}" value="${escapeXml(p.value)}"/>`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
lines.push(' </properties>')
|
|
73
|
+
}
|
|
74
|
+
if (c.failure) {
|
|
75
|
+
lines.push(` <failure message="${escapeXml(c.failure.message)}">`)
|
|
76
|
+
lines.push(escapeXml(c.failure.body))
|
|
77
|
+
lines.push(' </failure>')
|
|
78
|
+
}
|
|
79
|
+
lines.push(' </testcase>')
|
|
80
|
+
}
|
|
81
|
+
lines.push(' </testsuite>')
|
|
82
|
+
lines.push('</testsuites>')
|
|
83
|
+
return `${lines.join('\n')}\n`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// the first failed trace step as a junit failure. a flow that died before
|
|
87
|
+
// producing steps (parse error, sim never attached) has no failed step, so
|
|
88
|
+
// the caller passes its own fallback text instead of an empty failure.
|
|
89
|
+
export function failureFromTraceSteps(
|
|
90
|
+
steps: SootSimFlowTraceStep[],
|
|
91
|
+
fallbackMessage: string,
|
|
92
|
+
): { message: string; body: string } {
|
|
93
|
+
const failed = steps.find((step) => step.status === 'failure')
|
|
94
|
+
if (!failed) {
|
|
95
|
+
return { message: fallbackMessage, body: fallbackMessage }
|
|
96
|
+
}
|
|
97
|
+
const error = failed.error || 'flow step failed'
|
|
98
|
+
const message = error.split('\n')[0] || fallbackMessage
|
|
99
|
+
const bodyLines = [`step ${failed.stepIndex + 1} (${failed.stepName}): ${error}`]
|
|
100
|
+
if (failed.screenshotPath) {
|
|
101
|
+
bodyLines.push(`screenshot: ${failed.screenshotPath}`)
|
|
102
|
+
}
|
|
103
|
+
return { message, body: bodyLines.join('\n') }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface FlowCommandsEntry {
|
|
107
|
+
sequenceNumber: number
|
|
108
|
+
command: string
|
|
109
|
+
status: SootSimFlowTraceStep['status']
|
|
110
|
+
durationMs: number
|
|
111
|
+
error?: string
|
|
112
|
+
artifacts: string[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildFlowCommandsJson(steps: SootSimFlowTraceStep[]): string {
|
|
116
|
+
const entries: FlowCommandsEntry[] = steps.map((step) => ({
|
|
117
|
+
sequenceNumber: step.stepIndex + 1,
|
|
118
|
+
command: step.targetLabel ? `${step.stepName} ${step.targetLabel}` : step.stepName,
|
|
119
|
+
status: step.status,
|
|
120
|
+
durationMs: step.durationMs,
|
|
121
|
+
...(step.error ? { error: step.error } : {}),
|
|
122
|
+
artifacts: step.screenshotPath ? [step.screenshotPath] : [],
|
|
123
|
+
}))
|
|
124
|
+
return `${JSON.stringify(entries, null, 2)}\n`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SessionLogFlow {
|
|
128
|
+
file: string
|
|
129
|
+
passed: boolean
|
|
130
|
+
durationMs: number
|
|
131
|
+
steps: SootSimFlowTraceStep[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatLogDuration(ms: number): string {
|
|
135
|
+
return `${(Math.max(0, ms) / 1000).toFixed(1)}s`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatLogStep(step: SootSimFlowTraceStep): string {
|
|
139
|
+
const label = step.targetLabel ? `${step.stepName} ${step.targetLabel}` : step.stepName
|
|
140
|
+
const base = ` ${step.stepIndex + 1}. ${label} - ${step.status} (${formatLogDuration(step.durationMs)})`
|
|
141
|
+
if (step.status === 'failure' && step.error) {
|
|
142
|
+
return `${base}: ${step.error.split('\n')[0]}`
|
|
143
|
+
}
|
|
144
|
+
return base
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// one flow's section, shared by the session log and the per-flow logs file
|
|
148
|
+
// so the two never disagree about what a flow did.
|
|
149
|
+
export function buildFlowLogLines(flow: SessionLogFlow): string[] {
|
|
150
|
+
const lines = [
|
|
151
|
+
`# flow: ${flow.file} - ${flow.passed ? 'pass' : 'FAIL'} (${formatLogDuration(flow.durationMs)}, ${flow.steps.length} steps)`,
|
|
152
|
+
]
|
|
153
|
+
for (const step of flow.steps) {
|
|
154
|
+
lines.push(formatLogStep(step))
|
|
155
|
+
}
|
|
156
|
+
return lines
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildSessionLog(input: {
|
|
160
|
+
startedAtIso: string
|
|
161
|
+
root: string
|
|
162
|
+
device: string
|
|
163
|
+
flows: SessionLogFlow[]
|
|
164
|
+
}): string {
|
|
165
|
+
const passed = input.flows.filter((flow) => flow.passed).length
|
|
166
|
+
const lines = [
|
|
167
|
+
'# rnx maestro session log',
|
|
168
|
+
`# started: ${input.startedAtIso}`,
|
|
169
|
+
`# root: ${input.root}`,
|
|
170
|
+
`# device: ${input.device}`,
|
|
171
|
+
`# result: ${passed}/${input.flows.length} passed`,
|
|
172
|
+
]
|
|
173
|
+
for (const flow of input.flows) {
|
|
174
|
+
lines.push('#')
|
|
175
|
+
lines.push(...buildFlowLogLines(flow))
|
|
176
|
+
}
|
|
177
|
+
return `${lines.join('\n')}\n`
|
|
178
|
+
}
|
package/cli/main.ts
CHANGED
|
@@ -254,7 +254,7 @@ const commandArgsWithSim = globalSimTarget
|
|
|
254
254
|
|
|
255
255
|
// apply device/theme/home-screen to settings store if provided
|
|
256
256
|
if (device || theme || homeScreen) {
|
|
257
|
-
const { settingsStore } = await import('
|
|
257
|
+
const { settingsStore } = await import('peach-engine/settings/store')
|
|
258
258
|
const overrides: Record<string, any> = {}
|
|
259
259
|
if (device) overrides.deviceModel = device
|
|
260
260
|
if (theme) overrides.colorScheme = theme
|