Add pytest suite and wire it into CI (#85) (#86)
* Add pytest suite and wire it into CI
Stands up the first automated tests for the repo (151 tests) and turns on
the CI test step.
- Lift slack-bot handlers out of create_app() closures to module level so
they're unit-testable; create_app is now a thin Bolt-wiring layer. No
behavior change (handler entrypoints and create_app signature unchanged).
- tests/ mirrors src/: shared layer (schedule, blocks, 3CX client,
ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/
admin/register/rate, pickup button, roster sync, queue scheduler).
- All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via
responses, Slack via fakes, time via freezegun. No real network/AWS.
- pyproject.toml pytest config (pythonpath=src/shared, importlib mode);
per-package conftest loads each app.py under a unique name to avoid the
four-app.py collision. tests/requirements.txt for test-only deps.
- ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint
the tests dir too.
- README Testing section.
Closes #85
* Add least-privilege permissions block to CI workflow
Resolves the CodeQL actions/missing-workflow-permissions alert: the CI
workflow now restricts GITHUB_TOKEN to contents: read (it only checks out,
lints, and runs tests).
* Stop logging extension numbers in 3CX queue updates
Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the
queue/ring-group forwarding logs no longer include the routed extension
values (closed/holiday/extension). Non-sensitive context (resource id,
queue number) is retained.
2026-06-01 19:07:08 -04:00
|
|
|
"""Tests for shared.blocks — Block Kit builders."""
|
|
|
|
|
|
|
|
|
|
from freezegun import freeze_time
|
|
|
|
|
|
|
|
|
|
from shared.blocks import (
|
|
|
|
|
build_help_blocks,
|
|
|
|
|
build_pay_summary_blocks,
|
Add changelog-driven releases and App Home tab (#112)
* Add changelog-driven releases and App Home tab
Version the bot continuously from CHANGELOG.md (the single source of
truth for both the version and the staff-readable notes) and surface
changes to users in two ways:
- A new afterhours-release-notifier Lambda posts a "What's New" message
to the shift channel on minor/major releases (patches stay silent).
- The bot gains an App Home "About" tab showing what it does, the
command list, and the current version's notes.
release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN
events don't start downstream workflows), checks out the deployed commit,
and tags + publishes a GitHub Release + invokes the notifier. It assumes a
dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the
notifier; the account's cfn role gates role creation on that boundary.
The manual Version Bump workflow is retired. A CI guard enforces that a
CHANGELOG edit is a clean SemVer bump and that the in-package copy matches.
* Harden release workflow and regex against CodeQL findings
Address three code-scanning alerts on the PR:
- Critical (actions/untrusted-checkout): split release.yaml into a
read-only `prepare` job that checks out and runs repo code, and a
privileged `publish` job (contents:write + OIDC) that never checks out
repo code — it tags, releases, and invokes purely through the GitHub
and AWS APIs. Also assert head_branch == main.
- High x2 (py/polynomial-redos): rewrite the italic and link regexes in
markdown_to_mrkdwn with possessive quantifiers and exclusive character
classes so they run in linear time on adversarial input. Adds a
regression test.
* Move release/announce into Deploy workflow to clear CodeQL
The workflow_run-triggered release.yaml kept tripping CodeQL's
privileged-context rules (untrusted-checkout, then cache-poisoning) —
CodeQL distrusts any workflow_run that checks out a ref, regardless of
the main-only guarantee, and there is no autofix.
Fold the release job into deploy.yaml gated on `needs: deploy`. A
push-to-main run is a trusted context, so checking out and running repo
code with write/OIDC is safe there. This still gates on deploy success
and serializes via the deploy concurrency group, and removes the
separate workflow entirely.
2026-06-11 19:41:31 -04:00
|
|
|
build_release_announcement_blocks,
|
Add pytest suite and wire it into CI (#85) (#86)
* Add pytest suite and wire it into CI
Stands up the first automated tests for the repo (151 tests) and turns on
the CI test step.
- Lift slack-bot handlers out of create_app() closures to module level so
they're unit-testable; create_app is now a thin Bolt-wiring layer. No
behavior change (handler entrypoints and create_app signature unchanged).
- tests/ mirrors src/: shared layer (schedule, blocks, 3CX client,
ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/
admin/register/rate, pickup button, roster sync, queue scheduler).
- All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via
responses, Slack via fakes, time via freezegun. No real network/AWS.
- pyproject.toml pytest config (pythonpath=src/shared, importlib mode);
per-package conftest loads each app.py under a unique name to avoid the
four-app.py collision. tests/requirements.txt for test-only deps.
- ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint
the tests dir too.
- README Testing section.
Closes #85
* Add least-privilege permissions block to CI workflow
Resolves the CodeQL actions/missing-workflow-permissions alert: the CI
workflow now restricts GITHUB_TOKEN to contents: read (it only checks out,
lints, and runs tests).
* Stop logging extension numbers in 3CX queue updates
Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the
queue/ring-group forwarding logs no longer include the routed extension
values (closed/holiday/extension). Non-sensitive context (resource id,
queue number) is retained.
2026-06-01 19:07:08 -04:00
|
|
|
build_roster_blocks,
|
|
|
|
|
build_shift_change_message,
|
2026-06-01 19:22:55 -04:00
|
|
|
build_swap_request_blocks,
|
|
|
|
|
build_swap_resolved_blocks,
|
Add pytest suite and wire it into CI (#85) (#86)
* Add pytest suite and wire it into CI
Stands up the first automated tests for the repo (151 tests) and turns on
the CI test step.
- Lift slack-bot handlers out of create_app() closures to module level so
they're unit-testable; create_app is now a thin Bolt-wiring layer. No
behavior change (handler entrypoints and create_app signature unchanged).
- tests/ mirrors src/: shared layer (schedule, blocks, 3CX client,
ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/
admin/register/rate, pickup button, roster sync, queue scheduler).
- All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via
responses, Slack via fakes, time via freezegun. No real network/AWS.
- pyproject.toml pytest config (pythonpath=src/shared, importlib mode);
per-package conftest loads each app.py under a unique name to avoid the
four-app.py collision. tests/requirements.txt for test-only deps.
- ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint
the tests dir too.
- README Testing section.
Closes #85
* Add least-privilege permissions block to CI workflow
Resolves the CodeQL actions/missing-workflow-permissions alert: the CI
workflow now restricts GITHUB_TOKEN to contents: read (it only checks out,
lints, and runs tests).
* Stop logging extension numbers in 3CX queue updates
Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the
queue/ring-group forwarding logs no longer include the routed extension
values (closed/holiday/extension). Non-sensitive context (resource id,
queue number) is retained.
2026-06-01 19:07:08 -04:00
|
|
|
build_week_schedule,
|
Add changelog-driven releases and App Home tab (#112)
* Add changelog-driven releases and App Home tab
Version the bot continuously from CHANGELOG.md (the single source of
truth for both the version and the staff-readable notes) and surface
changes to users in two ways:
- A new afterhours-release-notifier Lambda posts a "What's New" message
to the shift channel on minor/major releases (patches stay silent).
- The bot gains an App Home "About" tab showing what it does, the
command list, and the current version's notes.
release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN
events don't start downstream workflows), checks out the deployed commit,
and tags + publishes a GitHub Release + invokes the notifier. It assumes a
dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the
notifier; the account's cfn role gates role creation on that boundary.
The manual Version Bump workflow is retired. A CI guard enforces that a
CHANGELOG edit is a clean SemVer bump and that the in-package copy matches.
* Harden release workflow and regex against CodeQL findings
Address three code-scanning alerts on the PR:
- Critical (actions/untrusted-checkout): split release.yaml into a
read-only `prepare` job that checks out and runs repo code, and a
privileged `publish` job (contents:write + OIDC) that never checks out
repo code — it tags, releases, and invokes purely through the GitHub
and AWS APIs. Also assert head_branch == main.
- High x2 (py/polynomial-redos): rewrite the italic and link regexes in
markdown_to_mrkdwn with possessive quantifiers and exclusive character
classes so they run in linear time on adversarial input. Adds a
regression test.
* Move release/announce into Deploy workflow to clear CodeQL
The workflow_run-triggered release.yaml kept tripping CodeQL's
privileged-context rules (untrusted-checkout, then cache-poisoning) —
CodeQL distrusts any workflow_run that checks out a ref, regardless of
the main-only guarantee, and there is no autofix.
Fold the release job into deploy.yaml gated on `needs: deploy`. A
push-to-main run is a trusted context, so checking out and running repo
code with write/OIDC is safe there. This still gates on deploy success
and serializes via the deploy concurrency group, and removes the
separate workflow entirely.
2026-06-11 19:41:31 -04:00
|
|
|
markdown_to_mrkdwn,
|
Add pytest suite and wire it into CI (#85) (#86)
* Add pytest suite and wire it into CI
Stands up the first automated tests for the repo (151 tests) and turns on
the CI test step.
- Lift slack-bot handlers out of create_app() closures to module level so
they're unit-testable; create_app is now a thin Bolt-wiring layer. No
behavior change (handler entrypoints and create_app signature unchanged).
- tests/ mirrors src/: shared layer (schedule, blocks, 3CX client,
ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/
admin/register/rate, pickup button, roster sync, queue scheduler).
- All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via
responses, Slack via fakes, time via freezegun. No real network/AWS.
- pyproject.toml pytest config (pythonpath=src/shared, importlib mode);
per-package conftest loads each app.py under a unique name to avoid the
four-app.py collision. tests/requirements.txt for test-only deps.
- ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint
the tests dir too.
- README Testing section.
Closes #85
* Add least-privilege permissions block to CI workflow
Resolves the CodeQL actions/missing-workflow-permissions alert: the CI
workflow now restricts GITHUB_TOKEN to contents: read (it only checks out,
lints, and runs tests).
* Stop logging extension numbers in 3CX queue updates
Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the
queue/ring-group forwarding logs no longer include the routed extension
values (closed/holiday/extension). Non-sensitive context (resource id,
queue number) is retained.
2026-06-01 19:07:08 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _all_action_ids(blocks):
|
|
|
|
|
ids = []
|
|
|
|
|
for b in blocks:
|
|
|
|
|
if b.get("type") == "actions":
|
|
|
|
|
ids.extend(e["action_id"] for e in b["elements"])
|
|
|
|
|
return ids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildWeekSchedule:
|
|
|
|
|
@freeze_time("2026-06-01 12:00:00") # Monday
|
|
|
|
|
def test_header_and_section_present(self, schedule):
|
|
|
|
|
blocks = build_week_schedule(schedule)
|
|
|
|
|
assert blocks[0]["type"] == "header"
|
|
|
|
|
assert "After-Hours Schedule" in blocks[0]["text"]["text"]
|
|
|
|
|
assert blocks[1]["type"] == "section"
|
|
|
|
|
|
|
|
|
|
@freeze_time("2026-06-01 12:00:00")
|
|
|
|
|
def test_all_available_produces_pickup_buttons(self, schedule):
|
|
|
|
|
# Empty schedule → every shift is available → pickup buttons exist,
|
|
|
|
|
# including a weekend day button with the _day suffix.
|
|
|
|
|
blocks = build_week_schedule(schedule)
|
|
|
|
|
action_ids = _all_action_ids(blocks)
|
|
|
|
|
assert "pickup_2026-06-03" in action_ids # Wednesday night
|
|
|
|
|
assert "pickup_2026-06-06_day" in action_ids # Saturday day shift
|
|
|
|
|
assert "pickup_2026-06-06" in action_ids # Saturday night shift
|
|
|
|
|
|
|
|
|
|
@freeze_time("2026-06-01 12:00:00")
|
|
|
|
|
def test_assigned_shift_has_no_pickup_button(self, schedule, seed):
|
|
|
|
|
seed.weekly("Wednesday", "114", "Alice")
|
|
|
|
|
blocks = build_week_schedule(schedule)
|
|
|
|
|
assert "pickup_2026-06-03" not in _all_action_ids(blocks)
|
|
|
|
|
assert "Alice (Ext 114)" in blocks[1]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildShiftChangeMessage:
|
|
|
|
|
def test_picked_up_weekday(self):
|
|
|
|
|
blocks = build_shift_change_message(
|
|
|
|
|
"U1", "2026-06-03", "picked_up", "114", "Alice"
|
|
|
|
|
)
|
|
|
|
|
text = blocks[0]["text"]["text"]
|
|
|
|
|
assert "<@U1>" in text and "picked up" in text and "Ext 114" in text
|
|
|
|
|
# Weekday → no (Day/Night) label
|
|
|
|
|
assert "(Night" not in text
|
|
|
|
|
|
|
|
|
|
def test_dropped_shows_available(self):
|
|
|
|
|
blocks = build_shift_change_message(
|
|
|
|
|
"U1", "2026-06-03", "dropped", "114", "Alice"
|
|
|
|
|
)
|
|
|
|
|
assert "Available" in blocks[0]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
def test_swapped_text(self):
|
|
|
|
|
blocks = build_shift_change_message("U2", "2026-06-03", "swapped", "115", "Bob")
|
|
|
|
|
assert "swapped" in blocks[0]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
def test_weekend_includes_shift_label(self):
|
|
|
|
|
blocks = build_shift_change_message(
|
|
|
|
|
"U1", "2026-06-06", "picked_up", "200", "Alice", shift_type="day"
|
|
|
|
|
)
|
|
|
|
|
assert "Day (8am" in blocks[0]["text"]["text"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildPaySummaryBlocks:
|
|
|
|
|
def test_renders_breakdown_and_totals(self):
|
|
|
|
|
breakdown = [
|
|
|
|
|
{
|
|
|
|
|
"day": "Mon",
|
|
|
|
|
"date_label": "Jun 1",
|
|
|
|
|
"name": "Alice",
|
|
|
|
|
"extension": "114",
|
|
|
|
|
"rate": 50.0,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
totals = {
|
|
|
|
|
"Alice": {"shifts": 1, "total": 50.0, "extension": "114", "rate": 50.0}
|
|
|
|
|
}
|
|
|
|
|
blocks = build_pay_summary_blocks("Jun 1 to Jun 7", breakdown, totals)
|
|
|
|
|
assert blocks[0]["type"] == "header"
|
|
|
|
|
assert "Jun 1 to Jun 7" in blocks[0]["text"]["text"]
|
|
|
|
|
text = blocks[1]["text"]["text"]
|
|
|
|
|
assert "Alice" in text and "$50.00" in text and "1 shift" in text
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 19:22:55 -04:00
|
|
|
class TestBuildSwapRequestBlocks:
|
|
|
|
|
def _action_ids(self, blocks):
|
|
|
|
|
return [
|
|
|
|
|
e["action_id"]
|
|
|
|
|
for b in blocks
|
|
|
|
|
if b["type"] == "actions"
|
|
|
|
|
for e in b["elements"]
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def test_weekday_request_has_accept_decline(self):
|
|
|
|
|
blocks = build_swap_request_blocks("U_REQ", "2026-06-03", "night")
|
|
|
|
|
assert "<@U_REQ>" in blocks[0]["text"]["text"]
|
|
|
|
|
ids = self._action_ids(blocks)
|
|
|
|
|
assert ids == ["swap_accept_2026-06-03", "swap_decline_2026-06-03"]
|
|
|
|
|
|
|
|
|
|
def test_weekend_day_request_uses_day_suffix_and_label(self):
|
|
|
|
|
blocks = build_swap_request_blocks("U_REQ", "2026-06-06", "day")
|
|
|
|
|
assert "Day (8am" in blocks[0]["text"]["text"]
|
|
|
|
|
ids = self._action_ids(blocks)
|
|
|
|
|
assert ids == ["swap_accept_2026-06-06_day", "swap_decline_2026-06-06_day"]
|
|
|
|
|
|
|
|
|
|
def test_button_styles(self):
|
|
|
|
|
elements = build_swap_request_blocks("U_REQ", "2026-06-03", "night")[1][
|
|
|
|
|
"elements"
|
|
|
|
|
]
|
|
|
|
|
assert elements[0]["style"] == "primary" # Accept
|
|
|
|
|
assert elements[1]["style"] == "danger" # Decline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildSwapResolvedBlocks:
|
|
|
|
|
def test_renders_text_no_buttons(self):
|
|
|
|
|
blocks = build_swap_resolved_blocks("All done.")
|
|
|
|
|
assert blocks == [
|
|
|
|
|
{"type": "section", "text": {"type": "mrkdwn", "text": "All done."}}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
Add pytest suite and wire it into CI (#85) (#86)
* Add pytest suite and wire it into CI
Stands up the first automated tests for the repo (151 tests) and turns on
the CI test step.
- Lift slack-bot handlers out of create_app() closures to module level so
they're unit-testable; create_app is now a thin Bolt-wiring layer. No
behavior change (handler entrypoints and create_app signature unchanged).
- tests/ mirrors src/: shared layer (schedule, blocks, 3CX client,
ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/
admin/register/rate, pickup button, roster sync, queue scheduler).
- All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via
responses, Slack via fakes, time via freezegun. No real network/AWS.
- pyproject.toml pytest config (pythonpath=src/shared, importlib mode);
per-package conftest loads each app.py under a unique name to avoid the
four-app.py collision. tests/requirements.txt for test-only deps.
- ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint
the tests dir too.
- README Testing section.
Closes #85
* Add least-privilege permissions block to CI workflow
Resolves the CodeQL actions/missing-workflow-permissions alert: the CI
workflow now restricts GITHUB_TOKEN to contents: read (it only checks out,
lints, and runs tests).
* Stop logging extension numbers in 3CX queue updates
Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the
queue/ring-group forwarding logs no longer include the routed extension
values (closed/holiday/extension). Non-sensitive context (resource id,
queue number) is retained.
2026-06-01 19:07:08 -04:00
|
|
|
class TestBuildHelpBlocks:
|
|
|
|
|
def test_non_admin_excludes_admin_section(self):
|
|
|
|
|
text = build_help_blocks(is_admin=False)[0]["text"]["text"]
|
|
|
|
|
assert "Admin Commands" not in text
|
|
|
|
|
|
|
|
|
|
def test_admin_includes_admin_section(self):
|
|
|
|
|
text = build_help_blocks(is_admin=True)[0]["text"]["text"]
|
|
|
|
|
assert "Admin Commands" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildRosterBlocks:
|
|
|
|
|
def test_empty_roster(self):
|
|
|
|
|
text = build_roster_blocks([])[0]["text"]["text"]
|
|
|
|
|
assert "No employees" in text
|
|
|
|
|
|
|
|
|
|
def test_linked_and_unlinked(self):
|
|
|
|
|
roster = [
|
|
|
|
|
{"SK": "115", "name": "Bob", "slack_user_id": ""},
|
|
|
|
|
{"SK": "114", "name": "Alice", "slack_user_id": "U_ALICE"},
|
|
|
|
|
]
|
|
|
|
|
text = build_roster_blocks(roster)[0]["text"]["text"]
|
|
|
|
|
# Sorted by extension → Alice (114) appears before Bob (115)
|
|
|
|
|
assert text.index("Alice") < text.index("Bob")
|
|
|
|
|
assert "<@U_ALICE>" in text
|
|
|
|
|
assert "_not linked_" in text
|
Add changelog-driven releases and App Home tab (#112)
* Add changelog-driven releases and App Home tab
Version the bot continuously from CHANGELOG.md (the single source of
truth for both the version and the staff-readable notes) and surface
changes to users in two ways:
- A new afterhours-release-notifier Lambda posts a "What's New" message
to the shift channel on minor/major releases (patches stay silent).
- The bot gains an App Home "About" tab showing what it does, the
command list, and the current version's notes.
release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN
events don't start downstream workflows), checks out the deployed commit,
and tags + publishes a GitHub Release + invokes the notifier. It assumes a
dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the
notifier; the account's cfn role gates role creation on that boundary.
The manual Version Bump workflow is retired. A CI guard enforces that a
CHANGELOG edit is a clean SemVer bump and that the in-package copy matches.
* Harden release workflow and regex against CodeQL findings
Address three code-scanning alerts on the PR:
- Critical (actions/untrusted-checkout): split release.yaml into a
read-only `prepare` job that checks out and runs repo code, and a
privileged `publish` job (contents:write + OIDC) that never checks out
repo code — it tags, releases, and invokes purely through the GitHub
and AWS APIs. Also assert head_branch == main.
- High x2 (py/polynomial-redos): rewrite the italic and link regexes in
markdown_to_mrkdwn with possessive quantifiers and exclusive character
classes so they run in linear time on adversarial input. Adds a
regression test.
* Move release/announce into Deploy workflow to clear CodeQL
The workflow_run-triggered release.yaml kept tripping CodeQL's
privileged-context rules (untrusted-checkout, then cache-poisoning) —
CodeQL distrusts any workflow_run that checks out a ref, regardless of
the main-only guarantee, and there is no autofix.
Fold the release job into deploy.yaml gated on `needs: deploy`. A
push-to-main run is a trusted context, so checking out and running repo
code with write/OIDC is safe there. This still gates on deploy success
and serializes via the deploy concurrency group, and removes the
separate workflow entirely.
2026-06-11 19:41:31 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReleaseAnnouncement:
|
|
|
|
|
def test_markdown_bold_becomes_slack_bold(self):
|
|
|
|
|
assert markdown_to_mrkdwn("**Big news.** text") == "*Big news.* text"
|
|
|
|
|
|
|
|
|
|
def test_markdown_italic_becomes_underscore(self):
|
|
|
|
|
# Single asterisks are italic in Markdown; Slack uses underscores.
|
|
|
|
|
assert markdown_to_mrkdwn("a *note* here") == "a _note_ here"
|
|
|
|
|
|
|
|
|
|
def test_bold_and_italic_together(self):
|
|
|
|
|
out = markdown_to_mrkdwn("**Bold.** Then *(an aside)*")
|
|
|
|
|
assert out == "*Bold.* Then _(an aside)_"
|
|
|
|
|
|
|
|
|
|
def test_links_and_bullets(self):
|
|
|
|
|
out = markdown_to_mrkdwn("- see [docs](http://x)\n- next")
|
|
|
|
|
assert "• see <http://x|docs>" in out
|
|
|
|
|
assert "• next" in out
|
|
|
|
|
|
|
|
|
|
def test_adversarial_input_runs_in_linear_time(self):
|
|
|
|
|
# py/polynomial-redos regression: possessive quantifiers must keep these
|
|
|
|
|
# patterns from catastrophic backtracking. Pathological inputs that would
|
|
|
|
|
# hang a backtracking engine complete effectively instantly here.
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
for evil in ("*" + "*a" * 4000, "[" + "[\\(" * 4000, "[" + "](" * 4000):
|
|
|
|
|
start = time.perf_counter()
|
|
|
|
|
markdown_to_mrkdwn(evil)
|
|
|
|
|
assert time.perf_counter() - start < 1.0
|
|
|
|
|
|
|
|
|
|
def test_blocks_have_header_and_notes(self):
|
|
|
|
|
blocks = build_release_announcement_blocks(
|
|
|
|
|
"1.10.0", "**Release notes** now self-announce.", "June 11, 2026"
|
|
|
|
|
)
|
|
|
|
|
assert blocks[0]["type"] == "header"
|
|
|
|
|
assert blocks[0]["text"]["text"] == "What's New — v1.10.0"
|
|
|
|
|
assert any(b.get("type") == "context" for b in blocks)
|
|
|
|
|
section = blocks[-1]
|
|
|
|
|
assert section["type"] == "section"
|
|
|
|
|
assert "*Release notes*" in section["text"]["text"]
|
|
|
|
|
|
|
|
|
|
def test_date_label_optional(self):
|
|
|
|
|
blocks = build_release_announcement_blocks("2.0.0", "Notes.")
|
|
|
|
|
assert not any(b.get("type") == "context" for b in blocks)
|
|
|
|
|
|
|
|
|
|
def test_long_notes_truncated_under_section_limit(self):
|
|
|
|
|
blocks = build_release_announcement_blocks("1.10.0", "x " * 3000)
|
|
|
|
|
assert len(blocks[-1]["text"]["text"]) <= 3000
|
|
|
|
|
assert "full changelog" in blocks[-1]["text"]["text"]
|