@1aboveio/skills 1.0.17 → 1.0.18
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/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/resources.json +167 -139
- package/README.md +1 -1
- package/package.json +5 -4
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +14 -12
- package/skills/app-dev/android-development/references/diagnosis.md +4 -6
- package/skills/app-dev/android-development/references/gotchas.md +3 -9
- package/skills/app-dev/android-development/references/init.md +5 -2
- package/skills/app-dev/android-development/references/setup.md +2 -1
- package/skills/app-dev/android-development/references/ui-journeys.md +12 -127
- package/skills/app-dev/android-development/scripts/failure_capture.py +23 -84
- package/skills/app-dev/android-development/scripts/init.mjs +2 -1
- package/skills/app-dev/android-development/scripts/maestro_failure.py +43 -387
- package/skills/app-dev/android-development/scripts/test_maestro_failure.py +113 -424
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/maestro/LICENSE +3 -0
- package/skills/app-dev/maestro/SKILL.md +112 -0
- package/skills/app-dev/maestro/references/android-connection.md +54 -0
- package/skills/app-dev/maestro/references/authoring.md +113 -0
- package/skills/app-dev/maestro/references/diagnosis.md +50 -0
- package/skills/app-dev/maestro/references/evidence.md +74 -0
- package/skills/app-dev/maestro/references/journeys.md +100 -0
- package/skills/app-dev/maestro/references/migration.md +35 -0
- package/skills/app-dev/maestro/release-check.mjs +95 -0
- package/skills/app-dev/maestro/scripts/failure_record.py +110 -0
- package/skills/app-dev/maestro/scripts/maestro_failure.py +481 -0
- package/skills/app-dev/maestro/scripts/test_maestro_failure.py +710 -0
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/xai-search/SKILL.md +1 -1
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-report/SKILL.md +1 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/engineering/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/agent-catalog.mjs +44 -11
- package/skills/engineering/harness-runtime/contracts.md +1 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
|
@@ -1,461 +1,150 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Thin forwarding coverage for the Android ``maestro_failure`` compatibility entry point.
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
(
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The converter and its full suite now live in the sibling ``maestro`` skill
|
|
4
|
+
(``skills/fullstack/maestro/scripts``). These tests only prove that the old Android path
|
|
5
|
+
still reaches that converter and still shares its record model, including when both skill
|
|
6
|
+
folders are copied into a staged package.
|
|
7
7
|
|
|
8
8
|
Run directly (`python3 test_maestro_failure.py`) or through
|
|
9
|
-
`
|
|
9
|
+
`maestro-failure.test.mjs`, which is what CI discovers.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import contextlib
|
|
13
|
-
import io
|
|
14
12
|
import json
|
|
15
13
|
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
16
|
import sys
|
|
17
17
|
import tempfile
|
|
18
18
|
import unittest
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"sequenceNumber": 1,
|
|
59
|
-
"depth": 0,
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
FAILED_RECEIPT = {
|
|
64
|
-
"command": {
|
|
65
|
-
"assertConditionCommand": {
|
|
66
|
-
"condition": {"visible": {"text": "Thank you", "optional": False}},
|
|
67
|
-
"optional": False,
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"metadata": {
|
|
71
|
-
"status": "FAILED",
|
|
72
|
-
"timestamp": 1700000001700,
|
|
73
|
-
"duration": 30000,
|
|
74
|
-
"sequenceNumber": 2,
|
|
75
|
-
"depth": 0,
|
|
76
|
-
"error": {
|
|
77
|
-
"message": 'Assertion is false: "Thank you" is visible',
|
|
78
|
-
"debugMessage": "Screen hierarchy has no match",
|
|
79
|
-
},
|
|
80
|
-
"artifacts": [
|
|
81
|
-
{"type": "SCREEN_HIERARCHY", "path": "screen-hierarchy/step-003-assertCondition.json"},
|
|
82
|
-
{"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
}
|
|
20
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
FORWARDER = os.path.join(HERE, "maestro_failure.py")
|
|
22
|
+
MAESTRO_SCRIPTS = os.path.normpath(os.path.join(HERE, "..", "..", "maestro", "scripts"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_python(arguments, cwd):
|
|
26
|
+
"""Run a Python command with no inherited PYTHONPATH, so imports come from `arguments`."""
|
|
27
|
+
environment = {key: value for key, value in os.environ.items() if key != "PYTHONPATH"}
|
|
28
|
+
return subprocess.run(
|
|
29
|
+
[sys.executable, *arguments],
|
|
30
|
+
cwd=cwd,
|
|
31
|
+
env=environment,
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=120,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_failing_flow(root):
|
|
39
|
+
"""A Flow folder whose only step failed, with no artifacts beside it."""
|
|
40
|
+
flow_dir = os.path.join(root, "session", "checkout_flow")
|
|
41
|
+
os.makedirs(flow_dir)
|
|
42
|
+
with open(os.path.join(flow_dir, "commands.json"), "w", encoding="utf-8") as stream:
|
|
43
|
+
json.dump(
|
|
44
|
+
[
|
|
45
|
+
{
|
|
46
|
+
"command": {"tapOnElement": {"selector": {"text": "Pay"}, "label": "Tap Pay"}},
|
|
47
|
+
"metadata": {
|
|
48
|
+
"status": "FAILED",
|
|
49
|
+
"timestamp": 1700000000000,
|
|
50
|
+
"sequenceNumber": 0,
|
|
51
|
+
"error": {"message": "Element not found: Pay"},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
stream,
|
|
56
|
+
)
|
|
57
|
+
return flow_dir
|
|
86
58
|
|
|
87
59
|
|
|
88
|
-
class
|
|
60
|
+
class ForwarderCase(unittest.TestCase):
|
|
89
61
|
def setUp(self):
|
|
90
62
|
self.directory = tempfile.TemporaryDirectory()
|
|
91
63
|
self.root = self.directory.name
|
|
92
|
-
self.
|
|
64
|
+
self.elsewhere = os.path.join(self.root, "elsewhere")
|
|
65
|
+
os.makedirs(self.elsewhere)
|
|
93
66
|
|
|
94
67
|
def tearDown(self):
|
|
95
68
|
self.directory.cleanup()
|
|
96
69
|
|
|
97
|
-
def write(self, relative, payload):
|
|
98
|
-
path = os.path.join(self.root, relative)
|
|
99
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
100
|
-
if isinstance(payload, bytes):
|
|
101
|
-
with open(path, "wb") as stream:
|
|
102
|
-
stream.write(payload)
|
|
103
|
-
else:
|
|
104
|
-
with open(path, "w", encoding="utf-8") as stream:
|
|
105
|
-
stream.write(payload)
|
|
106
|
-
return path
|
|
107
|
-
|
|
108
|
-
def write_json(self, relative, payload):
|
|
109
|
-
return self.write(relative, json.dumps(payload, indent=2))
|
|
110
|
-
|
|
111
|
-
def flow(self, steps, name=FLOW, session=SESSION, artifacts=None, manifest=True):
|
|
112
|
-
"""A Flow folder with `steps` in commands.json and the optional artifact files."""
|
|
113
|
-
base = os.path.join(session, name) if session else name
|
|
114
|
-
self.write_json(os.path.join(base, "commands.json"), steps)
|
|
115
|
-
if manifest:
|
|
116
|
-
self.write_json(
|
|
117
|
-
os.path.join(base, "manifest.json"),
|
|
118
|
-
{
|
|
119
|
-
"$schema": "https://storage.googleapis.com/maestro-schemas/artifact-manifest/v1.schema.json",
|
|
120
|
-
"entries": [
|
|
121
|
-
{"kind": "COMMAND_METADATA", "format": "JSON", "relativePath": "commands.json"}
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
)
|
|
125
|
-
for relative, payload in (artifacts or {}).items():
|
|
126
|
-
self.write(os.path.join(base, relative), payload)
|
|
127
|
-
return os.path.join(self.root, base)
|
|
128
|
-
|
|
129
|
-
def failing_flow(self, name=FLOW, session=SESSION):
|
|
130
|
-
"""The worked example: a launch and tap that passed, then a failed assertion."""
|
|
131
|
-
return self.flow(
|
|
132
|
-
[LAUNCHED, TAPPED, FAILED_RECEIPT],
|
|
133
|
-
name=name,
|
|
134
|
-
session=session,
|
|
135
|
-
artifacts={
|
|
136
|
-
"screenshots/step-003-assertCondition.png": b"\x89PNG\r\n\x1a\n",
|
|
137
|
-
"screen-hierarchy/step-003-assertCondition.json": '{"attributes": {"text": "Pay now"}}',
|
|
138
|
-
"logs/device-logcat.txt": "07-31 E ActivityManager: no match\n",
|
|
139
|
-
},
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
def run_convert(self, maestro_dir, **options):
|
|
143
|
-
options.setdefault("stream", self.output)
|
|
144
|
-
return convert(maestro_dir, **options)
|
|
145
|
-
|
|
146
|
-
def cli(self, *argv):
|
|
147
|
-
"""Run main() and keep its stderr out of the test output."""
|
|
148
|
-
with contextlib.redirect_stderr(io.StringIO()):
|
|
149
|
-
return main(list(argv))
|
|
150
|
-
|
|
151
70
|
def record(self, evidence_dir):
|
|
152
|
-
|
|
153
|
-
self.assertTrue(os.path.isfile(path), path)
|
|
154
|
-
with open(path, encoding="utf-8") as stream:
|
|
71
|
+
with open(os.path.join(evidence_dir, "failure.json"), encoding="utf-8") as stream:
|
|
155
72
|
return json.load(stream)
|
|
156
73
|
|
|
157
|
-
def lines(self):
|
|
158
|
-
return [line for line in self.output.getvalue().splitlines() if line]
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class FailingFlowTest(MaestroOutputCase):
|
|
162
|
-
def test_the_failing_step_becomes_the_record_and_the_line(self):
|
|
163
|
-
flow_dir = self.failing_flow()
|
|
164
|
-
failure = self.run_convert(flow_dir)
|
|
165
|
-
|
|
166
|
-
self.assertIsNotNone(failure)
|
|
167
|
-
record = self.record(flow_dir)
|
|
168
|
-
self.assertEqual(list(record), list(RECORD_KEYS))
|
|
169
|
-
self.assertEqual(record["case"], FLOW)
|
|
170
|
-
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
171
|
-
self.assertEqual(
|
|
172
|
-
record["error"],
|
|
173
|
-
'Assertion is false: "Thank you" is visible: Screen hierarchy has no match',
|
|
174
|
-
)
|
|
175
|
-
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
176
|
-
self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
|
|
177
|
-
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
178
|
-
self.assertEqual(record["evidenceDir"], flow_dir)
|
|
179
|
-
self.assertEqual(record["cleanupErrors"], [])
|
|
180
|
-
for name in (record["screenshot"], record["uiDump"], record["logcatTail"]):
|
|
181
|
-
self.assertTrue(os.path.isfile(os.path.join(flow_dir, name)), name)
|
|
182
|
-
|
|
183
|
-
lines = self.lines()
|
|
184
|
-
self.assertEqual(len(lines), 1)
|
|
185
|
-
self.assertTrue(lines[0].startswith(NATIVE_FAILURE_PREFIX))
|
|
186
|
-
self.assertEqual(
|
|
187
|
-
json.loads(lines[0][len(NATIVE_FAILURE_PREFIX) :]),
|
|
188
|
-
{
|
|
189
|
-
"case": FLOW,
|
|
190
|
-
"step": 'Assert condition "Thank you"',
|
|
191
|
-
"error": record["error"],
|
|
192
|
-
"evidenceDir": flow_dir,
|
|
193
|
-
},
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
def test_an_explicit_case_name_is_used(self):
|
|
197
|
-
flow_dir = self.failing_flow()
|
|
198
|
-
self.run_convert(flow_dir, case=CASE)
|
|
199
|
-
self.assertEqual(self.record(flow_dir)["case"], CASE)
|
|
200
|
-
|
|
201
|
-
def test_a_labelled_step_uses_the_label(self):
|
|
202
|
-
labelled = json.loads(json.dumps(FAILED_RECEIPT))
|
|
203
|
-
labelled["command"] = {
|
|
204
|
-
"tapOnElement": {
|
|
205
|
-
"selector": {"text": "Pay", "optional": False},
|
|
206
|
-
"label": "Tap Pay",
|
|
207
|
-
"optional": False,
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
flow_dir = self.flow([TAPPED, labelled])
|
|
211
|
-
self.run_convert(flow_dir)
|
|
212
|
-
self.assertEqual(self.record(flow_dir)["step"], "Tap Pay")
|
|
213
|
-
|
|
214
|
-
def test_the_failing_step_wins_over_a_later_one(self):
|
|
215
|
-
later = json.loads(json.dumps(FAILED_RECEIPT))
|
|
216
|
-
later["metadata"]["timestamp"] = 1700000009000
|
|
217
|
-
later["metadata"]["sequenceNumber"] = 3
|
|
218
|
-
later["metadata"]["error"] = {"message": "second failure"}
|
|
219
|
-
later["metadata"]["artifacts"] = [
|
|
220
|
-
{"type": "SCREENSHOT", "path": "screenshots/step-004-tapOnElement.png"}
|
|
221
|
-
]
|
|
222
|
-
flow_dir = self.flow(
|
|
223
|
-
[TAPPED, FAILED_RECEIPT, later],
|
|
224
|
-
artifacts={"screenshots/step-003-assertCondition.png": b"png"},
|
|
225
|
-
)
|
|
226
|
-
self.run_convert(flow_dir)
|
|
227
|
-
|
|
228
|
-
record = self.record(flow_dir)
|
|
229
|
-
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
230
|
-
self.assertIn("Assertion is false", record["error"])
|
|
231
|
-
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class PassingFlowTest(MaestroOutputCase):
|
|
235
|
-
def test_a_passing_flow_writes_nothing(self):
|
|
236
|
-
flow_dir = self.flow(
|
|
237
|
-
[LAUNCHED, TAPPED],
|
|
238
|
-
artifacts={"logs/device-logcat.txt": "07-31 I ActivityManager: clean\n"},
|
|
239
|
-
)
|
|
240
|
-
self.assertIsNone(self.run_convert(flow_dir))
|
|
241
|
-
self.assertFalse(os.path.exists(os.path.join(flow_dir, RECORD_NAME)))
|
|
242
|
-
self.assertEqual(self.output.getvalue(), "")
|
|
243
|
-
|
|
244
|
-
def test_a_flow_with_no_steps_writes_nothing(self):
|
|
245
|
-
flow_dir = self.flow([])
|
|
246
|
-
self.assertIsNone(self.run_convert(flow_dir))
|
|
247
|
-
self.assertEqual(self.output.getvalue(), "")
|
|
248
|
-
|
|
249
|
-
def test_a_folder_without_commands_json_is_not_converted(self):
|
|
250
|
-
self.write(os.path.join(SESSION, FLOW, "manifest.json"), "{}")
|
|
251
|
-
with self.assertRaises(MaestroOutputError):
|
|
252
|
-
self.run_convert(os.path.join(self.root, SESSION))
|
|
253
|
-
self.assertEqual(self.cli("--maestro-dir", os.path.join(self.root, SESSION)), 2)
|
|
254
|
-
|
|
255
|
-
def test_a_missing_folder_is_a_usage_error(self):
|
|
256
|
-
self.assertEqual(self.cli("--maestro-dir", os.path.join(self.root, "absent")), 2)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
class ExistingRecordTest(MaestroOutputCase):
|
|
260
|
-
def test_an_existing_record_is_kept_and_its_line_is_printed(self):
|
|
261
|
-
flow_dir = self.failing_flow()
|
|
262
|
-
kept = {
|
|
263
|
-
"case": CASE,
|
|
264
|
-
"step": "tap Pay",
|
|
265
|
-
"error": "RuntimeError: earlier failure",
|
|
266
|
-
"screenshot": None,
|
|
267
|
-
"uiDump": None,
|
|
268
|
-
"logcatTail": None,
|
|
269
|
-
"evidenceDir": flow_dir,
|
|
270
|
-
"cleanupErrors": [],
|
|
271
|
-
}
|
|
272
|
-
path = self.write_json(os.path.join(SESSION, FLOW, RECORD_NAME), kept)
|
|
273
|
-
|
|
274
|
-
failure = self.run_convert(flow_dir)
|
|
275
74
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
self.assertIn("earlier failure", stream.read())
|
|
280
|
-
self.assertEqual(self.lines(), [failure.native_failure_line()])
|
|
75
|
+
class ForwarderTest(ForwarderCase):
|
|
76
|
+
def test_the_old_command_line_still_converts(self):
|
|
77
|
+
flow_dir = write_failing_flow(self.root)
|
|
281
78
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
285
|
-
kept = {"case": CASE, "step": None, "error": "RuntimeError: earlier failure", "cleanupErrors": []}
|
|
286
|
-
self.write_json(os.path.join("run-evidence", RECORD_NAME), kept)
|
|
287
|
-
|
|
288
|
-
self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
289
|
-
|
|
290
|
-
self.assertEqual(self.record(evidence_dir)["error"], "RuntimeError: earlier failure")
|
|
291
|
-
# The first failure owns the folder, so no Maestro artifact was copied into it.
|
|
292
|
-
self.assertFalse(os.path.exists(os.path.join(evidence_dir, "screenshots")))
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
class MissingOptionalFilesTest(MaestroOutputCase):
|
|
296
|
-
def test_absent_artifacts_are_null_and_noted_without_losing_the_error(self):
|
|
297
|
-
flow_dir = self.flow([TAPPED, FAILED_RECEIPT], manifest=False)
|
|
298
|
-
self.run_convert(flow_dir)
|
|
299
|
-
|
|
300
|
-
record = self.record(flow_dir)
|
|
301
|
-
self.assertIsNone(record["screenshot"])
|
|
302
|
-
self.assertIsNone(record["uiDump"])
|
|
303
|
-
self.assertIsNone(record["logcatTail"])
|
|
304
|
-
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
305
|
-
self.assertIn("Assertion is false", record["error"])
|
|
306
|
-
self.assertEqual(len(record["cleanupErrors"]), 3)
|
|
307
|
-
self.assertTrue(record["cleanupErrors"][0].startswith("screenshot: "))
|
|
308
|
-
self.assertTrue(record["cleanupErrors"][1].startswith("uiDump: "))
|
|
309
|
-
self.assertTrue(record["cleanupErrors"][2].startswith("logcatTail: "))
|
|
310
|
-
|
|
311
|
-
def test_the_error_survives_a_listed_artifact_that_is_not_on_disk(self):
|
|
312
|
-
broken = json.loads(json.dumps(FAILED_RECEIPT))
|
|
313
|
-
broken["metadata"]["artifacts"] = [
|
|
314
|
-
{"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"}
|
|
315
|
-
]
|
|
316
|
-
flow_dir = self.flow([broken], artifacts={"logs/device-logcat.txt": "log\n"})
|
|
317
|
-
self.run_convert(flow_dir)
|
|
318
|
-
|
|
319
|
-
record = self.record(flow_dir)
|
|
320
|
-
self.assertIsNone(record["screenshot"])
|
|
321
|
-
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
322
|
-
self.assertIn(
|
|
323
|
-
"screenshots/step-003-assertCondition.png is not in the Maestro output",
|
|
324
|
-
" ".join(record["cleanupErrors"]),
|
|
79
|
+
result = run_python(
|
|
80
|
+
[FORWARDER, "--maestro-dir", flow_dir, "--case", "checkout"], cwd=self.elsewhere
|
|
325
81
|
)
|
|
326
|
-
self.assertIn("Assertion is false", record["error"])
|
|
327
|
-
|
|
328
|
-
def test_an_unreadable_commands_json_is_not_silently_a_pass(self):
|
|
329
|
-
flow_dir = self.flow([FAILED_RECEIPT])
|
|
330
|
-
self.write(os.path.join(SESSION, FLOW, "commands.json"), "{not json")
|
|
331
|
-
with self.assertRaises(MaestroOutputError):
|
|
332
|
-
self.run_convert(flow_dir)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
class SessionFolderTest(MaestroOutputCase):
|
|
336
|
-
def test_the_session_folder_finds_the_failing_flow(self):
|
|
337
|
-
self.flow([LAUNCHED, TAPPED], name="login_flow")
|
|
338
|
-
failing_flow = self.failing_flow()
|
|
339
|
-
session_dir = os.path.join(self.root, SESSION)
|
|
340
|
-
|
|
341
|
-
failure = self.run_convert(session_dir)
|
|
342
|
-
|
|
343
|
-
self.assertEqual(failure.evidenceDir, failing_flow)
|
|
344
|
-
self.assertEqual(self.record(failing_flow)["case"], FLOW)
|
|
345
|
-
|
|
346
|
-
def test_the_earliest_failing_step_decides(self):
|
|
347
|
-
earlier = json.loads(json.dumps(FAILED_RECEIPT))
|
|
348
|
-
earlier["metadata"]["timestamp"] = 1700000000500
|
|
349
|
-
earlier["metadata"]["error"] = {"message": "earlier failure"}
|
|
350
|
-
earlier_dir = self.flow([LAUNCHED, earlier], name="login_flow")
|
|
351
|
-
later_dir = self.failing_flow(name="checkout_flow")
|
|
352
|
-
|
|
353
|
-
self.run_convert(os.path.join(self.root, SESSION))
|
|
354
|
-
|
|
355
|
-
self.assertEqual(self.record(earlier_dir)["error"], "earlier failure")
|
|
356
|
-
self.assertFalse(os.path.exists(os.path.join(later_dir, RECORD_NAME)))
|
|
357
|
-
|
|
358
|
-
def test_the_output_root_above_the_session_folder_works(self):
|
|
359
|
-
failing_flow = self.failing_flow()
|
|
360
|
-
self.run_convert(self.root)
|
|
361
|
-
self.assertTrue(os.path.isfile(os.path.join(failing_flow, RECORD_NAME)))
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
class EvidenceDirTest(MaestroOutputCase):
|
|
365
|
-
def test_an_override_evidence_dir_holds_copies_and_relative_names(self):
|
|
366
|
-
flow_dir = self.failing_flow()
|
|
367
|
-
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
368
|
-
|
|
369
|
-
failure = self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
370
|
-
|
|
371
|
-
record = self.record(evidence_dir)
|
|
372
|
-
self.assertEqual(record["evidenceDir"], evidence_dir)
|
|
373
|
-
self.assertEqual(failure.evidenceDir, evidence_dir)
|
|
374
|
-
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
375
|
-
self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
|
|
376
|
-
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
377
|
-
for name in (record["screenshot"], record["uiDump"], record["logcatTail"]):
|
|
378
|
-
self.assertTrue(os.path.isfile(os.path.join(evidence_dir, name)), name)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
class DocumentedShapeTest(MaestroOutputCase):
|
|
382
|
-
def test_the_documented_flat_entry_shape_is_accepted(self):
|
|
383
|
-
flow_dir = self.flow(
|
|
384
|
-
[
|
|
385
|
-
{"command": "launchApp", "status": "COMPLETED", "duration": 1200, "sequenceNumber": 0},
|
|
386
|
-
{
|
|
387
|
-
"command": "tapOn: Pay",
|
|
388
|
-
"status": "FAILED",
|
|
389
|
-
"duration": 300,
|
|
390
|
-
"sequenceNumber": 1,
|
|
391
|
-
"error": "Element not found: Pay",
|
|
392
|
-
"artifacts": ["screenshots/step-002-tapOn.png"],
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
artifacts={
|
|
396
|
-
"screenshots/step-002-tapOn.png": b"png",
|
|
397
|
-
"logs/device-logcat.txt": "log\n",
|
|
398
|
-
},
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
self.run_convert(flow_dir)
|
|
402
82
|
|
|
83
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
84
|
+
self.assertEqual(len([line for line in result.stdout.splitlines() if line]), 1)
|
|
403
85
|
record = self.record(flow_dir)
|
|
404
|
-
self.assertEqual(record["
|
|
86
|
+
self.assertEqual(record["case"], "checkout")
|
|
87
|
+
self.assertEqual(record["step"], "Tap Pay")
|
|
405
88
|
self.assertEqual(record["error"], "Element not found: Pay")
|
|
406
|
-
self.assertEqual(record["screenshot"], "screenshots/step-002-tapOn.png")
|
|
407
|
-
self.assertEqual(record["uiDump"], None)
|
|
408
|
-
|
|
409
|
-
def test_a_step_named_screenshot_is_found_without_an_artifacts_list(self):
|
|
410
|
-
unlisted = json.loads(json.dumps(FAILED_RECEIPT))
|
|
411
|
-
del unlisted["metadata"]["artifacts"]
|
|
412
|
-
unlisted["metadata"]["sequenceNumber"] = 1
|
|
413
|
-
flow_dir = self.flow(
|
|
414
|
-
[unlisted],
|
|
415
|
-
artifacts={
|
|
416
|
-
"screenshots/step-002-assertCondition.png": b"png",
|
|
417
|
-
"screen-hierarchy/step-002-assertCondition.json": "{}",
|
|
418
|
-
"logs/device-logcat.txt": "log\n",
|
|
419
|
-
},
|
|
420
|
-
)
|
|
421
89
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
[
|
|
431
|
-
|
|
90
|
+
def test_the_old_import_api_returns_the_shared_record_model(self):
|
|
91
|
+
flow_dir = write_failing_flow(self.root)
|
|
92
|
+
out = os.path.join(self.elsewhere, "line.txt")
|
|
93
|
+
program = (
|
|
94
|
+
"import sys\n"
|
|
95
|
+
"sys.path.insert(0, sys.argv[1])\n"
|
|
96
|
+
"import maestro_failure\n"
|
|
97
|
+
"with open(sys.argv[3], 'w', encoding='utf-8') as stream:\n"
|
|
98
|
+
" failure = maestro_failure.convert(sys.argv[2], stream=stream)\n"
|
|
99
|
+
"print(type(failure).__module__)\n"
|
|
432
100
|
)
|
|
433
|
-
self.run_convert(flow_dir)
|
|
434
|
-
self.assertEqual(self.record(flow_dir)["logcatTail"], "logs/device-simulator.log")
|
|
435
101
|
|
|
102
|
+
result = run_python(["-c", program, HERE, flow_dir, out], cwd=self.elsewhere)
|
|
436
103
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
self.assertEqual(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
"
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
104
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
105
|
+
self.assertEqual(result.stdout.strip(), "failure_record")
|
|
106
|
+
self.assertEqual(self.record(flow_dir)["step"], "Tap Pay")
|
|
107
|
+
with open(out, encoding="utf-8") as stream:
|
|
108
|
+
self.assertTrue(stream.read().startswith("NATIVE_FAILURE "))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class StagedSiblingLayoutTest(ForwarderCase):
|
|
112
|
+
"""Both skill folders copied into a staged package, with no repository beside them."""
|
|
113
|
+
|
|
114
|
+
def copy_scripts(self, staged, skill, source):
|
|
115
|
+
scripts = os.path.join(staged, "skills", skill, "scripts")
|
|
116
|
+
os.makedirs(scripts)
|
|
117
|
+
for name in sorted(os.listdir(source)):
|
|
118
|
+
if name.endswith((".py", ".mjs")):
|
|
119
|
+
shutil.copyfile(os.path.join(source, name), os.path.join(scripts, name))
|
|
120
|
+
return scripts
|
|
121
|
+
|
|
122
|
+
def test_a_copied_pair_still_forwards_and_shares_the_record_model(self):
|
|
123
|
+
flow_dir = write_failing_flow(self.root)
|
|
124
|
+
with tempfile.TemporaryDirectory() as staged:
|
|
125
|
+
android_scripts = self.copy_scripts(staged, "android-development", HERE)
|
|
126
|
+
self.copy_scripts(staged, "maestro", MAESTRO_SCRIPTS)
|
|
127
|
+
work = os.path.join(staged, "work")
|
|
128
|
+
os.makedirs(work)
|
|
129
|
+
|
|
130
|
+
result = run_python(
|
|
131
|
+
[os.path.join(android_scripts, "maestro_failure.py"), "--maestro-dir", flow_dir],
|
|
132
|
+
cwd=work,
|
|
133
|
+
)
|
|
134
|
+
program = (
|
|
135
|
+
"import sys\n"
|
|
136
|
+
"sys.path.insert(0, sys.argv[1])\n"
|
|
137
|
+
"import failure_capture, failure_record\n"
|
|
138
|
+
"assert failure_capture.Failure is failure_record.Failure\n"
|
|
139
|
+
"assert failure_capture.read_record is failure_record.read_record\n"
|
|
140
|
+
"print(failure_capture.RECORD_NAME)\n"
|
|
141
|
+
)
|
|
142
|
+
model = run_python(["-c", program, android_scripts], cwd=work)
|
|
455
143
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
144
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
145
|
+
self.assertEqual(self.record(flow_dir)["step"], "Tap Pay")
|
|
146
|
+
self.assertEqual(model.returncode, 0, model.stderr)
|
|
147
|
+
self.assertEqual(model.stdout.strip(), "failure.json")
|
|
459
148
|
|
|
460
149
|
|
|
461
150
|
if __name__ == "__main__":
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: app-debug
|
|
3
3
|
description: "Diagnose runtime failures in deployed Cloud Run services — 5xx/422 errors, tracebacks, Next.js SSR/hydration or Server Action failures, DB/Secret Manager connection errors, cold-start timeouts, container OOM. NOT for build or deploy failures (cloud-debug)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.49.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# App Debug
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: better-auth
|
|
3
3
|
description: "Add or debug Better Auth for Next.js App Router: login, sessions, protected routes, middleware, OAuth (including Feishu/Lark), cookies, Prisma, and Auth.js migration."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.49.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Better Auth for Next.js
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: data-catalog
|
|
3
3
|
description: "Column definitions for warehouse tables, written inside the Spark/PySpark or SQL script that builds each table (CATALOG_TABLE / CATALOG_FIELDS, or a `-- @field` comment block), with a generated markdown catalog in docs/catalog/ and a CI check that fails a PR whose catalog is stale. Use when writing or changing a table script that adds, renames, removes, or changes the meaning of an output column or feature; when asked what a warehouse column or feature means; to set a repo up ('init the data catalog', 'generate the data dictionary', '字段字典', 'add catalog CI'); or to audit existing SQL or Spark scripts for undocumented columns. NOT for implementing or tuning the Spark job itself (pyspark), DAG wiring (airflow), or general workflow YAML (github-actions)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.49.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Data catalog
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: data-quality
|
|
3
3
|
description: "Check a published warehouse table's data against its data-catalog definitions with read-only SELECTs: columns never filled or stuck at one value, counts below zero, ratios outside 0–1, declared values that never appear and undeclared values that do. Then classify each finding as a wrong definition, wrong code, or wrong data. Use when asked whether a table's data is right, to profile or sanity-check a dwd/ads table, to find dead or constant columns, to check a column's values against its documented list, after a pipeline change ships, or when a data-catalog definition and the data might disagree. NOT for validating a Spark change before it ships (pyspark canary and reconciliation), writing definitions (data-catalog), or monitoring dashboards."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.49.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Data quality
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ios-foundation
|
|
3
3
|
description: "Set up and prove an Expo / React Native native iOS workspace from an assigned or fresh isolated workspace to a manual Login smoke on an owned iOS Simulator: isolated workspace and private caches, Xcode/runtime/CocoaPods preflight, frozen install with `pod install --deployment`, platform-scoped prebuild, an ad-hoc-signed Debug simulator build, a private simulator by exact UDID, Metro on a free port, a disposable PostgreSQL backend with owned cleanup, and the evidence record (setup proof only; feature work reports the smoke result). Use for 'set up iOS', 'build the iOS app', 'run the app on the iOS simulator', 'add iOS to the React Native app', 'prove the iOS build', or when a pod install, xcodebuild, simulator boot, Metro connection or Keychain session fails. NOT for Android (android-development), signing, TestFlight or App Store release, or general workflow YAML (github-actions)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.49.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# iOS foundation
|