From 47097465e7db08ec0024a2e7e5d08d482eae31eb Mon Sep 17 00:00:00 2001 From: leiwilson <305842734+leiwilson@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:18:17 +0100 Subject: [PATCH 1/2] feat(catalog): include tags in json catalog output Co-authored-by: leiwilson <305842734+leiwilson@users.noreply.github.com> --- README.md | 4 ++++ scenarios/catalog.py | 13 +++++++++++-- tests/test_catalog.py | 6 +++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index faf19b6..3415fa5 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ Includes a small client-side exponential backoff helper for exercising those fai - `scenarios/retry` — handler that raises a transient error - `scenarios/throttle` — handler that simulates 429 / rate-limit responses +The JSON catalog output (`python -m scenarios.catalog --format json`) now includes +`scenario_id`, `description`, and `tags` for each entry so downstream tooling can +filter by capability without parsing free-form text. + ## Client helpers - `clients/backoff.py` — exponential backoff retries diff --git a/scenarios/catalog.py b/scenarios/catalog.py index 7eda0bc..92403e9 100644 --- a/scenarios/catalog.py +++ b/scenarios/catalog.py @@ -13,6 +13,7 @@ class ScenarioEntry: scenario_id: str location: str description: str + tags: tuple[str, ...] CATALOG: tuple[ScenarioEntry, ...] = ( @@ -20,21 +21,25 @@ class ScenarioEntry: "timeout", "scenarios/timeout", "Sleep past a configured timeout to simulate Lambda time limits.", + ("timeout", "latency", "failure-mode"), ), ScenarioEntry( "retry", "scenarios/retry", "Raise transient errors so callers can exercise retry logic.", + ("retry", "transient", "backoff"), ), ScenarioEntry( "throttle", "scenarios/throttle", "Simulate 429 rate-limit responses with optional Retry-After.", + ("throttle", "rate-limit", "retry-after"), ), ScenarioEntry( "circuit-breaker", "clients/circuit_breaker.py", "Open the circuit after repeated failures and recover after a cooldown.", + ("circuit-breaker", "resilience", "recovery"), ), ) @@ -53,10 +58,14 @@ def format_catalog() -> str: return "\n".join(lines) -def catalog_entries_json() -> list[dict[str, str]]: +def catalog_entries_json() -> list[dict[str, object]]: """Return catalog entries as stable JSON-serializable dicts.""" return [ - {"scenario_id": entry.scenario_id, "description": entry.description} + { + "scenario_id": entry.scenario_id, + "description": entry.description, + "tags": list(entry.tags), + } for entry in CATALOG ] diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 1423eba..4fca389 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -23,6 +23,7 @@ def test_catalog_has_descriptions(self): for entry in CATALOG: self.assertTrue(entry.description) self.assertTrue(entry.location) + self.assertTrue(entry.tags) def test_format_catalog_includes_entries(self): text = format_catalog() @@ -44,7 +45,9 @@ def test_catalog_entries_json_shape(self): for entry in entries: self.assertIn("scenario_id", entry) self.assertIn("description", entry) - self.assertEqual(set(entry.keys()), {"scenario_id", "description"}) + self.assertIn("tags", entry) + self.assertIsInstance(entry["tags"], list) + self.assertEqual(set(entry.keys()), {"scenario_id", "description", "tags"}) def test_format_catalog_json_is_stable(self): parsed = json.loads(format_catalog_json()) @@ -52,6 +55,7 @@ def test_format_catalog_json_is_stable(self): [entry["scenario_id"] for entry in parsed], ["timeout", "retry", "throttle", "circuit-breaker"], ) + self.assertEqual(parsed[0]["tags"], ["timeout", "latency", "failure-mode"]) def test_main_json_format(self): buffer = io.StringIO() From 20fddf258215627e5df04fb50831be5cacfae244 Mon Sep 17 00:00:00 2001 From: leiwilson <305842734+leiwilson@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:20:12 +0100 Subject: [PATCH 2/2] test(cli): assert stable json shape for show and find Co-authored-by: leiwilson <305842734+leiwilson@users.noreply.github.com> --- tests/test_cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 979cc65..0c1d63b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -173,9 +173,13 @@ def test_show_json_output(self): code = main(["show", "retry", "--format", "json"]) self.assertEqual(code, 0) parsed = json.loads(buffer.getvalue()) + self.assertEqual( + set(parsed.keys()), + {"scenario_id", "location", "description"}, + ) self.assertEqual(parsed["scenario_id"], "retry") self.assertEqual(parsed["location"], "scenarios/retry") - self.assertIn("description", parsed) + self.assertTrue(parsed["description"]) def test_show_unknown_scenario_returns_error(self): buffer = io.StringIO() @@ -281,7 +285,9 @@ def test_find_json_format(self): code = main(["find", "CIRCUIT-BREAKER", "--format", "json"]) self.assertEqual(code, 0) parsed = json.loads(buffer.getvalue()) + self.assertIsInstance(parsed, list) self.assertEqual(parsed, ["circuit-breaker"]) + self.assertTrue(all(isinstance(item, str) for item in parsed)) def test_find_text_format_default_unchanged(self): buffer = io.StringIO() @@ -297,6 +303,7 @@ def test_find_json_no_matches_returns_empty_array(self): code = main(["find", "zzzz-no-such", "--format", "json"]) self.assertEqual(code, 1) parsed = json.loads(buffer.getvalue()) + self.assertIsInstance(parsed, list) self.assertEqual(parsed, [])