@1aboveio/skills 1.0.14 → 1.0.16
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 +164 -160
- package/package.json +4 -4
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +30 -2
- package/skills/app-dev/android-development/references/diagnosis.md +55 -0
- package/skills/app-dev/android-development/references/feature-work.md +15 -0
- package/skills/app-dev/android-development/references/init.md +57 -6
- package/skills/app-dev/android-development/references/setup.md +8 -2
- package/skills/app-dev/android-development/references/ui-journeys.md +58 -0
- package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
- package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
- package/skills/app-dev/android-development/scripts/init.mjs +234 -17
- package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
- package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
- package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
- 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 +53 -16
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +231 -21
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +12 -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/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 +10 -1
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +231 -21
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +18 -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 +7 -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/house-style/references/persistence.md +8 -3
- package/skills/core/house-style/scripts/init-guidance.mjs +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/terminology/references/init.md +11 -0
- 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 +19 -4
- package/skills/engineering/engineering/references/init-scope.md +5 -2
- package/skills/engineering/engineering/references/scope-management.md +4 -2
- package/skills/engineering/engineering/scripts/init.mjs +34 -9
- package/skills/engineering/engineering/scripts/persist-conventions-core.mjs +231 -21
- package/skills/engineering/engineering/scripts/persist-conventions.mjs +48 -21
- 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 +4 -3
- package/skills/engineering/harness-runtime/agent-catalog.mjs +928 -0
- package/skills/engineering/harness-runtime/contracts.md +2 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
- package/skills/engineering/resolve-issues/SKILL.md +9 -6
- package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
- package/skills/engineering/resolve-issues/references/expire.md +6 -3
- package/skills/engineering/resolve-issues/references/implementation.md +6 -1
- package/skills/engineering/resolve-issues/references/preflight.md +26 -17
- package/skills/engineering/resolve-issues/references/review.md +39 -4
- package/skills/engineering/resolve-issues/references/shared-modules.md +88 -20
- package/skills/engineering/resolve-issues/references/task-plan.md +35 -1
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +4 -4
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +3 -1
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -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
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""Focused tests for maestro_failure.py: fixture Maestro output, no device, no Maestro.
|
|
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.
|
|
7
|
+
|
|
8
|
+
Run directly (`python3 test_maestro_failure.py`) or through
|
|
9
|
+
`scripts/maestro-failure.test.mjs`, which is what CI discovers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import unittest
|
|
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
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MaestroOutputCase(unittest.TestCase):
|
|
89
|
+
def setUp(self):
|
|
90
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
91
|
+
self.root = self.directory.name
|
|
92
|
+
self.output = io.StringIO()
|
|
93
|
+
|
|
94
|
+
def tearDown(self):
|
|
95
|
+
self.directory.cleanup()
|
|
96
|
+
|
|
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
|
+
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:
|
|
155
|
+
return json.load(stream)
|
|
156
|
+
|
|
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
|
+
|
|
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()])
|
|
281
|
+
|
|
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"]),
|
|
325
|
+
)
|
|
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
|
+
|
|
403
|
+
record = self.record(flow_dir)
|
|
404
|
+
self.assertEqual(record["step"], "tapOn: Pay")
|
|
405
|
+
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
|
+
|
|
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"},
|
|
432
|
+
)
|
|
433
|
+
self.run_convert(flow_dir)
|
|
434
|
+
self.assertEqual(self.record(flow_dir)["logcatTail"], "logs/device-simulator.log")
|
|
435
|
+
|
|
436
|
+
|
|
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")
|
|
455
|
+
|
|
456
|
+
def test_read_record_ignores_a_non_record(self):
|
|
457
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
458
|
+
self.assertIsNone(read_record(directory))
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
if __name__ == "__main__":
|
|
462
|
+
unittest.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.48.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.48.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.48.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Data catalog
|
|
@@ -12,23 +12,49 @@ and CI fails a pull request when the pages and the scripts disagree.
|
|
|
12
12
|
|
|
13
13
|
## First use in a repo
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
No `data-catalog.json` means the repo is not set up. That is a fact about the repo,
|
|
16
|
+
not permission to write to it, and the `## Data catalog task entry` rule that loaded
|
|
17
|
+
this skill is not one either: an audit, a review, a column question or a table-script
|
|
18
|
+
change is the task, and it is done from the sources the repo already has.
|
|
19
|
+
|
|
20
|
+
- **Read what exists.** `docs/catalog/<table>.md` when the repo has pages, else the
|
|
21
|
+
script's `CATALOG_TABLE`/`CATALOG_FIELDS`, else its `-- @table`/`-- @field` block,
|
|
22
|
+
else the SQL or DataFrame code. A definition in the script answers the question;
|
|
23
|
+
never hand-write the missing page.
|
|
24
|
+
- **Audit read-only.** `python3 <skill-base-dir>/scripts/data_catalog.py audit
|
|
25
|
+
'jobs/**/*.py' 'sql/**/*.sql'` needs no config and writes nothing; it exits 2 only
|
|
26
|
+
with neither a config nor globs.
|
|
27
|
+
- **A check you cannot run stays pending.** `generate`, `generate --check` and `drift`
|
|
28
|
+
exit 2 without `data-catalog.json`. Report them pending — "the catalog is not set up
|
|
29
|
+
here" — not "no findings", "green" or "CI passes", and do not run `init --write` to
|
|
30
|
+
make them runnable: running them is the task, making them runnable is setup.
|
|
31
|
+
- **Offer setup once.** One short line: `init --write` with the `--scripts` globs and
|
|
32
|
+
`--languages` you would use, plus `persist-conventions.mjs --write` for the
|
|
33
|
+
conventions section, the `AGENTS.md` pointer and the task-entry rule. Say nothing was
|
|
34
|
+
written, keep working, once per session per repo. Not an approval checkpoint.
|
|
35
|
+
- **Setting up is an explicit request.** "init the data catalog", "generate the data
|
|
36
|
+
dictionary", "字段字典", "add catalog CI", "set up the catalog here" — that
|
|
37
|
+
authorizes these writes in that repo only. An automatic review, the loader rule, a
|
|
38
|
+
handoff from another skill, or having read this section do not.
|
|
39
|
+
|
|
40
|
+
With that request, setup is:
|
|
19
41
|
|
|
20
42
|
1. `python3 <skill-base-dir>/scripts/data_catalog.py audit '<globs>'` to find the
|
|
21
43
|
table scripts; choose `--scripts` globs that cover them and nothing else.
|
|
22
44
|
2. Pick `--languages` from the repo's existing docs (Chinese docs: `zh,en`;
|
|
23
45
|
otherwise `en`). Run `init` with them and `--write` ([Init](#init)).
|
|
24
46
|
3. `node <skill-base-dir>/scripts/persist-conventions.mjs --repo-root <repo> --write`
|
|
25
|
-
writes the `## Data catalog conventions` section of `CONTRIBUTING.md
|
|
26
|
-
`AGENTS.md` at it
|
|
47
|
+
writes the `## Data catalog conventions` section of `CONTRIBUTING.md`, points
|
|
48
|
+
`AGENTS.md` at it, and adds the `## Data catalog task entry` rule that loads this
|
|
49
|
+
skill for column or table-script work. It is the only writer of those files and
|
|
50
|
+
writes only with its own `--write`; an ordinary catalog task never runs it.
|
|
27
51
|
4. Then do the task. In the reply, list what setup added, and say the Data catalog
|
|
28
52
|
check runs on PRs but is not required.
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
Outside a git repo there is nothing to set up: say so. Never document tables during
|
|
55
|
+
setup unless the task asks for it. Repeating setup with the same inputs leaves the
|
|
56
|
+
configured files unchanged; report a missing input or a conflicting rule instead of
|
|
57
|
+
claiming success.
|
|
32
58
|
|
|
33
59
|
## Find your task
|
|
34
60
|
|
|
@@ -36,7 +62,7 @@ Never document tables during setup unless the task asks for it.
|
|
|
36
62
|
|---|---|
|
|
37
63
|
| Writing or changing a table script | [While coding](#while-coding), then [references/field-definitions.md](references/field-definitions.md) |
|
|
38
64
|
| "What does column X mean?" | Read `docs/catalog/<table>.md`; with no page, read the script's `CATALOG_FIELDS`, then its SQL |
|
|
39
|
-
| The repo has no `data-catalog.json` | [First use in a repo](#first-use-in-a-repo) |
|
|
65
|
+
| The repo has no `data-catalog.json` | Do the task read-only, then offer setup once ([First use in a repo](#first-use-in-a-repo)) |
|
|
40
66
|
| Find scripts with missing or weak definitions | [Audit](#audit) |
|
|
41
67
|
| Document an existing table | [Backfill one table](#backfill-one-table) |
|
|
42
68
|
| CI says the catalog is stale | `python3 <engine> generate`, commit the pages |
|
|
@@ -46,8 +72,8 @@ Never document tables during setup unless the task asks for it.
|
|
|
46
72
|
|
|
47
73
|
`<engine>` is `scripts/data-catalog/data_catalog.py` in the repo, or
|
|
48
74
|
`<skill-base-dir>/scripts/data_catalog.py` before the repo has a copy. When
|
|
49
|
-
`python3 <engine> version` differs from the skill's copy, refresh
|
|
50
|
-
`init --write
|
|
75
|
+
`python3 <engine> version` differs from the skill's copy, the refresh is
|
|
76
|
+
`init --write`, which needs a setup request ([First use in a repo](#first-use-in-a-repo)).
|
|
51
77
|
|
|
52
78
|
## The files
|
|
53
79
|
|
|
@@ -61,7 +87,7 @@ Never document tables during setup unless the task asks for it.
|
|
|
61
87
|
| `scripts/data-catalog/data_catalog.py` | engine copied from this skill | never; refresh with init |
|
|
62
88
|
| `.github/workflows/data-catalog.yml` | generated CI check | never; rerun init |
|
|
63
89
|
| `CONTRIBUTING.md` section between `conventions:data-catalog` markers | the conventions, for agents without this skill | never; rerun `persist-conventions.mjs --write`; the rest is yours |
|
|
64
|
-
| `AGENTS.md` `## Contributing` pointer
|
|
90
|
+
| `AGENTS.md` `## Contributing` pointer and `## Data catalog task entry` rule | reach `CONTRIBUTING.md` and this skill; the rule routes work and authorizes no write | written once by `persist-conventions.mjs`; re-run `--write` to restore a missing one |
|
|
65
91
|
|
|
66
92
|
## Rules
|
|
67
93
|
|
|
@@ -118,6 +144,11 @@ Never document tables during setup unless the task asks for it.
|
|
|
118
144
|
PR why it still holds.
|
|
119
145
|
6. Commit the script and the regenerated pages together.
|
|
120
146
|
|
|
147
|
+
A repo with no `data-catalog.json`: steps 4 and 5 cannot run, because both exit 2
|
|
148
|
+
without a config. Write the definitions the change needs, say those checks are
|
|
149
|
+
pending, and offer setup once ([First use in a repo](#first-use-in-a-repo)). Do not
|
|
150
|
+
run `init --write` to make them run.
|
|
151
|
+
|
|
121
152
|
Example: changing `WHERE status = 'POSTED'` to `status IN ('POSTED', 'SETTLED')`
|
|
122
153
|
passes CI, and `drift` reports "filters, joins or CTEs changed; no definition
|
|
123
154
|
changed" until the affected sources say "posted or settled".
|
|
@@ -141,6 +172,10 @@ to `data-catalog.json` take effect on the next `init --write`. After a skill upd
|
|
|
141
172
|
rerun both `init --write` and `persist-conventions.mjs --write` so the repo's copies
|
|
142
173
|
match.
|
|
143
174
|
|
|
175
|
+
Without `--write`, `init` only prints its plan, so a dry run is always safe. With
|
|
176
|
+
`--write` it is repo setup and needs the user's setup request
|
|
177
|
+
([First use in a repo](#first-use-in-a-repo)).
|
|
178
|
+
|
|
144
179
|
The scripts import `from catalog_fields import Field, Table` at run time, so the
|
|
145
180
|
module must ship with them like any sibling module (for example `--py-files` on
|
|
146
181
|
`spark-submit`). Check that before merging.
|
|
@@ -188,7 +223,8 @@ script is clean when its columns were `unknown`.
|
|
|
188
223
|
3. Add `CATALOG_TABLE` and `CATALOG_FIELDS` in contract order. Keep the literal
|
|
189
224
|
contract while you write.
|
|
190
225
|
4. Run `generate` until it passes, then derive the contract and run the repo's
|
|
191
|
-
schema tests (rule 5).
|
|
226
|
+
schema tests (rule 5). In a repo with no `data-catalog.json` it exits 2: report it
|
|
227
|
+
pending and offer setup once, and do not run `init --write` for it.
|
|
192
228
|
5. Rerun the audit for that script and fix its writing findings (`repeated-text`,
|
|
193
229
|
`code-in-description`, `long-definition`, `thin-definition`,
|
|
194
230
|
`relative-definition`), or leave one only when the text really belongs in that row.
|
|
@@ -201,8 +237,9 @@ empty text.
|
|
|
201
237
|
|
|
202
238
|
## Done when
|
|
203
239
|
|
|
204
|
-
- `python3 <engine> generate --check` exits 0
|
|
240
|
+
- `python3 <engine> generate --check` exits 0, or you said it is pending because the
|
|
241
|
+
repo has no `data-catalog.json`.
|
|
205
242
|
- `python3 <engine> drift` exits 0, or you said in the PR why each listed definition
|
|
206
|
-
still holds.
|
|
243
|
+
still holds, or you reported it pending for the same reason.
|
|
207
244
|
- `python3 <engine> audit` has no findings for the tables you touched, or you
|
|
208
245
|
reported the ones left and why.
|