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.
- package/LICENSE +202 -0
- package/README.md +200 -0
- package/README.zh-CN.md +200 -0
- package/assets/brand/taphound-mark.svg +9 -0
- package/assets/skills/taphound-ai-journey/GUIDE.md +862 -0
- package/assets/skills/taphound-ai-journey/SKILL.md +377 -0
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +526 -0
- package/assets/skills/taphound-ai-journey/prompts/check-completion.md +52 -0
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +109 -0
- package/assets/skills/taphound-ai-journey/schemas/observe-output.json +42 -0
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +81 -0
- package/assets/skills/taphound-ai-journey/schemas/proposed-step-envelope.json +267 -0
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +49 -0
- package/dist/adapters/adb/adb-adapter.d.ts +23 -0
- package/dist/adapters/adb/adb-adapter.js +218 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +11 -0
- package/dist/adapters/android-cli/android-cli-adapter.js +64 -0
- package/dist/adapters/android-cli/layout-parser.d.ts +3 -0
- package/dist/adapters/android-cli/layout-parser.js +174 -0
- package/dist/adapters/clock/system-clock.d.ts +5 -0
- package/dist/adapters/clock/system-clock.js +13 -0
- package/dist/adapters/filesystem/artifact-store.d.ts +4 -0
- package/dist/adapters/filesystem/artifact-store.js +73 -0
- package/dist/adapters/filesystem/generation-meta-writer.d.ts +16 -0
- package/dist/adapters/filesystem/generation-meta-writer.js +34 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +68 -0
- package/dist/adapters/filesystem/generation-session-store.js +1603 -0
- package/dist/adapters/filesystem/journey-writer.d.ts +16 -0
- package/dist/adapters/filesystem/journey-writer.js +34 -0
- package/dist/adapters/filesystem/project-bound-file.d.ts +6 -0
- package/dist/adapters/filesystem/project-bound-file.js +240 -0
- package/dist/adapters/filesystem/project-file-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-file-inspector.js +198 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +10 -0
- package/dist/adapters/filesystem/skill-installer.js +39 -0
- package/dist/adapters/process/node-process-runner.d.ts +8 -0
- package/dist/adapters/process/node-process-runner.js +155 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.d.ts +55 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +213 -0
- package/dist/adapters/prompt/inquirer-init-prompt.d.ts +32 -0
- package/dist/adapters/prompt/inquirer-init-prompt.js +55 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +47 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +157 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +61 -0
- package/dist/application/assertion/expectation-evaluator.js +207 -0
- package/dist/application/collector/logcat-collector.d.ts +39 -0
- package/dist/application/collector/logcat-collector.js +107 -0
- package/dist/application/context/context-validator.d.ts +27 -0
- package/dist/application/context/context-validator.js +120 -0
- package/dist/application/doctor/doctor-service.d.ts +35 -0
- package/dist/application/doctor/doctor-service.js +178 -0
- package/dist/application/generation/focused-input.d.ts +2 -0
- package/dist/application/generation/focused-input.js +9 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +76 -0
- package/dist/application/generation/generation-confirmation-service.js +383 -0
- package/dist/application/generation/generation-finalizer.d.ts +67 -0
- package/dist/application/generation/generation-finalizer.js +646 -0
- package/dist/application/generation/generation-publisher.d.ts +46 -0
- package/dist/application/generation/generation-publisher.js +178 -0
- package/dist/application/generation/generation-starter.d.ts +31 -0
- package/dist/application/generation/generation-starter.js +102 -0
- package/dist/application/generation/generation-step-executor.d.ts +52 -0
- package/dist/application/generation/generation-step-executor.js +668 -0
- package/dist/application/generation/proposed-step-validator.d.ts +11 -0
- package/dist/application/generation/proposed-step-validator.js +88 -0
- package/dist/application/generation/risk-evaluator.d.ts +9 -0
- package/dist/application/generation/risk-evaluator.js +14 -0
- package/dist/application/generation/runtime-observer.d.ts +39 -0
- package/dist/application/generation/runtime-observer.js +157 -0
- package/dist/application/init/init-service.d.ts +17 -0
- package/dist/application/init/init-service.js +44 -0
- package/dist/application/interaction/action-executor.d.ts +25 -0
- package/dist/application/interaction/action-executor.js +103 -0
- package/dist/application/interaction/fallback-resolver.d.ts +24 -0
- package/dist/application/interaction/fallback-resolver.js +48 -0
- package/dist/application/interaction/scroll-to-executor.d.ts +40 -0
- package/dist/application/interaction/scroll-to-executor.js +191 -0
- package/dist/application/locator/locator-resolver.d.ts +20 -0
- package/dist/application/locator/locator-resolver.js +89 -0
- package/dist/application/project/project-describer.d.ts +14 -0
- package/dist/application/project/project-describer.js +8 -0
- package/dist/application/recorder/locator-selector.d.ts +10 -0
- package/dist/application/recorder/locator-selector.js +80 -0
- package/dist/application/recorder/recorder-service.d.ts +41 -0
- package/dist/application/recorder/recorder-service.js +331 -0
- package/dist/application/report/report-writer.d.ts +10 -0
- package/dist/application/report/report-writer.js +39 -0
- package/dist/application/runtime/activity-waiter.d.ts +33 -0
- package/dist/application/runtime/activity-waiter.js +73 -0
- package/dist/application/runtime/launch-failure.d.ts +2 -0
- package/dist/application/runtime/launch-failure.js +20 -0
- package/dist/application/runtime/process-waiter.d.ts +27 -0
- package/dist/application/runtime/process-waiter.js +73 -0
- package/dist/application/runtime/step-runner.d.ts +50 -0
- package/dist/application/runtime/step-runner.js +429 -0
- package/dist/application/runtime/verify-runtime.d.ts +44 -0
- package/dist/application/runtime/verify-runtime.js +350 -0
- package/dist/application/wait/idle-waiter.d.ts +30 -0
- package/dist/application/wait/idle-waiter.js +90 -0
- package/dist/cli/commands/context.d.ts +3 -0
- package/dist/cli/commands/context.js +94 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +51 -0
- package/dist/cli/commands/generation.d.ts +3 -0
- package/dist/cli/commands/generation.js +518 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.js +104 -0
- package/dist/cli/commands/project.d.ts +3 -0
- package/dist/cli/commands/project.js +58 -0
- package/dist/cli/commands/record.d.ts +3 -0
- package/dist/cli/commands/record.js +88 -0
- package/dist/cli/commands/verify.d.ts +3 -0
- package/dist/cli/commands/verify.js +106 -0
- package/dist/cli/dependencies.d.ts +66 -0
- package/dist/cli/dependencies.js +204 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +73 -0
- package/dist/cli/output.d.ts +16 -0
- package/dist/cli/output.js +22 -0
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +43 -0
- package/dist/domain/activity.d.ts +7 -0
- package/dist/domain/activity.js +65 -0
- package/dist/domain/app-process.d.ts +7 -0
- package/dist/domain/app-process.js +10 -0
- package/dist/domain/config.d.ts +15 -0
- package/dist/domain/config.js +17 -0
- package/dist/domain/failure.d.ts +4 -0
- package/dist/domain/failure.js +43 -0
- package/dist/domain/generation.d.ts +912 -0
- package/dist/domain/generation.js +342 -0
- package/dist/domain/init.d.ts +42 -0
- package/dist/domain/init.js +114 -0
- package/dist/domain/journey.d.ts +673 -0
- package/dist/domain/journey.js +109 -0
- package/dist/domain/layout.d.ts +35 -0
- package/dist/domain/layout.js +33 -0
- package/dist/domain/locator.d.ts +8 -0
- package/dist/domain/locator.js +5 -0
- package/dist/domain/project-context.d.ts +101 -0
- package/dist/domain/project-context.js +64 -0
- package/dist/domain/proposed-step.d.ts +341 -0
- package/dist/domain/proposed-step.js +87 -0
- package/dist/domain/report.d.ts +349 -0
- package/dist/domain/report.js +135 -0
- package/dist/domain/runtime-snapshot.d.ts +16 -0
- package/dist/domain/runtime-snapshot.js +46 -0
- package/dist/ports/adb.d.ts +42 -0
- package/dist/ports/adb.js +1 -0
- package/dist/ports/android-cli.d.ts +21 -0
- package/dist/ports/android-cli.js +1 -0
- package/dist/ports/artifact-store.d.ts +12 -0
- package/dist/ports/artifact-store.js +1 -0
- package/dist/ports/clock.d.ts +4 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/generation-meta-writer.d.ts +10 -0
- package/dist/ports/generation-meta-writer.js +1 -0
- package/dist/ports/generation-prompt.d.ts +19 -0
- package/dist/ports/generation-prompt.js +6 -0
- package/dist/ports/generation-session-store.d.ts +32 -0
- package/dist/ports/generation-session-store.js +26 -0
- package/dist/ports/init-prompt.d.ts +8 -0
- package/dist/ports/init-prompt.js +6 -0
- package/dist/ports/journey-writer.d.ts +10 -0
- package/dist/ports/journey-writer.js +1 -0
- package/dist/ports/process-runner.d.ts +32 -0
- package/dist/ports/process-runner.js +1 -0
- package/dist/ports/project-bound-file.d.ts +8 -0
- package/dist/ports/project-bound-file.js +1 -0
- package/dist/ports/project-file-inspector.d.ts +33 -0
- package/dist/ports/project-file-inspector.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +29 -0
- package/dist/ports/recorder-prompt.js +1 -0
- package/dist/ports/skill-installer.d.ts +8 -0
- package/dist/ports/skill-installer.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { confirm, input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
2
|
+
const defaultPrompts = {
|
|
3
|
+
select: async (config) => select({
|
|
4
|
+
message: config.message,
|
|
5
|
+
choices: [...config.choices]
|
|
6
|
+
}),
|
|
7
|
+
input: async (config) => input({
|
|
8
|
+
message: config.message,
|
|
9
|
+
...(config.validate === undefined ? {} : { validate: config.validate })
|
|
10
|
+
}),
|
|
11
|
+
confirm: async (config) => confirm(config),
|
|
12
|
+
number: async (config) => numberPrompt(config)
|
|
13
|
+
};
|
|
14
|
+
const ACTIONS = [
|
|
15
|
+
"click",
|
|
16
|
+
"longClick",
|
|
17
|
+
"inputText",
|
|
18
|
+
"swipe",
|
|
19
|
+
"scrollTo",
|
|
20
|
+
"back",
|
|
21
|
+
"wait",
|
|
22
|
+
"finish",
|
|
23
|
+
"cancel"
|
|
24
|
+
];
|
|
25
|
+
function selectedString(value, allowed) {
|
|
26
|
+
if (typeof value !== "string"
|
|
27
|
+
|| (allowed !== undefined && !allowed.includes(value))) {
|
|
28
|
+
throw new Error("Prompt returned an invalid selection");
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function selectedNumber(value, minimum, maximum = Number.POSITIVE_INFINITY) {
|
|
33
|
+
if (typeof value !== "number"
|
|
34
|
+
|| !Number.isFinite(value)
|
|
35
|
+
|| value < minimum
|
|
36
|
+
|| value > maximum) {
|
|
37
|
+
throw new Error("Prompt returned an invalid number");
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
export class InquirerRecorderPrompt {
|
|
42
|
+
prompts;
|
|
43
|
+
diagnostics;
|
|
44
|
+
constructor(prompts = defaultPrompts, diagnostics = process.stderr) {
|
|
45
|
+
this.prompts = prompts;
|
|
46
|
+
this.diagnostics = diagnostics;
|
|
47
|
+
}
|
|
48
|
+
async selectAction() {
|
|
49
|
+
const value = await this.prompts.select({
|
|
50
|
+
message: "Choose the next TapHound Action",
|
|
51
|
+
choices: ACTIONS.map((action) => ({ name: action, value: action }))
|
|
52
|
+
});
|
|
53
|
+
return selectedString(value, ACTIONS);
|
|
54
|
+
}
|
|
55
|
+
async selectTarget(choices) {
|
|
56
|
+
const value = await this.prompts.select({
|
|
57
|
+
message: "Choose a Layout target",
|
|
58
|
+
choices: choices.map((choice) => ({
|
|
59
|
+
name: choice.label,
|
|
60
|
+
value: choice.id
|
|
61
|
+
}))
|
|
62
|
+
});
|
|
63
|
+
return selectedString(value, choices.map((choice) => choice.id));
|
|
64
|
+
}
|
|
65
|
+
async inputText() {
|
|
66
|
+
const value = await this.prompts.input({
|
|
67
|
+
message: "Text to enter",
|
|
68
|
+
validate: (answer) => answer.length > 0 || "Text must not be empty"
|
|
69
|
+
});
|
|
70
|
+
return selectedString(value);
|
|
71
|
+
}
|
|
72
|
+
async selectSwipeDirection() {
|
|
73
|
+
const directions = ["up", "down", "left", "right"];
|
|
74
|
+
const value = await this.prompts.select({
|
|
75
|
+
message: "Swipe direction",
|
|
76
|
+
choices: directions.map((direction) => ({
|
|
77
|
+
name: direction,
|
|
78
|
+
value: direction
|
|
79
|
+
}))
|
|
80
|
+
});
|
|
81
|
+
return selectedString(value, directions);
|
|
82
|
+
}
|
|
83
|
+
async longClickDuration() {
|
|
84
|
+
return selectedNumber(await this.prompts.number({
|
|
85
|
+
message: "Long-click duration (ms)",
|
|
86
|
+
default: 800,
|
|
87
|
+
min: 1
|
|
88
|
+
}), 1);
|
|
89
|
+
}
|
|
90
|
+
async swipeOptions() {
|
|
91
|
+
const distancePercent = selectedNumber(await this.prompts.number({
|
|
92
|
+
message: "Swipe distance (0–1)",
|
|
93
|
+
default: 0.6,
|
|
94
|
+
min: 0.01,
|
|
95
|
+
max: 1
|
|
96
|
+
}), 0.01, 1);
|
|
97
|
+
const durationMs = selectedNumber(await this.prompts.number({
|
|
98
|
+
message: "Swipe duration (ms)",
|
|
99
|
+
default: 300,
|
|
100
|
+
min: 1
|
|
101
|
+
}), 1);
|
|
102
|
+
return { distancePercent, durationMs };
|
|
103
|
+
}
|
|
104
|
+
async selectFallbackLabel(annotatedScreenshotPath) {
|
|
105
|
+
const enabled = await this.prompts.confirm({
|
|
106
|
+
message: `Add annotated-label fallback from ${annotatedScreenshotPath}?`,
|
|
107
|
+
default: false
|
|
108
|
+
});
|
|
109
|
+
if (enabled !== true) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const label = await this.prompts.input({
|
|
113
|
+
message: "Android CLI annotated label",
|
|
114
|
+
validate: (value) => /^#\d+$/.test(value) || "Use the #number format"
|
|
115
|
+
});
|
|
116
|
+
const selected = selectedString(label);
|
|
117
|
+
if (!/^#\d+$/.test(selected)) {
|
|
118
|
+
throw new Error("Fallback label must use the #number format");
|
|
119
|
+
}
|
|
120
|
+
return selected;
|
|
121
|
+
}
|
|
122
|
+
notifyFailure(message) {
|
|
123
|
+
this.diagnostics.write(`TapHound: ${message}\n`);
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
}
|
|
126
|
+
async selectScrollContainer(choices) {
|
|
127
|
+
const value = await this.prompts.select({
|
|
128
|
+
message: "Choose the scrollable container",
|
|
129
|
+
choices: choices.map((choice) => ({ name: choice.label, value: choice.id }))
|
|
130
|
+
});
|
|
131
|
+
return selectedString(value, choices.map((choice) => choice.id));
|
|
132
|
+
}
|
|
133
|
+
async scrollTargetDecision(choices) {
|
|
134
|
+
const scrollMore = "__scroll_more__";
|
|
135
|
+
const cancel = "__cancel__";
|
|
136
|
+
const value = await this.prompts.select({
|
|
137
|
+
message: "Select the target once visible, or scroll again",
|
|
138
|
+
choices: [
|
|
139
|
+
...choices.map((choice) => ({ name: choice.label, value: choice.id })),
|
|
140
|
+
{ name: "Scroll again", value: scrollMore },
|
|
141
|
+
{ name: "Cancel scrollTo", value: cancel }
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
const selected = selectedString(value, [
|
|
145
|
+
...choices.map((choice) => choice.id),
|
|
146
|
+
scrollMore,
|
|
147
|
+
cancel
|
|
148
|
+
]);
|
|
149
|
+
if (selected === scrollMore) {
|
|
150
|
+
return { kind: "scrollMore" };
|
|
151
|
+
}
|
|
152
|
+
if (selected === cancel) {
|
|
153
|
+
return { kind: "cancel" };
|
|
154
|
+
}
|
|
155
|
+
return { kind: "select", id: selected };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { FailureCode } from "../../domain/failure.js";
|
|
2
|
+
import type { Expectation } from "../../domain/journey.js";
|
|
3
|
+
import type { LayoutElement } from "../../domain/layout.js";
|
|
4
|
+
import type { AdbPort } from "../../ports/adb.js";
|
|
5
|
+
import type { AndroidCliPort } from "../../ports/android-cli.js";
|
|
6
|
+
import type { Clock } from "../../ports/clock.js";
|
|
7
|
+
import type { LogcatCollector } from "../collector/logcat-collector.js";
|
|
8
|
+
export interface ExpectationContext {
|
|
9
|
+
packageName: string;
|
|
10
|
+
deviceSerial: string;
|
|
11
|
+
stepStartedAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ExpectationObservationInput {
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
signal?: AbortSignal | undefined;
|
|
16
|
+
}
|
|
17
|
+
export type ActivityExpectationObservation = {
|
|
18
|
+
status: "observed";
|
|
19
|
+
activity: string;
|
|
20
|
+
} | {
|
|
21
|
+
status: "failed";
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export type LayoutExpectationObservation = {
|
|
25
|
+
status: "observed";
|
|
26
|
+
layout: readonly LayoutElement[];
|
|
27
|
+
} | {
|
|
28
|
+
status: "failed";
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
export interface ExpectationObservationBoundary {
|
|
32
|
+
activity?: (input: ExpectationObservationInput) => Promise<ActivityExpectationObservation>;
|
|
33
|
+
layout?: (input: ExpectationObservationInput) => Promise<LayoutExpectationObservation>;
|
|
34
|
+
}
|
|
35
|
+
export type ExpectationResult = {
|
|
36
|
+
status: "passed";
|
|
37
|
+
type: Expectation["type"];
|
|
38
|
+
durationMs: number;
|
|
39
|
+
actual?: string | undefined;
|
|
40
|
+
matchedLine?: string | undefined;
|
|
41
|
+
} | {
|
|
42
|
+
status: "failed";
|
|
43
|
+
type: Expectation["type"];
|
|
44
|
+
code: Extract<FailureCode, "EXPECT_ACTIVITY_FAILED" | "EXPECT_ELEMENT_FAILED" | "EXPECT_LOGCAT_FAILED">;
|
|
45
|
+
message: string;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
actual?: string | undefined;
|
|
48
|
+
} | {
|
|
49
|
+
status: "cancelled";
|
|
50
|
+
type: Expectation["type"];
|
|
51
|
+
durationMs: number;
|
|
52
|
+
};
|
|
53
|
+
export declare class ExpectationEvaluator {
|
|
54
|
+
private readonly adb;
|
|
55
|
+
private readonly androidCli;
|
|
56
|
+
private readonly logcat;
|
|
57
|
+
private readonly clock;
|
|
58
|
+
private readonly pollIntervalMs;
|
|
59
|
+
constructor(adb: AdbPort, androidCli: AndroidCliPort, logcat: LogcatCollector, clock: Clock, pollIntervalMs?: number);
|
|
60
|
+
evaluate(expectation: Expectation, context: ExpectationContext, signal?: AbortSignal, observations?: ExpectationObservationBoundary): Promise<ExpectationResult>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { resolveLocator } from "../locator/locator-resolver.js";
|
|
2
|
+
function hasElement(elements, locator) {
|
|
3
|
+
return resolveLocator(elements, locator, { requireEnabled: false }).status
|
|
4
|
+
=== "found";
|
|
5
|
+
}
|
|
6
|
+
function matchesLogcat(line, expectation, pattern) {
|
|
7
|
+
if (line.tag !== expectation.tag
|
|
8
|
+
|| (expectation.level !== undefined
|
|
9
|
+
&& line.level !== expectation.level)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const content = line.message ?? line.raw;
|
|
13
|
+
return typeof pattern === "string"
|
|
14
|
+
? content.includes(pattern)
|
|
15
|
+
: pattern.test(content);
|
|
16
|
+
}
|
|
17
|
+
function isAborted(signal) {
|
|
18
|
+
return signal?.aborted === true;
|
|
19
|
+
}
|
|
20
|
+
export class ExpectationEvaluator {
|
|
21
|
+
adb;
|
|
22
|
+
androidCli;
|
|
23
|
+
logcat;
|
|
24
|
+
clock;
|
|
25
|
+
pollIntervalMs;
|
|
26
|
+
constructor(adb, androidCli, logcat, clock, pollIntervalMs = 100) {
|
|
27
|
+
this.adb = adb;
|
|
28
|
+
this.androidCli = androidCli;
|
|
29
|
+
this.logcat = logcat;
|
|
30
|
+
this.clock = clock;
|
|
31
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
32
|
+
}
|
|
33
|
+
async evaluate(expectation, context, signal, observations = {}) {
|
|
34
|
+
const startedAt = this.clock.now();
|
|
35
|
+
const logPattern = expectation.type === "logcat"
|
|
36
|
+
? (expectation.match === "regex"
|
|
37
|
+
? new RegExp(expectation.pattern)
|
|
38
|
+
: expectation.pattern)
|
|
39
|
+
: undefined;
|
|
40
|
+
let actual;
|
|
41
|
+
for (;;) {
|
|
42
|
+
if (isAborted(signal)) {
|
|
43
|
+
return {
|
|
44
|
+
status: "cancelled",
|
|
45
|
+
type: expectation.type,
|
|
46
|
+
durationMs: this.clock.now() - startedAt
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const commandTimeoutMs = Math.max(1, expectation.timeoutMs - (this.clock.now() - startedAt));
|
|
50
|
+
try {
|
|
51
|
+
switch (expectation.type) {
|
|
52
|
+
case "activity": {
|
|
53
|
+
const observation = observations.activity === undefined
|
|
54
|
+
? {
|
|
55
|
+
status: "observed",
|
|
56
|
+
activity: await this.adb.currentActivity({
|
|
57
|
+
packageName: context.packageName,
|
|
58
|
+
deviceSerial: context.deviceSerial,
|
|
59
|
+
...(signal === undefined ? {} : { signal }),
|
|
60
|
+
timeoutMs: commandTimeoutMs
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
: await observations.activity({
|
|
64
|
+
timeoutMs: commandTimeoutMs,
|
|
65
|
+
...(signal === undefined ? {} : { signal })
|
|
66
|
+
});
|
|
67
|
+
if (isAborted(signal)) {
|
|
68
|
+
return {
|
|
69
|
+
status: "cancelled",
|
|
70
|
+
type: expectation.type,
|
|
71
|
+
durationMs: this.clock.now() - startedAt
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (observation.status === "failed") {
|
|
75
|
+
return {
|
|
76
|
+
status: "failed",
|
|
77
|
+
type: expectation.type,
|
|
78
|
+
code: "EXPECT_ACTIVITY_FAILED",
|
|
79
|
+
message: observation.message,
|
|
80
|
+
durationMs: this.clock.now() - startedAt
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
actual = observation.activity;
|
|
84
|
+
if (actual === expectation.value) {
|
|
85
|
+
return {
|
|
86
|
+
status: "passed",
|
|
87
|
+
type: expectation.type,
|
|
88
|
+
durationMs: this.clock.now() - startedAt,
|
|
89
|
+
actual
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "element": {
|
|
95
|
+
const observation = observations.layout === undefined
|
|
96
|
+
? {
|
|
97
|
+
status: "observed",
|
|
98
|
+
layout: await this.androidCli.layout({
|
|
99
|
+
deviceSerial: context.deviceSerial,
|
|
100
|
+
...(signal === undefined ? {} : { signal }),
|
|
101
|
+
timeoutMs: commandTimeoutMs
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
: await observations.layout({
|
|
105
|
+
timeoutMs: commandTimeoutMs,
|
|
106
|
+
...(signal === undefined ? {} : { signal })
|
|
107
|
+
});
|
|
108
|
+
if (isAborted(signal)) {
|
|
109
|
+
return {
|
|
110
|
+
status: "cancelled",
|
|
111
|
+
type: expectation.type,
|
|
112
|
+
durationMs: this.clock.now() - startedAt
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (observation.status === "failed") {
|
|
116
|
+
return {
|
|
117
|
+
status: "failed",
|
|
118
|
+
type: expectation.type,
|
|
119
|
+
code: "EXPECT_ELEMENT_FAILED",
|
|
120
|
+
message: observation.message,
|
|
121
|
+
durationMs: this.clock.now() - startedAt
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (hasElement(observation.layout, expectation.locator)) {
|
|
125
|
+
return {
|
|
126
|
+
status: "passed",
|
|
127
|
+
type: expectation.type,
|
|
128
|
+
durationMs: this.clock.now() - startedAt
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "logcat": {
|
|
134
|
+
const matched = this.logcat
|
|
135
|
+
.linesBetween(context.stepStartedAt, this.clock.now())
|
|
136
|
+
.find((line) => matchesLogcat(line, expectation, logPattern ?? ""));
|
|
137
|
+
if (matched !== undefined) {
|
|
138
|
+
return {
|
|
139
|
+
status: "passed",
|
|
140
|
+
type: expectation.type,
|
|
141
|
+
durationMs: this.clock.now() - startedAt,
|
|
142
|
+
matchedLine: matched.raw
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
const elapsed = this.clock.now() - startedAt;
|
|
151
|
+
if (isAborted(signal)) {
|
|
152
|
+
return {
|
|
153
|
+
status: "cancelled",
|
|
154
|
+
type: expectation.type,
|
|
155
|
+
durationMs: elapsed
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (elapsed >= expectation.timeoutMs) {
|
|
159
|
+
const codes = {
|
|
160
|
+
activity: "EXPECT_ACTIVITY_FAILED",
|
|
161
|
+
element: "EXPECT_ELEMENT_FAILED",
|
|
162
|
+
logcat: "EXPECT_LOGCAT_FAILED"
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
status: "failed",
|
|
166
|
+
type: expectation.type,
|
|
167
|
+
code: codes[expectation.type],
|
|
168
|
+
message: `${expectation.type} Expect command exceeded its timeout`,
|
|
169
|
+
durationMs: elapsed,
|
|
170
|
+
...(actual === undefined ? {} : { actual })
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
const elapsed = this.clock.now() - startedAt;
|
|
176
|
+
if (elapsed >= expectation.timeoutMs) {
|
|
177
|
+
const codes = {
|
|
178
|
+
activity: "EXPECT_ACTIVITY_FAILED",
|
|
179
|
+
element: "EXPECT_ELEMENT_FAILED",
|
|
180
|
+
logcat: "EXPECT_LOGCAT_FAILED"
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
status: "failed",
|
|
184
|
+
type: expectation.type,
|
|
185
|
+
code: codes[expectation.type],
|
|
186
|
+
message: `${expectation.type} Expect did not match before timeout`,
|
|
187
|
+
durationMs: elapsed,
|
|
188
|
+
...(actual === undefined ? {} : { actual })
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const remaining = expectation.timeoutMs - elapsed;
|
|
192
|
+
try {
|
|
193
|
+
await this.clock.sleep(Math.min(this.pollIntervalMs, remaining), signal);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (isAborted(signal)) {
|
|
197
|
+
return {
|
|
198
|
+
status: "cancelled",
|
|
199
|
+
type: expectation.type,
|
|
200
|
+
durationMs: this.clock.now() - startedAt
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AdbPort } from "../../ports/adb.js";
|
|
2
|
+
import type { Clock } from "../../ports/clock.js";
|
|
3
|
+
import type { CommandResult } from "../../ports/process-runner.js";
|
|
4
|
+
export type LogLevel = "V" | "D" | "I" | "W" | "E" | "F" | "A";
|
|
5
|
+
export interface LogcatLine {
|
|
6
|
+
receivedAt: number;
|
|
7
|
+
raw: string;
|
|
8
|
+
pid?: number | undefined;
|
|
9
|
+
tid?: number | undefined;
|
|
10
|
+
level?: LogLevel | undefined;
|
|
11
|
+
tag?: string | undefined;
|
|
12
|
+
message?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface LogcatMetadata {
|
|
15
|
+
deviceSerial: string;
|
|
16
|
+
pids: readonly number[];
|
|
17
|
+
}
|
|
18
|
+
export interface StartLogcatOptions {
|
|
19
|
+
deviceSerial: string;
|
|
20
|
+
signal?: AbortSignal | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare class LogcatCollector {
|
|
23
|
+
private readonly adb;
|
|
24
|
+
private readonly clock;
|
|
25
|
+
private readonly collected;
|
|
26
|
+
private readonly stderr;
|
|
27
|
+
private readonly scopedPids;
|
|
28
|
+
private running?;
|
|
29
|
+
private streamMetadata?;
|
|
30
|
+
private stopPromise?;
|
|
31
|
+
constructor(adb: AdbPort, clock: Clock);
|
|
32
|
+
start(options: StartLogcatOptions): Promise<void>;
|
|
33
|
+
scopeToPids(pids: readonly number[]): void;
|
|
34
|
+
metadata(): LogcatMetadata;
|
|
35
|
+
lines(): readonly LogcatLine[];
|
|
36
|
+
diagnosticLines(): readonly string[];
|
|
37
|
+
linesBetween(startedAt: number, finishedAt: number): LogcatLine[];
|
|
38
|
+
stop(): Promise<CommandResult>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+(\d+)\s+(\d+)\s+([VDIWEFA])\s+([^:]+):\s?(.*)$/;
|
|
2
|
+
function parseLine(raw, receivedAt) {
|
|
3
|
+
const match = THREADTIME.exec(raw);
|
|
4
|
+
if (match === null) {
|
|
5
|
+
return { receivedAt, raw };
|
|
6
|
+
}
|
|
7
|
+
const [, pid, tid, level, tag, message] = match;
|
|
8
|
+
if (pid === undefined
|
|
9
|
+
|| tid === undefined
|
|
10
|
+
|| level === undefined
|
|
11
|
+
|| tag === undefined
|
|
12
|
+
|| message === undefined) {
|
|
13
|
+
return { receivedAt, raw };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
receivedAt,
|
|
17
|
+
raw,
|
|
18
|
+
pid: Number(pid),
|
|
19
|
+
tid: Number(tid),
|
|
20
|
+
level: level,
|
|
21
|
+
tag: tag.trim(),
|
|
22
|
+
message
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function startupFailure(result) {
|
|
26
|
+
return result.stderr.trim()
|
|
27
|
+
|| result.spawnError
|
|
28
|
+
|| (result.cancelled
|
|
29
|
+
? "Logcat startup was cancelled"
|
|
30
|
+
: result.timedOut
|
|
31
|
+
? "Logcat startup timed out"
|
|
32
|
+
: `Logcat exited during startup with code ${String(result.exitCode)}`);
|
|
33
|
+
}
|
|
34
|
+
export class LogcatCollector {
|
|
35
|
+
adb;
|
|
36
|
+
clock;
|
|
37
|
+
collected = [];
|
|
38
|
+
stderr = [];
|
|
39
|
+
scopedPids = new Set();
|
|
40
|
+
running;
|
|
41
|
+
streamMetadata;
|
|
42
|
+
stopPromise;
|
|
43
|
+
constructor(adb, clock) {
|
|
44
|
+
this.adb = adb;
|
|
45
|
+
this.clock = clock;
|
|
46
|
+
}
|
|
47
|
+
async start(options) {
|
|
48
|
+
if (this.running !== undefined) {
|
|
49
|
+
throw new Error("Logcat collector already started");
|
|
50
|
+
}
|
|
51
|
+
const logcatOptions = {
|
|
52
|
+
deviceSerial: options.deviceSerial,
|
|
53
|
+
onStdoutLine: (line) => {
|
|
54
|
+
this.collected.push(parseLine(line, this.clock.now()));
|
|
55
|
+
},
|
|
56
|
+
onStderrLine: (line) => {
|
|
57
|
+
this.stderr.push(line);
|
|
58
|
+
},
|
|
59
|
+
...(options.signal === undefined ? {} : { signal: options.signal })
|
|
60
|
+
};
|
|
61
|
+
this.running = this.adb.startLogcat(logcatOptions);
|
|
62
|
+
this.streamMetadata = { deviceSerial: options.deviceSerial };
|
|
63
|
+
const startupResult = await this.running.started;
|
|
64
|
+
if (startupResult !== undefined) {
|
|
65
|
+
throw new Error(startupFailure(startupResult));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
scopeToPids(pids) {
|
|
69
|
+
if (this.streamMetadata === undefined) {
|
|
70
|
+
throw new Error("Logcat collector has not started");
|
|
71
|
+
}
|
|
72
|
+
for (const pid of pids) {
|
|
73
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
74
|
+
throw new Error("Logcat PID must be a positive integer");
|
|
75
|
+
}
|
|
76
|
+
this.scopedPids.add(pid);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
metadata() {
|
|
80
|
+
if (this.streamMetadata === undefined) {
|
|
81
|
+
throw new Error("Logcat collector has not started");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
deviceSerial: this.streamMetadata.deviceSerial,
|
|
85
|
+
pids: [...this.scopedPids].sort((left, right) => left - right)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
lines() {
|
|
89
|
+
if (this.scopedPids.size === 0) {
|
|
90
|
+
return [...this.collected];
|
|
91
|
+
}
|
|
92
|
+
return this.collected.filter((line) => line.pid === undefined || this.scopedPids.has(line.pid));
|
|
93
|
+
}
|
|
94
|
+
diagnosticLines() {
|
|
95
|
+
return [...this.stderr];
|
|
96
|
+
}
|
|
97
|
+
linesBetween(startedAt, finishedAt) {
|
|
98
|
+
return this.lines().filter((line) => line.receivedAt >= startedAt && line.receivedAt <= finishedAt);
|
|
99
|
+
}
|
|
100
|
+
stop() {
|
|
101
|
+
if (this.running === undefined) {
|
|
102
|
+
throw new Error("Logcat collector has not started");
|
|
103
|
+
}
|
|
104
|
+
this.stopPromise ??= this.running.stop();
|
|
105
|
+
return this.stopPromise;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TapHoundConfig } from "../../domain/config.js";
|
|
2
|
+
import { type ProjectFileInspector } from "../../ports/project-file-inspector.js";
|
|
3
|
+
export declare const MAX_CONTEXT_EVIDENCE_BYTES: number;
|
|
4
|
+
export type ContextValidationReasonCode = "CONTEXT_SCHEMA_INVALID" | "CONTEXT_IDENTITY_MISMATCH" | "PROJECT_ROOT_UNREADABLE" | "PROJECT_ROOT_NOT_DIRECTORY" | "EVIDENCE_SECRET_PATH" | "EVIDENCE_NOT_FOUND" | "EVIDENCE_UNREADABLE" | "EVIDENCE_NOT_FILE" | "EVIDENCE_PATH_ESCAPE" | "EVIDENCE_CHANGED_IDENTITY" | "EVIDENCE_TOO_LARGE" | "EVIDENCE_HASH_MISMATCH";
|
|
5
|
+
export interface ContextValidationReason {
|
|
6
|
+
code: ContextValidationReasonCode;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export type ContextValidationResult = {
|
|
10
|
+
status: "valid";
|
|
11
|
+
} | {
|
|
12
|
+
status: "stale";
|
|
13
|
+
reason: ContextValidationReason;
|
|
14
|
+
} | {
|
|
15
|
+
status: "invalid";
|
|
16
|
+
reason: ContextValidationReason;
|
|
17
|
+
};
|
|
18
|
+
export interface ContextValidationInput {
|
|
19
|
+
context: unknown;
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
config: TapHoundConfig;
|
|
22
|
+
}
|
|
23
|
+
export declare class ContextValidator {
|
|
24
|
+
private readonly files;
|
|
25
|
+
constructor(files: ProjectFileInspector);
|
|
26
|
+
readonly validate: (input: ContextValidationInput) => Promise<ContextValidationResult>;
|
|
27
|
+
}
|