@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.
Files changed (91) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +167 -139
  3. package/README.md +1 -1
  4. package/package.json +5 -4
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-development/SKILL.md +14 -12
  7. package/skills/app-dev/android-development/references/diagnosis.md +4 -6
  8. package/skills/app-dev/android-development/references/gotchas.md +3 -9
  9. package/skills/app-dev/android-development/references/init.md +5 -2
  10. package/skills/app-dev/android-development/references/setup.md +2 -1
  11. package/skills/app-dev/android-development/references/ui-journeys.md +12 -127
  12. package/skills/app-dev/android-development/scripts/failure_capture.py +23 -84
  13. package/skills/app-dev/android-development/scripts/init.mjs +2 -1
  14. package/skills/app-dev/android-development/scripts/maestro_failure.py +43 -387
  15. package/skills/app-dev/android-development/scripts/test_maestro_failure.py +113 -424
  16. package/skills/app-dev/app-debug/SKILL.md +1 -1
  17. package/skills/app-dev/better-auth/SKILL.md +1 -1
  18. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  19. package/skills/app-dev/data-quality/SKILL.md +1 -1
  20. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  21. package/skills/app-dev/maestro/LICENSE +3 -0
  22. package/skills/app-dev/maestro/SKILL.md +112 -0
  23. package/skills/app-dev/maestro/references/android-connection.md +54 -0
  24. package/skills/app-dev/maestro/references/authoring.md +113 -0
  25. package/skills/app-dev/maestro/references/diagnosis.md +50 -0
  26. package/skills/app-dev/maestro/references/evidence.md +74 -0
  27. package/skills/app-dev/maestro/references/journeys.md +100 -0
  28. package/skills/app-dev/maestro/references/migration.md +35 -0
  29. package/skills/app-dev/maestro/release-check.mjs +95 -0
  30. package/skills/app-dev/maestro/scripts/failure_record.py +110 -0
  31. package/skills/app-dev/maestro/scripts/maestro_failure.py +481 -0
  32. package/skills/app-dev/maestro/scripts/test_maestro_failure.py +710 -0
  33. package/skills/app-dev/monorepo/SKILL.md +1 -1
  34. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  35. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  36. package/skills/app-dev/pyspark/SKILL.md +1 -1
  37. package/skills/app-dev/python-backend/SKILL.md +1 -1
  38. package/skills/app-dev/shadcn/SKILL.md +1 -1
  39. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  40. package/skills/cicd/cloud-build/SKILL.md +1 -1
  41. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  42. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  43. package/skills/cicd/docker/SKILL.md +1 -1
  44. package/skills/cicd/github-actions/SKILL.md +1 -1
  45. package/skills/cicd/github-flow/SKILL.md +1 -1
  46. package/skills/cicd/google-cloud/SKILL.md +1 -1
  47. package/skills/cicd/mergify/SKILL.md +1 -1
  48. package/skills/cicd/podman/SKILL.md +1 -1
  49. package/skills/content/article-review/SKILL.md +1 -1
  50. package/skills/content/article-to-image/SKILL.md +1 -1
  51. package/skills/content/flowchart-svg/SKILL.md +1 -1
  52. package/skills/content/image-gen/SKILL.md +1 -1
  53. package/skills/content/mermaid/SKILL.md +1 -1
  54. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  55. package/skills/content/wechat-publisher/SKILL.md +1 -1
  56. package/skills/content/wechat-search/SKILL.md +1 -1
  57. package/skills/content/xai-search/SKILL.md +1 -1
  58. package/skills/core/distill-writing/SKILL.md +1 -1
  59. package/skills/core/doc-to-md/SKILL.md +1 -1
  60. package/skills/core/editorial-review/SKILL.md +1 -1
  61. package/skills/core/house-style/SKILL.md +1 -1
  62. package/skills/core/plain-language/SKILL.md +1 -1
  63. package/skills/core/polish-cn/SKILL.md +1 -1
  64. package/skills/core/reg-parser/SKILL.md +1 -1
  65. package/skills/core/terminology/SKILL.md +1 -1
  66. package/skills/core/write-doc/SKILL.md +1 -1
  67. package/skills/core/write-report/SKILL.md +1 -1
  68. package/skills/engineering/better-review/SKILL.md +1 -1
  69. package/skills/engineering/e2e-test/SKILL.md +1 -1
  70. package/skills/engineering/engineering/SKILL.md +1 -1
  71. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  72. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  73. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  74. package/skills/engineering/golden-path/SKILL.md +1 -1
  75. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  76. package/skills/engineering/harness-runtime/agent-catalog.mjs +44 -11
  77. package/skills/engineering/harness-runtime/contracts.md +1 -1
  78. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  79. package/skills/engineering/resolve-release/SKILL.md +1 -1
  80. package/skills/engineering/rush-issues/SKILL.md +1 -1
  81. package/skills/engineering/rush-release/SKILL.md +1 -1
  82. package/skills/engineering/test-quality/SKILL.md +1 -1
  83. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  84. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  85. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  86. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  87. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  88. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  89. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  90. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  91. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
