Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions scenarios/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,33 @@ class ScenarioEntry:
scenario_id: str
location: str
description: str
tags: tuple[str, ...]


CATALOG: tuple[ScenarioEntry, ...] = (
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"),
),
)

Expand All @@ -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
]

Expand Down
6 changes: 5 additions & 1 deletion tests/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -44,14 +45,17 @@ 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())
self.assertEqual(
[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()
Expand Down
9 changes: 8 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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, [])


Expand Down
Loading