taphound 0.2.0-dev.1

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.
Files changed (176) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +200 -0
  3. package/README.zh-CN.md +200 -0
  4. package/assets/brand/taphound-mark.svg +9 -0
  5. package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
  6. package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
  7. package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
  8. package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
  9. package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
  10. package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
  11. package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
  12. package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
  13. package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
  14. package/dist/adapters/adb/adb-adapter.d.ts +23 -0
  15. package/dist/adapters/adb/adb-adapter.js +218 -0
  16. package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
  17. package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
  18. package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
  19. package/dist/adapters/android-cli/layout-parser.js +174 -0
  20. package/dist/adapters/clock/system-clock.d.ts +5 -0
  21. package/dist/adapters/clock/system-clock.js +13 -0
  22. package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
  23. package/dist/adapters/filesystem/artifact-store.js +73 -0
  24. package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
  25. package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
  26. package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
  27. package/dist/adapters/filesystem/generation-session-store.js +1603 -0
  28. package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
  29. package/dist/adapters/filesystem/journey-writer.js +34 -0
  30. package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
  31. package/dist/adapters/filesystem/project-bound-file.js +240 -0
  32. package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
  33. package/dist/adapters/filesystem/project-file-inspector.js +198 -0
  34. package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
  35. package/dist/adapters/filesystem/skill-installer.js +39 -0
  36. package/dist/adapters/process/node-process-runner.d.ts +8 -0
  37. package/dist/adapters/process/node-process-runner.js +155 -0
  38. package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
  39. package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
  40. package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
  41. package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
  42. package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
  43. package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
  44. package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
  45. package/dist/application/assertion/expectation-evaluator.js +207 -0
  46. package/dist/application/collector/logcat-collector.d.ts +39 -0
  47. package/dist/application/collector/logcat-collector.js +107 -0
  48. package/dist/application/context/context-validator.d.ts +27 -0
  49. package/dist/application/context/context-validator.js +120 -0
  50. package/dist/application/doctor/doctor-service.d.ts +35 -0
  51. package/dist/application/doctor/doctor-service.js +178 -0
  52. package/dist/application/generation/focused-input.d.ts +2 -0
  53. package/dist/application/generation/focused-input.js +9 -0
  54. package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
  55. package/dist/application/generation/generation-confirmation-service.js +383 -0
  56. package/dist/application/generation/generation-finalizer.d.ts +67 -0
  57. package/dist/application/generation/generation-finalizer.js +646 -0
  58. package/dist/application/generation/generation-publisher.d.ts +46 -0
  59. package/dist/application/generation/generation-publisher.js +178 -0
  60. package/dist/application/generation/generation-starter.d.ts +31 -0
  61. package/dist/application/generation/generation-starter.js +102 -0
  62. package/dist/application/generation/generation-step-executor.d.ts +52 -0
  63. package/dist/application/generation/generation-step-executor.js +668 -0
  64. package/dist/application/generation/proposed-step-validator.d.ts +11 -0
  65. package/dist/application/generation/proposed-step-validator.js +88 -0
  66. package/dist/application/generation/risk-evaluator.d.ts +9 -0
  67. package/dist/application/generation/risk-evaluator.js +14 -0
  68. package/dist/application/generation/runtime-observer.d.ts +39 -0
  69. package/dist/application/generation/runtime-observer.js +157 -0
  70. package/dist/application/init/init-service.d.ts +17 -0
  71. package/dist/application/init/init-service.js +44 -0
  72. package/dist/application/interaction/action-executor.d.ts +25 -0
  73. package/dist/application/interaction/action-executor.js +103 -0
  74. package/dist/application/interaction/fallback-resolver.d.ts +24 -0
  75. package/dist/application/interaction/fallback-resolver.js +48 -0
  76. package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
  77. package/dist/application/interaction/scroll-to-executor.js +191 -0
  78. package/dist/application/locator/locator-resolver.d.ts +20 -0
  79. package/dist/application/locator/locator-resolver.js +89 -0
  80. package/dist/application/project/project-describer.d.ts +14 -0
  81. package/dist/application/project/project-describer.js +8 -0
  82. package/dist/application/recorder/locator-selector.d.ts +10 -0
  83. package/dist/application/recorder/locator-selector.js +80 -0
  84. package/dist/application/recorder/recorder-service.d.ts +41 -0
  85. package/dist/application/recorder/recorder-service.js +331 -0
  86. package/dist/application/report/report-writer.d.ts +10 -0
  87. package/dist/application/report/report-writer.js +39 -0
  88. package/dist/application/runtime/activity-waiter.d.ts +33 -0
  89. package/dist/application/runtime/activity-waiter.js +73 -0
  90. package/dist/application/runtime/launch-failure.d.ts +2 -0
  91. package/dist/application/runtime/launch-failure.js +20 -0
  92. package/dist/application/runtime/process-waiter.d.ts +27 -0
  93. package/dist/application/runtime/process-waiter.js +73 -0
  94. package/dist/application/runtime/step-runner.d.ts +50 -0
  95. package/dist/application/runtime/step-runner.js +429 -0
  96. package/dist/application/runtime/verify-runtime.d.ts +44 -0
  97. package/dist/application/runtime/verify-runtime.js +350 -0
  98. package/dist/application/wait/idle-waiter.d.ts +30 -0
  99. package/dist/application/wait/idle-waiter.js +90 -0
  100. package/dist/cli/commands/context.d.ts +3 -0
  101. package/dist/cli/commands/context.js +94 -0
  102. package/dist/cli/commands/doctor.d.ts +3 -0
  103. package/dist/cli/commands/doctor.js +51 -0
  104. package/dist/cli/commands/generation.d.ts +3 -0
  105. package/dist/cli/commands/generation.js +518 -0
  106. package/dist/cli/commands/init.d.ts +3 -0
  107. package/dist/cli/commands/init.js +104 -0
  108. package/dist/cli/commands/project.d.ts +3 -0
  109. package/dist/cli/commands/project.js +58 -0
  110. package/dist/cli/commands/record.d.ts +3 -0
  111. package/dist/cli/commands/record.js +88 -0
  112. package/dist/cli/commands/verify.d.ts +3 -0
  113. package/dist/cli/commands/verify.js +106 -0
  114. package/dist/cli/dependencies.d.ts +66 -0
  115. package/dist/cli/dependencies.js +204 -0
  116. package/dist/cli/main.d.ts +8 -0
  117. package/dist/cli/main.js +73 -0
  118. package/dist/cli/output.d.ts +16 -0
  119. package/dist/cli/output.js +22 -0
  120. package/dist/cli/program.d.ts +3 -0
  121. package/dist/cli/program.js +43 -0
  122. package/dist/domain/activity.d.ts +7 -0
  123. package/dist/domain/activity.js +65 -0
  124. package/dist/domain/app-process.d.ts +7 -0
  125. package/dist/domain/app-process.js +10 -0
  126. package/dist/domain/config.d.ts +15 -0
  127. package/dist/domain/config.js +17 -0
  128. package/dist/domain/failure.d.ts +4 -0
  129. package/dist/domain/failure.js +43 -0
  130. package/dist/domain/generation.d.ts +912 -0
  131. package/dist/domain/generation.js +342 -0
  132. package/dist/domain/init.d.ts +42 -0
  133. package/dist/domain/init.js +114 -0
  134. package/dist/domain/journey.d.ts +673 -0
  135. package/dist/domain/journey.js +109 -0
  136. package/dist/domain/layout.d.ts +35 -0
  137. package/dist/domain/layout.js +33 -0
  138. package/dist/domain/locator.d.ts +8 -0
  139. package/dist/domain/locator.js +5 -0
  140. package/dist/domain/project-context.d.ts +101 -0
  141. package/dist/domain/project-context.js +64 -0
  142. package/dist/domain/proposed-step.d.ts +341 -0
  143. package/dist/domain/proposed-step.js +87 -0
  144. package/dist/domain/report.d.ts +349 -0
  145. package/dist/domain/report.js +135 -0
  146. package/dist/domain/runtime-snapshot.d.ts +16 -0
  147. package/dist/domain/runtime-snapshot.js +46 -0
  148. package/dist/ports/adb.d.ts +42 -0
  149. package/dist/ports/adb.js +1 -0
  150. package/dist/ports/android-cli.d.ts +21 -0
  151. package/dist/ports/android-cli.js +1 -0
  152. package/dist/ports/artifact-store.d.ts +12 -0
  153. package/dist/ports/artifact-store.js +1 -0
  154. package/dist/ports/clock.d.ts +4 -0
  155. package/dist/ports/clock.js +1 -0
  156. package/dist/ports/generation-meta-writer.d.ts +10 -0
  157. package/dist/ports/generation-meta-writer.js +1 -0
  158. package/dist/ports/generation-prompt.d.ts +19 -0
  159. package/dist/ports/generation-prompt.js +6 -0
  160. package/dist/ports/generation-session-store.d.ts +32 -0
  161. package/dist/ports/generation-session-store.js +26 -0
  162. package/dist/ports/init-prompt.d.ts +8 -0
  163. package/dist/ports/init-prompt.js +6 -0
  164. package/dist/ports/journey-writer.d.ts +10 -0
  165. package/dist/ports/journey-writer.js +1 -0
  166. package/dist/ports/process-runner.d.ts +32 -0
  167. package/dist/ports/process-runner.js +1 -0
  168. package/dist/ports/project-bound-file.d.ts +8 -0
  169. package/dist/ports/project-bound-file.js +1 -0
  170. package/dist/ports/project-file-inspector.d.ts +33 -0
  171. package/dist/ports/project-file-inspector.js +1 -0
  172. package/dist/ports/recorder-prompt.d.ts +29 -0
  173. package/dist/ports/recorder-prompt.js +1 -0
  174. package/dist/ports/skill-installer.d.ts +8 -0
  175. package/dist/ports/skill-installer.js +1 -0
  176. package/package.json +64 -0