@@ -1,461 +1,150 @@
1
- """Focused tests for maestro_failure.py: fixture Maestro output, no device, no Maestro.
1
+ """Thin forwarding coverage for the Android ``maestro_failure`` compatibility entry point.
2
2
 
3
- The fixtures are the artifact layout Maestro's docs describe for a Flow folder
4
- (manifest.json, commands.json, screenshots/, screen-hierarchy/, logs/), with the
5
- commands.json entry shape taken from Maestro's source (TestOutputWriter and
6
- CommandDebugMetadata) because the docs name the fields but show no example.
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
- `scripts/maestro-failure.test.mjs`, which is what CI discovers.
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
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
-
22
- from failure_capture import NATIVE_FAILURE_PREFIX, RECORD_KEYS, RECORD_NAME, read_record # noqa: E402
23
- from maestro_failure import ( # noqa: E402
24
- MaestroOutputError,
25
- convert,
26
- describe_command,
27
- error_text,
28
- main,
29
- )
30
-
31
- SESSION = "2026-07-31_173617"
32
- FLOW = "checkout_flow"
33
- CASE = "checkout smoke"
34
-
35
- LAUNCHED = {
36
- "command": {"launchAppCommand": {"appId": "com.example.app", "optional": False}},
37
- "metadata": {
38
- "status": "COMPLETED",
39
- "timestamp": 1700000000000,
40
- "duration": 1200,
41
- "sequenceNumber": 0,
42
- "depth": 0,
43
- },
44
- }
45
-
46
- TAPPED = {
47
- "command": {
48
- "tapOnElement": {
49
- "selector": {"text": "Pay", "optional": False},
50
- "label": "Tap Pay",
51
- "optional": False,
52
- }
53
- },
54
- "metadata": {
55
- "status": "COMPLETED",
56
- "timestamp": 1700000001300,
57
- "duration": 300,
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 MaestroOutputCase(unittest.TestCase):
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.output = io.StringIO()
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
- path = os.path.join(evidence_dir, RECORD_NAME)
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
- self.assertEqual(failure.case, CASE)
277
- self.assertEqual(self.record(flow_dir), kept)
278
- with open(path, encoding="utf-8") as stream:
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
- def test_a_record_already_in_an_override_evidence_dir_is_kept(self):
283
- flow_dir = self.failing_flow()
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["step"], "tapOn: Pay")
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
- self.run_convert(flow_dir)
423
-
424
- record = self.record(flow_dir)
425
- self.assertEqual(record["screenshot"], "screenshots/step-002-assertCondition.png")
426
- self.assertEqual(record["uiDump"], "screen-hierarchy/step-002-assertCondition.json")
427
-
428
- def test_an_ios_device_log_is_used_when_there_is_no_logcat(self):
429
- flow_dir = self.flow(
430
- [FAILED_RECEIPT],
431
- artifacts={"logs/device-simulator.log": "simulator log\n"},
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
- class ReadingTest(unittest.TestCase):
438
- def test_a_command_without_a_label_reads_as_its_type_and_target(self):
439
- self.assertEqual(describe_command({"scrollCommand": {"optional": False}}), "Scroll")
440
- self.assertEqual(
441
- describe_command({"launchAppCommand": {"appId": "com.example.app"}}),
442
- 'Launch app "com.example.app"',
443
- )
444
- self.assertEqual(describe_command("tapOn: Pay"), "tapOn: Pay")
445
- self.assertIsNone(describe_command(None))
446
-
447
- def test_error_text_keeps_the_debug_detail(self):
448
- self.assertEqual(error_text({"error": "boom"}), "boom")
449
- self.assertEqual(
450
- error_text({"error": {"message": "boom", "debugMessage": "screen has no Foo"}}),
451
- "boom: screen has no Foo",
452
- )
453
- self.assertEqual(error_text({"error": {"message": "boom"}}), "boom")
454
- self.assertEqual(error_text({}), "Maestro marked the step FAILED and gave no error message")
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
- def test_read_record_ignores_a_non_record(self):
457
- with tempfile.TemporaryDirectory() as directory:
458
- self.assertIsNone(read_record(directory))
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.48.1
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.48.1
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.48.1
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.48.1
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.48.1
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
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AboveIO