Skip to content

Testing plan: the kickoff prefix, the declared set, and the refusal that answers

Derived from requirements.md and design.md, before tasks.md. Authored at test-planning; the results section is filled at verification.

This file is executable content. Commands below are what the agent runs; credentials appear by reference only.

Test matrix

#TypeApplies?Scope / what it provesWhere it runs
T1Unityeschannels/repos.py: the declared set from kickoff.repo + poll sources, dedup by key, declared string preserved, a malformed entry skipped, a non-github source ignored, an unreadable polling section contributing nothing. channels/kickoff.py: the grammar (bare / owner/repo / host/owner/repo, case, a four-segment path, ../, $(id), a;b, https://…, a prefix on line two); every row of the resolution table; stripping (first line replaced, later lines kept, a blank first line kept for issue_title to skip, nothing left refused as empty-message); refusal_text (the four wordings, the cap at twelve, the empty-set wording, no token / no other config value). SlackChannelConfig.kickoff_enabled without kickoff.repouv run --project cli python -m pytest -q cli/tests/test_channels_kickoff.py
T2Integration (scenario)yesthrough process_kickoff and the socket handler: a prefixed kickoff opens the issue in a polled repository with the prefix stripped from title and body and kickoff.labels applied; an ambiguous prefix is refused in the member's own thread with the candidates and creates nothing; a prefix-less message with kickoff.repo behaves exactly as 13.11.1; a prefix-less message with no kickoff.repo is refused with the ask-for-a-prefix replyuv run --project cli python -m pytest -q cli/tests/test_channels_integration.py -k "kickoff"
T3Contract (OpenAPI / GraphQL SDL)n/a — no API route or payload changes
T4End-to-endn/a — the poller arming a created issue is test_poller.py's subject and is unchanged; T2 proves the create call this work item composes
T5UI / visualn/a — the only surface is a Slack message, asserted as text in T1/T2
T6Snapshotn/a — assertions on dataclasses, call arguments and message text
T7Performance / loadn/a — resolution is one regex and a set build per message, both in-process
T8Security / abuse caseyesone negative test per abuse case A1–A7 (requirements.md § Security considerations): an unlisted member gets no reply and no disclosure; an undeclared qualified prefix is never absorbed by the fallback; metacharacters never leave the text; a foreign host matches nothing; the refusal carries no token or other config value; a broken polling section widens nothing; no grant means no readuv run --project cli python -m pytest -q cli/tests/test_channels_kickoff.py -k "unauthorized or undeclared or metachar or host or leak or malformed or grant"
T9Accessibilityn/a — no UI of the-loop's own
T10Migration / upgradeyesa 13.11.1 config parses unchanged (no schema key added or removed); both schema copies identical; docs parity (P3/P4/P5); the event catalog knows every reason emitted; the existing channels, commands, buttons and digest suites greenuv run --project cli python -m pytest -q cli/tests/test_config_schema_parity.py cli/tests/test_docs_parity.py cli/tests/test_eventlog.py cli/tests/test_channels.py cli/tests/test_channels_commands.py cli/tests/test_channels_buttons.py cli/tests/test_channels_integration.py
T11Manual exploratoryn/a — no Slack workspace is reachable from this session; the reviewer's walk-through is the PR briefing's "what to check"
T12Lint / format / typecheck / config validation / full suiteyesthe repository's own gates, as pre-commit and CI run themmake check
T13Security review (gate)yesthe-loop checklist against A1–A7, recorded as evidence; tier 3 needs no human sign-off (humanSignOffMinTier: 4)evidence/security-review.md

Scenarios & requirement trace

RowRequirement(s)Scenario / case
T1R2.1test_the_declared_set_is_kickoff_repo_and_every_poll_source, test_a_declared_entry_keeps_the_operators_own_slug, test_the_set_is_deduplicated_by_key, test_a_malformed_declared_entry_is_skipped
T1R1.2, R1.4test_the_prefix_grammar_accepts_three_shapes, test_the_prefix_is_case_insensitive, test_prose_is_not_a_prefix
T1R1.1, R1.3test_a_bare_prefix_resolves_and_is_stripped, test_a_qualified_prefix_resolves, test_stripping_keeps_the_following_lines, test_a_prefix_alone_is_refused_as_an_empty_message
T1R2.2, R2.3, R2.4test_an_undeclared_qualified_prefix_is_refused, test_an_ambiguous_bare_prefix_is_refused, test_an_unmatched_bare_prefix_falls_back
T1R2.5test_refusal_text_names_the_candidates, test_the_candidate_list_is_capped, test_refusal_text_with_no_declared_repositories
T1R5.1test_status_names_the_fallback_and_how_many_a_prefix_may_pick, test_status_says_when_there_is_no_fallback, test_status_says_kickoff_is_off_without_the_grant
T1R3.1, R3.2, R3.3test_no_prefix_uses_the_fallback, test_no_prefix_and_no_fallback_is_no_target, test_kickoff_is_enabled_without_a_repo
T2R1.1, R1.3, R1.5Scenario: A prefixed kickoff opens its issue in the repository the message named
T2R2.3, R2.5Scenario: An ambiguous kickoff prefix is refused in the thread and creates nothing
T2R3.1, R3.4Scenario: A kickoff with no prefix still opens in the configured fallback repository
T2R3.2, R3.3Scenario: With no fallback repository a kickoff is asked for a prefix, not dropped in silence
T8A1, R2.6test_an_unlisted_member_is_told_nothing
T8A2test_an_undeclared_qualified_prefix_never_falls_back
T8A3test_metacharacters_never_parse_as_a_prefix, test_only_a_declared_slug_reaches_the_writer
T8A4test_a_foreign_host_matches_nothing
T8A5test_the_refusal_carries_no_token_or_other_config
T8A6test_a_malformed_polling_section_widens_nothing
T8A7test_without_the_grant_nothing_is_read_or_answered
T10R3.4, R4.2, R5.1test_config_schema_parity.py, test_docs_parity.py, test_eventlog.py

Verification environment

The repository checkout, uv run --project cli, no network. Slack is the existing FakeSlackClient (cli/tests/test_channels.py, test_channels_integration.py); the ledger is the injected create_issue / post_comment callables process_kickoff already accepts. No credential is read: the bot token is a monkeypatch.setenv fixture value and never appears in evidence.

Evidence to capture

  • evidence/verification.md — the commands of T1, T2, T8, T10, T12 with their pass/fail output (counts, not full logs), and the red-first note for the new suites.
  • evidence/security-review.md — A1–A7, each with the test that closes it.
  • Redaction: no tokens, no member ids beyond the U… fixtures, no repository names beyond the fixtures and the reporter's public list.

Activities checklist

  • [x] Red first: test_channels_kickoff.py fails to import against cd1ae94.
  • [x] T1 unit suite green.
  • [x] T2 scenarios green, each with a Gherkin docstring (testing.gherkinDocstrings: required).
  • [x] T8 abuse cases green, one per A1–A7.
  • [x] T10 migration suites green, including both schema copies.
  • [x] T12 make check green.
  • [x] T13 security review recorded.

Verification results

Executed 2026-09-11 on claude/github-issue-341-k4mghj. Full output and the requirement trace: evidence/verification.md.

RowCommandOutcome
T1pytest -q cli/tests/test_channels_kickoff.py47 passed
T2pytest -q cli/tests/test_channels_integration.py -k kickoff7 passed, 30 deselected (5 new + 2 pre-existing)
T8pytest -q cli/tests/test_channels_kickoff.py -k "unauthorized or undeclared or metachar or host or leak or malformed or grant or told_nothing" + the integration case12 passed + 1 passed
T10the seven migration suites277 passed
T12make check3418 passed, 1 skipped; ruff, markdownlint, format, pyright and config validation clean
T13evidence/security-review.mdA1–A7 closed; tier 3, no human sign-off required

Red first: the unit suite did not collect against cd1ae94 (ImportError: cannot import name 'kickoff' from 'the_loop.channels'). One existing assertion changed on purpose — test_bus.py::test_kickoff_needs_the_grant_and_a_repo pinned the precondition R3.3 removes, and is now …_not_a_repo.

Review comments

Appended by the-loop's record-feedback hook when a human gate approves with comments.

Released under the MIT License.