@@ -0,0 +1,429 @@
1
+ import { appProcessPids, primaryAppPid } from "../../domain/app-process.js";
2
+ import { ActionExecutor } from "../interaction/action-executor.js";
3
+ import { FallbackResolver } from "../interaction/fallback-resolver.js";
4
+ import { ScrollToExecutor } from "../interaction/scroll-to-executor.js";
5
+ import { resolveLocator } from "../locator/locator-resolver.js";
6
+ import { ExpectationEvaluator } from "../assertion/expectation-evaluator.js";
7
+ import { IdleWaiter } from "../wait/idle-waiter.js";
8
+ import { hasExactlyOneEnabledFocusedElement } from "../generation/focused-input.js";
9
+ function stepPath(index, suffix) {
10
+ return `steps/${String(index + 1).padStart(3, "0")}-${suffix}`;
11
+ }
12
+ function targetForPoint(point) {
13
+ return {
14
+ point,
15
+ bounds: {
16
+ left: Math.max(0, point.x - 1),
17
+ top: Math.max(0, point.y - 1),
18
+ right: Math.max(1, point.x + 1),
19
+ bottom: Math.max(1, point.y + 1)
20
+ }
21
+ };
22
+ }
23
+ export class StepRunner {
24
+ options;
25
+ actionExecutor;
26
+ fallbackResolver;
27
+ idleWaiter;
28
+ expectationEvaluator;
29
+ scrollToExecutor;
30
+ constructor(options) {
31
+ this.options = options;
32
+ this.actionExecutor = new ActionExecutor(options.adb, options.deviceSerial);
33
+ this.fallbackResolver = new FallbackResolver(options.androidCli, options.deviceSerial);
34
+ this.idleWaiter = new IdleWaiter(options.androidCli, options.clock, options.deviceSerial);
35
+ this.expectationEvaluator = new ExpectationEvaluator(options.adb, options.androidCli, options.logcat, options.clock);
36
+ this.scrollToExecutor = new ScrollToExecutor({
37
+ androidCli: options.androidCli,
38
+ actionExecutor: this.actionExecutor,
39
+ idleWaiter: this.idleWaiter,
40
+ deviceSerial: options.deviceSerial,
41
+ idle: options.idle,
42
+ ...(options.generatedReplayPolicy === true
43
+ ? {
44
+ readLayout: async () => (this.captureGeneratedLayout(this.currentSignal)),
45
+ beforeMutation: async () => {
46
+ await this.assertGeneratedForeground(undefined, "ACTIVITY_BEFORE_MISMATCH", this.currentSignal);
47
+ },
48
+ requireLiveContainerCapability: true
49
+ }
50
+ : {})
51
+ });
52
+ }
53
+ identity = (signal) => ({
54
+ packageName: this.options.packageName,
55
+ deviceSerial: this.options.deviceSerial,
56
+ ...(signal === undefined ? {} : { signal }),
57
+ timeoutMs: this.options.idle.timeoutMs
58
+ });
59
+ primaryPid = async (identity) => {
60
+ const processes = await this.options.adb.appProcesses(identity);
61
+ this.options.logcat.scopeToPids(appProcessPids(processes));
62
+ return primaryAppPid(processes, this.options.packageName);
63
+ };
64
+ async generatedForeground(expectedActivity, code, signal) {
65
+ const foreground = await this.options.adb.foregroundComponent(this.identity(signal));
66
+ if (foreground.packageName !== this.options.packageName
67
+ || foreground.activity !== expectedActivity) {
68
+ throw Object.assign(new Error(`Expected foreground ${this.options.packageName}/${expectedActivity}, found ${foreground.packageName}/${foreground.activity}`), { code });
69
+ }
70
+ return foreground;
71
+ }
72
+ async assertGeneratedForeground(expectedActivity, code = "ACTIVITY_BEFORE_MISMATCH", signal) {
73
+ await this.generatedForeground(expectedActivity ?? this.currentExpectedActivity, code, signal);
74
+ }
75
+ currentExpectedActivity = "";
76
+ currentSignal;
77
+ async observeExpectedActivity(expectedActivity, expectedPid, input) {
78
+ const deadline = this.options.clock.now() + input.timeoutMs;
79
+ const identity = () => ({
80
+ packageName: this.options.packageName,
81
+ deviceSerial: this.options.deviceSerial,
82
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
83
+ timeoutMs: Math.max(1, deadline - this.options.clock.now())
84
+ });
85
+ const foreground = await this.options.adb.foregroundComponent(identity());
86
+ if (foreground.packageName !== this.options.packageName
87
+ || (expectedActivity !== undefined
88
+ && foreground.activity !== expectedActivity)) {
89
+ return {
90
+ status: "failed",
91
+ message: `Generated Expect foreground changed to ${foreground.packageName}/${foreground.activity}`
92
+ };
93
+ }
94
+ const pid = await this.primaryPid(identity());
95
+ if (pid !== expectedPid) {
96
+ return {
97
+ status: "failed",
98
+ message: `Generated Expect process changed from ${String(expectedPid)} to ${String(pid)}`
99
+ };
100
+ }
101
+ return { status: "observed", activity: foreground.activity };
102
+ }
103
+ async observeExpectedLayout(expectedActivity, expectedPid, input) {
104
+ const deadline = this.options.clock.now() + input.timeoutMs;
105
+ const observationInput = () => ({
106
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
107
+ timeoutMs: Math.max(1, deadline - this.options.clock.now())
108
+ });
109
+ const before = await this.observeExpectedActivity(expectedActivity, expectedPid, observationInput());
110
+ if (before.status === "failed")
111
+ return before;
112
+ const layout = await this.options.androidCli.layout({
113
+ deviceSerial: this.options.deviceSerial,
114
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
115
+ timeoutMs: observationInput().timeoutMs
116
+ });
117
+ const after = await this.observeExpectedActivity(expectedActivity, expectedPid, observationInput());
118
+ return after.status === "failed"
119
+ ? after
120
+ : { status: "observed", layout };
121
+ }
122
+ async captureGeneratedLayout(signal) {
123
+ await this.assertGeneratedForeground(this.currentExpectedActivity, "ACTIVITY_BEFORE_MISMATCH", signal);
124
+ const layout = await this.options.androidCli.layout({
125
+ deviceSerial: this.options.deviceSerial,
126
+ ...(signal === undefined ? {} : { signal }),
127
+ timeoutMs: this.options.idle.timeoutMs
128
+ });
129
+ await this.assertGeneratedForeground(this.currentExpectedActivity, "ACTIVITY_BEFORE_MISMATCH", signal);
130
+ return layout;
131
+ }
132
+ async run(step, index, signal) {
133
+ const startedAt = this.options.clock.now();
134
+ const logcatPath = stepPath(index, "logcat.txt");
135
+ const activityReport = {
136
+ before: {
137
+ status: "notRun",
138
+ expected: step.activity.before
139
+ },
140
+ after: {
141
+ status: "notRun",
142
+ expected: step.activity.after
143
+ }
144
+ };
145
+ const report = {
146
+ index,
147
+ action: step.action,
148
+ status: "notRun",
149
+ startedAtMs: startedAt,
150
+ finishedAtMs: startedAt,
151
+ durationMs: 0,
152
+ activity: activityReport,
153
+ logcatPath
154
+ };
155
+ const finish = async (status, failure) => {
156
+ const finishedAt = this.options.clock.now();
157
+ report.finishedAtMs = finishedAt;
158
+ report.durationMs = finishedAt - startedAt;
159
+ report.status = status === "cancelled" ? "notRun" : status;
160
+ const log = this.options.logcat
161
+ .linesBetween(startedAt, finishedAt)
162
+ .map((line) => line.raw)
163
+ .join("\n");
164
+ await this.options.artifacts.writeText(logcatPath, log.length === 0 ? "" : `${log}\n`);
165
+ if (status === "failed" && failure !== undefined) {
166
+ return { status, report, failure };
167
+ }
168
+ return status === "cancelled"
169
+ ? { status, report }
170
+ : { status: "passed", report };
171
+ };
172
+ const fail = async (code, message) => finish("failed", {
173
+ code,
174
+ message,
175
+ phase: "replay",
176
+ stepIndex: index
177
+ });
178
+ if (signal?.aborted === true) {
179
+ return finish("cancelled");
180
+ }
181
+ const identity = this.identity(signal);
182
+ this.currentExpectedActivity = step.activity.before;
183
+ this.currentSignal = signal;
184
+ let before;
185
+ try {
186
+ before = this.options.generatedReplayPolicy === true
187
+ ? (await this.generatedForeground(step.activity.before, "ACTIVITY_BEFORE_MISMATCH", signal)).activity
188
+ : await this.options.adb.currentActivity(identity);
189
+ }
190
+ catch (error) {
191
+ if (error !== null
192
+ && typeof error === "object"
193
+ && "code" in error
194
+ && error.code === "ACTIVITY_BEFORE_MISMATCH") {
195
+ return fail("ACTIVITY_BEFORE_MISMATCH", errorMessage(error));
196
+ }
197
+ throw error;
198
+ }
199
+ activityReport.before = {
200
+ status: before === step.activity.before ? "passed" : "failed",
201
+ expected: step.activity.before,
202
+ actual: before
203
+ };
204
+ if (before !== step.activity.before) {
205
+ return fail("ACTIVITY_BEFORE_MISMATCH", `Expected Activity ${step.activity.before}, found ${before}`);
206
+ }
207
+ const generatedPid = this.options.generatedReplayPolicy === true
208
+ ? await this.primaryPid(identity)
209
+ : undefined;
210
+ if (this.options.generatedReplayPolicy === true
211
+ && generatedPid === null) {
212
+ return fail("APP_CRASHED", "Generated replay process is not running");
213
+ }
214
+ let target;
215
+ if (step.action === "scrollTo") {
216
+ const scroll = await this.scrollToExecutor.execute(step, signal);
217
+ report.scroll = {
218
+ swipesUsed: scroll.swipesUsed,
219
+ maxSwipes: step.maxSwipes
220
+ };
221
+ if (scroll.status === "cancelled") {
222
+ return finish("cancelled");
223
+ }
224
+ if (scroll.status === "failed") {
225
+ if (scroll.idle !== undefined) {
226
+ await this.options.artifacts.writeJson(stepPath(index, "layout-diff.json"), scroll.idle.lastDiff);
227
+ report.idle = {
228
+ status: "timeout",
229
+ polls: scroll.idle.polls,
230
+ lastDiff: [...scroll.idle.lastDiff]
231
+ };
232
+ }
233
+ return fail(scroll.code, scroll.message);
234
+ }
235
+ }
236
+ else {
237
+ let layout;
238
+ try {
239
+ layout = this.options.generatedReplayPolicy === true
240
+ ? await this.captureGeneratedLayout(signal)
241
+ : await this.options.androidCli.layout({
242
+ deviceSerial: this.options.deviceSerial,
243
+ ...(signal === undefined ? {} : { signal }),
244
+ timeoutMs: this.options.idle.timeoutMs
245
+ });
246
+ }
247
+ catch (error) {
248
+ if (error !== null
249
+ && typeof error === "object"
250
+ && "code" in error
251
+ && error.code === "ACTIVITY_BEFORE_MISMATCH") {
252
+ return fail("ACTIVITY_BEFORE_MISMATCH", errorMessage(error));
253
+ }
254
+ throw error;
255
+ }
256
+ if (step.action === "click"
257
+ || step.action === "longClick"
258
+ || step.action === "swipe") {
259
+ const resolution = resolveLocator(layout, step.locator);
260
+ if (resolution.status === "found") {
261
+ target = {
262
+ point: resolution.point,
263
+ ...(resolution.element.bounds === undefined
264
+ ? {}
265
+ : { bounds: resolution.element.bounds })
266
+ };
267
+ report.locator = {
268
+ status: "found",
269
+ matchedBy: resolution.matchedBy,
270
+ fallbackUsed: false
271
+ };
272
+ }
273
+ else {
274
+ const annotatedPath = stepPath(index, "fallback-annotated.png");
275
+ const fallback = await this.fallbackResolver.resolve(step, this.options.artifacts.path(annotatedPath), signal);
276
+ if (fallback.status !== "found") {
277
+ const code = fallback.status === "failed"
278
+ ? fallback.code
279
+ : resolution.code;
280
+ const message = fallback.status === "failed"
281
+ ? fallback.message
282
+ : resolution.message;
283
+ report.locator = {
284
+ status: "failed",
285
+ fallbackUsed: fallback.status === "failed"
286
+ && fallback.label !== undefined
287
+ && fallback.annotatedScreenshotPath !== undefined,
288
+ ...(fallback.status === "failed" && fallback.label !== undefined
289
+ ? { fallbackLabel: fallback.label }
290
+ : {}),
291
+ ...(fallback.status === "failed"
292
+ && fallback.annotatedScreenshotPath !== undefined
293
+ ? { annotatedScreenshotPath: annotatedPath }
294
+ : {}),
295
+ message
296
+ };
297
+ return fail(code, message);
298
+ }
299
+ target = targetForPoint(fallback.point);
300
+ report.locator = {
301
+ status: "found",
302
+ fallbackUsed: true,
303
+ fallbackLabel: fallback.label,
304
+ annotatedScreenshotPath: annotatedPath
305
+ };
306
+ }
307
+ }
308
+ if (step.action === "inputText"
309
+ && this.options.requireFocusedInput === true
310
+ && !hasExactlyOneEnabledFocusedElement(layout)) {
311
+ return fail("ACTION_FAILED", "inputText requires exactly one enabled focused Layout element");
312
+ }
313
+ if (this.options.generatedReplayPolicy === true) {
314
+ try {
315
+ await this.assertGeneratedForeground(step.activity.before, "ACTIVITY_BEFORE_MISMATCH", signal);
316
+ }
317
+ catch (error) {
318
+ return fail("ACTIVITY_BEFORE_MISMATCH", errorMessage(error));
319
+ }
320
+ }
321
+ const action = await this.actionExecutor.execute(step, target, signal);
322
+ if (action.status === "failed") {
323
+ return fail(action.code, action.message);
324
+ }
325
+ const idle = await this.idleWaiter.waitUntilIdle(this.options.idle, signal);
326
+ report.idle = idle.status === "timeout"
327
+ ? {
328
+ status: "timeout",
329
+ polls: idle.polls,
330
+ lastDiff: [...idle.lastDiff]
331
+ }
332
+ : {
333
+ status: idle.status,
334
+ polls: idle.polls
335
+ };
336
+ if (idle.status === "cancelled") {
337
+ return finish("cancelled");
338
+ }
339
+ if (idle.status === "timeout") {
340
+ await this.options.artifacts.writeJson(stepPath(index, "layout-diff.json"), idle.lastDiff);
341
+ return fail(idle.code, "Layout did not become stable before timeout");
342
+ }
343
+ }
344
+ const pid = await this.primaryPid(identity);
345
+ if (pid === null) {
346
+ return fail("APP_CRASHED", "App process is no longer running");
347
+ }
348
+ if (generatedPid !== undefined
349
+ && generatedPid !== null
350
+ && pid !== generatedPid) {
351
+ return fail("APP_CRASHED", "Generated replay process identity changed");
352
+ }
353
+ let after;
354
+ try {
355
+ after = this.options.generatedReplayPolicy === true
356
+ ? (await this.generatedForeground(step.activity.after, "ACTIVITY_AFTER_MISMATCH", signal)).activity
357
+ : await this.options.adb.currentActivity(identity);
358
+ }
359
+ catch (error) {
360
+ if (error !== null
361
+ && typeof error === "object"
362
+ && "code" in error
363
+ && error.code === "ACTIVITY_AFTER_MISMATCH") {
364
+ return fail("ACTIVITY_AFTER_MISMATCH", errorMessage(error));
365
+ }
366
+ throw error;
367
+ }
368
+ activityReport.after = {
369
+ status: after === step.activity.after ? "passed" : "failed",
370
+ expected: step.activity.after,
371
+ actual: after
372
+ };
373
+ if (after !== step.activity.after) {
374
+ return fail("ACTIVITY_AFTER_MISMATCH", `Expected Activity ${step.activity.after}, found ${after}`);
375
+ }
376
+ if (step.expect !== undefined) {
377
+ const expectation = await this.expectationEvaluator.evaluate(step.expect, {
378
+ packageName: this.options.packageName,
379
+ deviceSerial: this.options.deviceSerial,
380
+ stepStartedAt: startedAt
381
+ }, signal, generatedPid === undefined || generatedPid === null
382
+ ? {}
383
+ : {
384
+ activity: (input) => this.observeExpectedActivity(undefined, generatedPid, input),
385
+ layout: (input) => this.observeExpectedLayout(step.activity.after, generatedPid, input)
386
+ });
387
+ if (expectation.status === "cancelled") {
388
+ report.expectation = {
389
+ type: expectation.type,
390
+ status: "notRun"
391
+ };
392
+ return finish("cancelled");
393
+ }
394
+ report.expectation = expectation.status === "passed"
395
+ ? {
396
+ type: expectation.type,
397
+ status: "passed"
398
+ }
399
+ : {
400
+ type: expectation.type,
401
+ status: "failed",
402
+ code: expectation.code,
403
+ message: expectation.message
404
+ };
405
+ if (expectation.status === "failed") {
406
+ return fail(expectation.code, expectation.message);
407
+ }
408
+ if (this.options.generatedReplayPolicy === true) {
409
+ const expectedActivity = step.expect.type === "activity"
410
+ ? step.expect.value
411
+ : step.activity.after;
412
+ try {
413
+ await this.assertGeneratedForeground(expectedActivity, "ACTIVITY_AFTER_MISMATCH", signal);
414
+ const guardedPid = await this.primaryPid(identity);
415
+ if (guardedPid !== generatedPid) {
416
+ return await fail("ACTIVITY_AFTER_MISMATCH", "Generated replay process changed after Expect");
417
+ }
418
+ }
419
+ catch (error) {
420
+ return fail("ACTIVITY_AFTER_MISMATCH", errorMessage(error));
421
+ }
422
+ }
423
+ }
424
+ return finish("passed");
425
+ }
426
+ }
427
+ function errorMessage(error) {
428
+ return error instanceof Error ? error.message : String(error);
429
+ }
@@ -0,0 +1,44 @@
1
+ import type { TapHoundConfig } from "../../domain/config.js";
2
+ import type { Journey } from "../../domain/journey.js";
3
+ import { type TapHoundReport } from "../../domain/report.js";
4
+ import type { AdbPort } from "../../ports/adb.js";
5
+ import type { AndroidCliPort } from "../../ports/android-cli.js";
6
+ import type { ArtifactStore } from "../../ports/artifact-store.js";
7
+ import type { Clock } from "../../ports/clock.js";
8
+ import type { ReportWriter } from "../report/report-writer.js";
9
+ import { type StepRunResult, type StepRunnerOptions } from "./step-runner.js";
10
+ export interface VerifyInput {
11
+ config: TapHoundConfig;
12
+ journey: Journey;
13
+ projectRoot: string;
14
+ deviceSerial: string;
15
+ toolVersions: Record<string, string>;
16
+ requireFocusedInput?: boolean | undefined;
17
+ generatedReplayPolicy?: boolean | undefined;
18
+ signal?: AbortSignal | undefined;
19
+ }
20
+ export interface StepRunnerLike {
21
+ run: (step: Journey["steps"][number], index: number, signal?: AbortSignal) => Promise<StepRunResult>;
22
+ }
23
+ export interface VerifyRuntimeDependencies {
24
+ androidCli: AndroidCliPort;
25
+ adb: AdbPort;
26
+ clock: Clock;
27
+ artifactStore: ArtifactStore;
28
+ reportWriter: Pick<ReportWriter, "writeAndPublish">;
29
+ now: () => Date;
30
+ createRunId: () => string;
31
+ createStepRunner?: ((options: StepRunnerOptions) => StepRunnerLike) | undefined;
32
+ }
33
+ export interface VerifyResult {
34
+ status: "passed" | "failed" | "error";
35
+ exitCode: 0 | 1 | 2 | 3 | 4;
36
+ report: TapHoundReport;
37
+ reportPath: string;
38
+ summaryPath: string;
39
+ }
40
+ export declare class VerifyRuntime {
41
+ private readonly dependencies;
42
+ constructor(dependencies: VerifyRuntimeDependencies);
43
+ verify(input: VerifyInput): Promise<VerifyResult>;
44
+ }