agentirc-cli 9.2.0__tar.gz → 9.3.0__tar.gz

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.
Files changed (104) hide show
  1. agentirc_cli-9.3.0/.claude/skills/pr-review/scripts/pr-sonar.sh +93 -0
  2. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/scripts/workflow.sh +8 -1
  3. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/CHANGELOG.md +86 -0
  4. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/CLAUDE.md +16 -7
  5. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/PKG-INFO +3 -3
  6. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/server_link.py +1 -1
  7. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +11 -2
  8. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/pyproject.toml +182 -4
  9. agentirc_cli-9.3.0/tests/_helpers.py +96 -0
  10. agentirc_cli-9.3.0/tests/conftest.py +295 -0
  11. agentirc_cli-9.3.0/tests/telemetry/__init__.py +0 -0
  12. agentirc_cli-9.3.0/tests/telemetry/_fakes.py +26 -0
  13. agentirc_cli-9.3.0/tests/telemetry/_metrics_helpers.py +75 -0
  14. agentirc_cli-9.3.0/tests/telemetry/test_audit_emit.py +145 -0
  15. agentirc_cli-9.3.0/tests/telemetry/test_audit_lifecycle.py +69 -0
  16. agentirc_cli-9.3.0/tests/telemetry/test_audit_module.py +306 -0
  17. agentirc_cli-9.3.0/tests/telemetry/test_audit_parse_error.py +177 -0
  18. agentirc_cli-9.3.0/tests/telemetry/test_config.py +26 -0
  19. agentirc_cli-9.3.0/tests/telemetry/test_dispatch_span.py +45 -0
  20. agentirc_cli-9.3.0/tests/telemetry/test_emit_event_span.py +42 -0
  21. agentirc_cli-9.3.0/tests/telemetry/test_metrics_init.py +58 -0
  22. agentirc_cli-9.3.0/tests/telemetry/test_metrics_s2s.py +160 -0
  23. agentirc_cli-9.3.0/tests/telemetry/test_outbound_inject.py +70 -0
  24. agentirc_cli-9.3.0/tests/telemetry/test_parse_error.py +33 -0
  25. agentirc_cli-9.3.0/tests/telemetry/test_s2s_relay_span.py +114 -0
  26. agentirc_cli-9.3.0/tests/telemetry/test_server_init.py +29 -0
  27. agentirc_cli-9.3.0/tests/telemetry/test_server_link_inject.py +124 -0
  28. agentirc_cli-9.3.0/tests/telemetry/test_tracing.py +49 -0
  29. agentirc_cli-9.3.0/tests/test_channel.py +131 -0
  30. agentirc_cli-9.3.0/tests/test_connection.py +135 -0
  31. agentirc_cli-9.3.0/tests/test_discovery.py +169 -0
  32. agentirc_cli-9.3.0/tests/test_events_basic.py +284 -0
  33. agentirc_cli-9.3.0/tests/test_events_catalog.py +70 -0
  34. agentirc_cli-9.3.0/tests/test_events_federation.py +110 -0
  35. agentirc_cli-9.3.0/tests/test_events_history.py +64 -0
  36. agentirc_cli-9.3.0/tests/test_events_lifecycle.py +343 -0
  37. agentirc_cli-9.3.0/tests/test_events_reserved_nick.py +68 -0
  38. agentirc_cli-9.3.0/tests/test_federation.py +1163 -0
  39. agentirc_cli-9.3.0/tests/test_history.py +553 -0
  40. agentirc_cli-9.3.0/tests/test_link_reconnect.py +141 -0
  41. agentirc_cli-9.3.0/tests/test_mentions.py +155 -0
  42. agentirc_cli-9.3.0/tests/test_messaging.py +103 -0
  43. agentirc_cli-9.3.0/tests/test_modes.py +327 -0
  44. agentirc_cli-9.3.0/tests/test_room_persistence.py +75 -0
  45. agentirc_cli-9.3.0/tests/test_rooms_federation.py +63 -0
  46. agentirc_cli-9.3.0/tests/test_rooms_integration.py +110 -0
  47. agentirc_cli-9.3.0/tests/test_server_icon_skill.py +214 -0
  48. agentirc_cli-9.3.0/tests/test_skills.py +212 -0
  49. agentirc_cli-9.3.0/tests/test_threads.py +384 -0
  50. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/uv.lock +3 -118
  51. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/SKILL.md +0 -0
  52. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
  53. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
  54. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
  55. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
  56. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
  57. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.claude/skills.local.yaml.example +0 -0
  58. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.github/workflows/publish.yml +0 -0
  59. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.github/workflows/tests.yml +0 -0
  60. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/.gitignore +0 -0
  61. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/LICENSE +0 -0
  62. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/README.md +0 -0
  63. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/__init__.py +0 -0
  64. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/__main__.py +0 -0
  65. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/__init__.py +0 -0
  66. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/aio.py +0 -0
  67. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/bots/__init__.py +0 -0
  68. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/bots/bot_manager.py +0 -0
  69. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/bots/http_listener.py +0 -0
  70. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/cli_shared/__init__.py +0 -0
  71. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/cli_shared/constants.py +0 -0
  72. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/cli_shared/mesh.py +0 -0
  73. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/constants.py +0 -0
  74. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/pidfile.py +0 -0
  75. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/protocol/__init__.py +0 -0
  76. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/protocol/message.py +0 -0
  77. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/protocol/replies.py +0 -0
  78. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/telemetry/__init__.py +0 -0
  79. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/telemetry/audit.py +0 -0
  80. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/telemetry/context.py +0 -0
  81. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/telemetry/metrics.py +0 -0
  82. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/telemetry/tracing.py +0 -0
  83. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/_internal/virtual_client.py +0 -0
  84. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/channel.py +0 -0
  85. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/cli.py +0 -0
  86. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/client.py +0 -0
  87. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/config.py +0 -0
  88. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/events.py +0 -0
  89. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/history_store.py +0 -0
  90. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/ircd.py +0 -0
  91. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/protocol.py +0 -0
  92. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/remote_client.py +0 -0
  93. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/room_store.py +0 -0
  94. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/rooms_util.py +0 -0
  95. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/skill.py +0 -0
  96. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/skills/__init__.py +0 -0
  97. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/skills/history.py +0 -0
  98. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/skills/icon.py +0 -0
  99. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/skills/rooms.py +0 -0
  100. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/skills/threads.py +0 -0
  101. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/agentirc/thread_store.py +0 -0
  102. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/docs/steward/onboarding.md +0 -0
  103. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/tests/__init__.py +0 -0
  104. {agentirc_cli-9.2.0 → agentirc_cli-9.3.0}/tests/test_cli.py +0 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # pr-sonar.sh — list every SonarCloud finding attached to a PR.
3
+ #
4
+ # Surfaces what the GitHub-side `poll` cannot: SonarCloud issues,
5
+ # security hotspots, and the duplication breakdown. The
6
+ # sonarqubecloud[bot] PR comment only links to the dashboard — actual
7
+ # findings live behind the SonarCloud API.
8
+ #
9
+ # Usage: pr-sonar.sh [--repo OWNER/REPO] [--sonar-key KEY] PR_NUMBER
10
+ #
11
+ # Defaults:
12
+ # --repo auto-detected via `gh repo view`
13
+ # --sonar-key derived from repo as `<owner>_<name>`
14
+ #
15
+ # Anonymous SonarCloud access is sufficient for public projects.
16
+ # Requires: gh, jq, curl, python3.
17
+
18
+ set -euo pipefail
19
+
20
+ REPO=""
21
+ SONAR_KEY=""
22
+
23
+ while [[ $# -gt 0 ]]; do
24
+ case "$1" in
25
+ --repo) REPO="$2"; shift 2 ;;
26
+ --sonar-key) SONAR_KEY="$2"; shift 2 ;;
27
+ *) break ;;
28
+ esac
29
+ done
30
+
31
+ PR_NUMBER="${1:?Usage: pr-sonar.sh [--repo OWNER/REPO] [--sonar-key KEY] PR_NUMBER}"
32
+
33
+ if [[ -z "$REPO" ]]; then
34
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
35
+ fi
36
+ if [[ -z "$SONAR_KEY" ]]; then
37
+ SONAR_KEY="${REPO%%/*}_${REPO##*/}"
38
+ fi
39
+
40
+ echo "════════════════ SONARCLOUD (project=$SONAR_KEY, PR=$PR_NUMBER) ════════════════"
41
+
42
+ # ── Quality gate
43
+ QG=$(curl -fsS "https://sonarcloud.io/api/qualitygates/project_status?projectKey=${SONAR_KEY}&pullRequest=${PR_NUMBER}" || echo '{}')
44
+ QG_STATUS=$(echo "$QG" | jq -r '.projectStatus.status // "UNKNOWN"')
45
+ echo "Quality Gate: $QG_STATUS"
46
+ echo "$QG" | jq -r '.projectStatus.conditions[]? | select(.status != "OK") | " ✗ \(.metricKey) = \(.actualValue) (threshold \(.comparator) \(.errorThreshold))"' || true
47
+
48
+ # ── Issues (BUG / VULNERABILITY / CODE_SMELL)
49
+ ISSUES=$(curl -fsS "https://sonarcloud.io/api/issues/search?componentKeys=${SONAR_KEY}&pullRequest=${PR_NUMBER}&statuses=OPEN,CONFIRMED&ps=500" || echo '{}')
50
+ ISSUE_TOTAL=$(echo "$ISSUES" | jq -r '.total // 0')
51
+ echo
52
+ echo "── Issues ($ISSUE_TOTAL OPEN/CONFIRMED) ─────────────────────────────"
53
+ if [[ "$ISSUE_TOTAL" != "0" ]]; then
54
+ echo "$ISSUES" | jq -r '
55
+ .issues[] |
56
+ " [\(.rule)] \(.severity) \(.component | sub("^[^:]+:"; ""))(:\(.line // "?"))\n \(.message)"
57
+ '
58
+ fi
59
+
60
+ # ── Security hotspots
61
+ HOTSPOTS=$(curl -fsS "https://sonarcloud.io/api/hotspots/search?projectKey=${SONAR_KEY}&pullRequest=${PR_NUMBER}&status=TO_REVIEW&ps=500" || echo '{}')
62
+ HOTSPOT_TOTAL=$(echo "$HOTSPOTS" | jq -r '.paging.total // 0')
63
+ echo
64
+ echo "── Hotspots ($HOTSPOT_TOTAL TO_REVIEW) ──────────────────────────────"
65
+ if [[ "$HOTSPOT_TOTAL" != "0" ]]; then
66
+ echo "$HOTSPOTS" | jq -r '
67
+ .hotspots[] |
68
+ " [\(.ruleKey)] \(.vulnerabilityProbability) \(.component | sub("^[^:]+:"; ""))(:\(.line // "?"))\n \(.message)"
69
+ '
70
+ fi
71
+
72
+ # ── Duplication
73
+ MEAS=$(curl -fsS "https://sonarcloud.io/api/measures/component?component=${SONAR_KEY}&pullRequest=${PR_NUMBER}&metricKeys=new_duplicated_lines_density,new_duplicated_lines,new_duplicated_blocks" || echo '{}')
74
+ DUP_PCT=$(echo "$MEAS" | jq -r '.component.measures[]? | select(.metric == "new_duplicated_lines_density") | .periods[0].value // ""')
75
+ DUP_LINES=$(echo "$MEAS" | jq -r '.component.measures[]? | select(.metric == "new_duplicated_lines") | .periods[0].value // ""')
76
+ DUP_BLOCKS=$(echo "$MEAS" | jq -r '.component.measures[]? | select(.metric == "new_duplicated_blocks") | .periods[0].value // ""')
77
+ echo
78
+ echo "── Duplication on new code ──────────────────────────────────────────"
79
+ echo " density: ${DUP_PCT:-?}% lines: ${DUP_LINES:-?} blocks: ${DUP_BLOCKS:-?}"
80
+
81
+ if [[ "${DUP_BLOCKS:-0}" != "0" && -n "${DUP_BLOCKS:-}" ]]; then
82
+ # Per-file duplication: list every file with duplicated_lines on new code.
83
+ # Sonar doesn't expose per-PR duplication-by-file directly; surface the
84
+ # files that appear in the issues + hotspots as a weak proxy.
85
+ DUP_FILES=$(echo "$ISSUES" | jq -r '[.issues[] | .component | sub("^[^:]+:"; "")] | unique | .[]')
86
+ if [[ -n "$DUP_FILES" ]]; then
87
+ echo " files with findings (likely duplication source):"
88
+ echo "$DUP_FILES" | sed 's/^/ - /'
89
+ fi
90
+ fi
91
+
92
+ echo
93
+ echo "Dashboard: https://sonarcloud.io/dashboard?id=${SONAR_KEY}&pullRequest=${PR_NUMBER}"
@@ -3,7 +3,8 @@
3
3
  #
4
4
  # Subcommands:
5
5
  # lint run the portability lint on the current diff (staged + unstaged)
6
- # poll <PR> fetch and display review comments
6
+ # poll <PR> fetch and display review comments + SonarCloud findings
7
+ # sonar <PR> list every SonarCloud issue / hotspot / duplication on a PR
7
8
  # reply <PR> batch reply to review comments (JSONL on stdin), --resolve
8
9
  # delta dump CLAUDE.md head + culture.yaml for each sibling project
9
10
  # listed in skills.local.yaml (alignment-delta check)
@@ -53,6 +54,12 @@ case "$cmd" in
53
54
  poll)
54
55
  PR="${1:?Usage: workflow.sh poll <PR>}"
55
56
  bash "$SCRIPT_DIR/pr-comments.sh" "$PR"
57
+ echo
58
+ bash "$SCRIPT_DIR/pr-sonar.sh" "$PR"
59
+ ;;
60
+ sonar)
61
+ PR="${1:?Usage: workflow.sh sonar <PR>}"
62
+ bash "$SCRIPT_DIR/pr-sonar.sh" "$PR"
56
63
  ;;
57
64
  reply)
58
65
  PR="${1:?Usage: workflow.sh reply <PR> (JSONL on stdin)}"
@@ -4,6 +4,92 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [9.3.0] - 2026-05-01
8
+
9
+ ### Added
10
+
11
+ - Test suite migration (PR-B3): 36 server-core / telemetry tests
12
+ vendored from `culture@df50942` (~6,500 LOC). 315 tests run under
13
+ `pytest -n auto` in ~29 seconds.
14
+ - 21 server-core tests in `tests/` cover IRC lifecycle, channels,
15
+ rooms, threads, history, federation (S2S), events, mentions,
16
+ and the icon skill.
17
+ - 15 telemetry integration tests in `tests/telemetry/` cover audit
18
+ JSONL emission, OTLP span injection on dispatch, S2S relay
19
+ spans, metrics initialization, and trace-context propagation.
20
+ - Test helper modules `_fakes.py` and `_metrics_helpers.py` also
21
+ vendored verbatim under the same package.
22
+ - `tests/conftest.py` — adapted from culture. Drops the
23
+ `_BOTS_DIR_*` patches and the `server_with_bot` /
24
+ `server_with_bots` fixtures (see Changed below). Keeps the
25
+ `IRCTestClient` raw-TCP helper and the IRCd lifecycle, telemetry,
26
+ and audit fixtures.
27
+ - `.claude/skills/pr-review/scripts/pr-sonar.sh` — new script that
28
+ fetches every SonarCloud issue, security hotspot, and duplication
29
+ measure for a PR via the SonarCloud API. `workflow.sh poll` now
30
+ runs it after `pr-comments.sh`; `workflow.sh sonar <PR>` runs it
31
+ standalone. Closes the gap where the GitHub-side poll only saw
32
+ the SonarCloud bot's "Quality Gate failed" link without the
33
+ underlying findings. Re-vendor to `steward` after this PR merges.
34
+ - Three `[tool.citation]` packages:
35
+ `culture-tests-conftest` (paraphrase),
36
+ `culture-tests-server-core` (mostly quote, paraphrase for
37
+ `test_events_basic.py`), and `culture-tests-telemetry` (mostly
38
+ quote, paraphrase for `test_tracing.py`).
39
+
40
+ ### Changed
41
+
42
+ - `agentirc/server_link.py:_replay_event` — parameter renamed from
43
+ `_seq` (PR-B1's unused-arg compliance variant) back to `seq` to
44
+ match the upstream signature culture's tests assume; signature
45
+ carries `# noqa: ARG002 # NOSONAR S1172`. Hash refreshed.
46
+
47
+ ### Fixed (post-review)
48
+
49
+ - `requires-python = ">=3.10"` → `">=3.11"`; classifiers updated
50
+ (drop 3.10, add 3.13). Resolves Copilot/Qodo "asyncio.timeout
51
+ not in 3.10" findings. The 3.10 floor was inherited from PR-B1/B2
52
+ and would have ImportError'd on the first test run.
53
+ - 2 SonarCloud `python:S5332` security hotspots cleared via
54
+ `# NOSONAR S5332` annotations on `tests/telemetry/test_config.py`'s
55
+ localhost OTLP test fixtures (URLs never reach the wire).
56
+ - New `tests/_helpers.py` (agentirc-native, not cited) extracts the
57
+ duplicated "boot two linked IRCds" pattern into `boot_linked_pair`
58
+ + `link_pair`. Refactored 9 sites that previously inlined ~22
59
+ lines of identical scaffolding: 5 in `test_federation.py`, 3 in
60
+ `test_link_reconnect.py`, plus the `linked_servers` conftest
61
+ fixture. Drops ~180 duplicated lines, addressing SonarCloud's
62
+ >3% duplication threshold while keeping the cited test bodies
63
+ readable. Re-snapshots from culture replay this extraction
64
+ mechanically.
65
+ - 42 SonarCloud OPEN issues cleared via inline `# NOSONAR <rule>`
66
+ annotations: `python:S1172` (server_link `_replay_event`'s upstream
67
+ signature compat), `python:S2068` (3 test fixture passwords),
68
+ `python:S7483` (3 `IRCTestClient.recv*` / `_wait_for_span` timeout
69
+ params kept by upstream design — see `RECV_TIMEOUT_SECONDS`
70
+ module-level note), `python:S7494` (2 `dict(t.split("=") for t in
71
+ tags)` patterns vendored verbatim from culture), `python:S125`
72
+ (1 `# 311 RPL_WHOISUSER` documentation comment SonarCloud
73
+ misclassified as commented-out code), `python:S1481` × 21
74
+ (`server_a, server_b = linked_servers` tuple-unpack sites where
75
+ one or both vars are used by the federation fixture but unread in
76
+ the test body — same idiom across 3 files; underscore-prefix
77
+ rename would diverge from culture upstream and complicate
78
+ re-snapshots).
79
+
80
+ ### Deferred / out of scope
81
+
82
+ - Three bot-fixtured telemetry tests
83
+ (`test_bot_event_dispatch_span.py`, `test_bot_run_span.py`,
84
+ `test_metrics_bots.py`) and `test_welcome_bot.py` stay in culture
85
+ — they depend on the real `BotManager` (forbidden by agentirc's
86
+ dependency boundary). Bucket C tests (cli/console/daemon/clients)
87
+ also remain in culture indefinitely.
88
+ - `tests/telemetry/conftest.py` is not migrated; its only consumer
89
+ was the deferred bot-fixtured tests above.
90
+ - Bootstrap docs (`docs/api-stability.md`, `docs/cli.md`,
91
+ `docs/deployment.md`) ship in PR-B4.
92
+
7
93
  ## [9.2.0] - 2026-05-01
8
94
 
9
95
  ### Added
@@ -2,20 +2,21 @@
2
2
 
3
3
  This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
4
 
5
- ## Current state: server-core + real CLI + protocol module landed (9.2.0)
5
+ ## Current state: bootstrap functionally complete (9.3.0); docs slice (PR-B4) remains
6
6
 
7
- This repo is the agentirc server-core extraction out of the sibling project [`culture`](https://github.com/OriNachum/culture). As of 9.2.0:
7
+ This repo is the agentirc server-core extraction out of the sibling project [`culture`](https://github.com/OriNachum/culture). As of 9.3.0:
8
8
 
9
9
  - **Server-core** (`agentirc/{ircd,server_link,channel,events,skill,remote_client,…}.py`, `agentirc/skills/{rooms,threads,history,icon}.py`) — vendored from `culture@df50942` via the `cite-don't-copy` pattern (see `[tool.citation]` in `pyproject.toml`).
10
10
  - **Client transport** (`agentirc/client.py`) — vendored from `culture/agentirc/client.py` in PR-B2. The bootstrap spec originally said this would "stay in culture", but the dependency-boundary analysis after PR-B1 showed `client.py` only imports already-vendored support modules plus opentelemetry. Without it, `agentirc/ircd.py:580`'s runtime `from agentirc.client import Client` raised `ImportError` on the first TCP IRC connection.
11
11
  - **Public CLI** (`agentirc/cli.py`) — real verb dispatch extracted from `culture/cli/server.py`. Verbs: `serve` (foreground, no PID; for systemd `Type=simple` and containers), `start`/`stop`/`status` (lifecycle), `restart`, `link` (peer-spec validator), `logs` (cat / tail of `~/.culture/logs/server-<name>.log`), `version`.
12
12
  - **Public protocol** (`agentirc/protocol.py`) — verb name constants, numerics, IRCv3 tag names. Wire-format quirks (`ROOMETAEND`, `ROOMETASET` typos, `ERR_NOSUCHCHANNEL` semantic misuse, `STHREAD` verb collapse) preserved verbatim — they need coordinated cross-repo bumps to fix.
13
+ - **Test suite** (PR-B3, 9.3.0) — 36 tests vendored from `culture@df50942` (~6.5kloc), 315 tests run under `pytest -n auto` in ~29s on default workers. Three telemetry tests (`test_bot_event_dispatch_span`, `test_bot_run_span`, `test_metrics_bots`) and `test_welcome_bot` stay in culture because they depend on the real `BotManager`. `tests/conftest.py` was adapted to drop bot-loader sandboxing and the `server_with_bot` / `server_with_bots` fixtures.
13
14
  - **Internal support** (`agentirc/_internal/`) — `aio`, `constants`, `protocol/`, `telemetry/`, `virtual_client`, `pidfile` (PR-B2), `cli_shared/` (PR-B2), `bots/` stubs.
14
15
 
15
16
  End-to-end verified: `agentirc start --port <p>` boots a real IRCd, TCP NICK/USER handshake returns `001 RPL_WELCOME`, `agentirc stop` shuts cleanly. `agentirc serve` is byte-indistinguishable from `culture server start` for the lifecycle contract culture's shim relies on.
16
17
 
17
18
  What is **not** done yet:
18
- - **Test suite migration** is PR-B3 only remaining bootstrap slice.
19
+ - **Bootstrap docs (PR-B4)** — `docs/api-stability.md`, `docs/cli.md`, `docs/deployment.md`. Pure prose; not gating on culture's cutover (the public API surface is already importable; PR-B4 just documents the contract).
19
20
 
20
21
  Read the bootstrap spec at `docs/superpowers/specs/2026-04-30-bootstrap-design.md` for the full plan; it is the operative source of truth and is intentionally self-contained. The culture-side counterpart spec is at `../culture/docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md` — not normally needed, but explains *why* if a decision looks arbitrary.
21
22
 
@@ -44,6 +45,13 @@ There are three different names in play. Don't conflate them:
44
45
 
45
46
  When migrating tests, the rule is: pure server tests come here, transport tests **also** come here now that we own `client.py`, mixed tests stay in culture and get rewritten to drive `agentirc serve` as a subprocess fixture rather than importing `IRCd` directly. When unsure, **prefer copying the test here** — this repo owns the IRCd and the client transport.
46
47
 
48
+ ### Test layout (since 9.3.0)
49
+
50
+ - `tests/conftest.py` — paraphrase of culture's conftest. Drops the `_BOTS_DIR_*` `unittest.mock.patch` calls (no-op against agentirc's bot stubs) and the `server_with_bot` / `server_with_bots` fixtures. Keeps `IRCTestClient`, the IRCd lifecycle fixtures (`server`, `linked_servers`, `make_client*`, `server_welcome_disabled`), telemetry fixtures (`tracing_exporter`, `metrics_reader`, `audit_dir`), and the `TEST_LINK_PASSWORD` constant.
51
+ - `tests/test_*.py` — 21 server-core tests + the agentirc-native `test_cli.py`. Cover IRC lifecycle, channels, rooms, threads, history, federation, events, mentions, the icon skill.
52
+ - `tests/telemetry/test_*.py` — 15 telemetry integration tests covering audit JSONL emission, OTLP span injection on dispatch, S2S relay spans, metrics initialization, trace-context propagation. Uses two private helper modules `_fakes.py` (FakeWriter etc.) and `_metrics_helpers.py`. No `tests/telemetry/conftest.py` (the upstream one was bot-coupled).
53
+ - Tests left in culture: `test_bot_event_dispatch_span.py`, `test_bot_run_span.py`, `test_metrics_bots.py`, `test_welcome_bot.py` (bot-manager-coupled), plus the entire bucket-C surface (cli, console, daemon, clients, credentials).
54
+
47
55
  ## Public API contract (semver-tracked)
48
56
 
49
57
  Only three modules are public. Everything else is internal and may be refactored without a major bump.
@@ -82,7 +90,7 @@ Do not rename on-disk artifacts during the bootstrap. That is explicitly out of
82
90
  # Dev setup
83
91
  uv venv && uv pip install -e ".[dev]"
84
92
 
85
- # Tests (the spec mandates parallel; no tests yet — collected 0)
93
+ # Tests (315 collected, ~29s on default workers)
86
94
  pytest -n auto
87
95
 
88
96
  # Run a single test
@@ -91,7 +99,7 @@ pytest tests/path/to/test_file.py::test_name -v
91
99
  # CLI smoke
92
100
  agentirc --help
93
101
  agentirc-cli --help # alias of agentirc
94
- agentirc version # prints "agentirc 9.2.0"
102
+ agentirc version # prints "agentirc 9.3.0"
95
103
  python -m agentirc version # equivalent
96
104
 
97
105
  # Lifecycle (functional since 9.2.0)
@@ -146,7 +154,8 @@ Per-machine paths for these skills go in `.claude/skills.local.yaml` (gitignored
146
154
 
147
155
  The full list lives in §"Acceptance criteria" of the bootstrap spec. The non-obvious ones:
148
156
 
149
- - `pip install agentirc-cli==9.2.0` on a clean venv produces working `agentirc` *and* `agentirc-cli` binaries (both pointing at `agentirc.cli:main`). ✅ since 9.0.0.
157
+ - `pip install agentirc-cli==9.3.0` on a clean venv produces working `agentirc` *and* `agentirc-cli` binaries (both pointing at `agentirc.cli:main`). ✅ since 9.0.0.
150
158
  - `agentirc serve` is byte-indistinguishable from `culture server start` (same socket, same logs, same systemd integration). ✅ since 9.2.0.
151
159
  - `agentirc.config.{ServerConfig, LinkConfig, TelemetryConfig}`, `agentirc.cli.{main, dispatch}`, `agentirc.protocol.*` all import from a clean Python session. ✅ since 9.2.0.
152
- - `docs/api-stability.md` names the three public modules. pending.
160
+ - `pytest -n auto` passes for the migrated suite (315 tests, ~29s). since 9.3.0.
161
+ - `docs/api-stability.md` names the three public modules. ⏳ pending PR-B4.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 9.2.0
3
+ Version: 9.3.0
4
4
  Summary: Agent-friendly IRCd: server core for AI agent meshes
5
5
  Project-URL: Homepage, https://github.com/OriNachum/agentirc
6
6
  Project-URL: Issues, https://github.com/OriNachum/agentirc/issues
@@ -32,11 +32,11 @@ Classifier: Development Status :: 3 - Alpha
32
32
  Classifier: Intended Audience :: Developers
33
33
  Classifier: License :: OSI Approved :: MIT License
34
34
  Classifier: Programming Language :: Python :: 3
35
- Classifier: Programming Language :: Python :: 3.10
36
35
  Classifier: Programming Language :: Python :: 3.11
37
36
  Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Programming Language :: Python :: 3.13
38
38
  Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
39
- Requires-Python: >=3.10
39
+ Requires-Python: >=3.11
40
40
  Requires-Dist: opentelemetry-api>=1.22
41
41
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.22
42
42
  Requires-Dist: opentelemetry-sdk>=1.22
@@ -968,7 +968,7 @@ class ServerLink:
968
968
  except ValueError:
969
969
  pass
970
970
 
971
- async def _replay_event(self, _seq: int, event: Event) -> None:
971
+ async def _replay_event(self, seq: int, event: Event) -> None: # noqa: ARG002 # NOSONAR S1172 — kept for upstream test signature compat
972
972
  """Replay a single event to the peer as S2S wire format."""
973
973
  origin = self.server.config.name
974
974
  # Federated events arrive with event.type as either an EventType enum
@@ -172,8 +172,8 @@ Wire-format quirks (`ROOMETAEND`, `ROOMETASET` typos; `ERR_NOSUCHCHANNEL` semant
172
172
  > - **Shape A — package skeleton** ✅ (PR #2, `9.0.0`): Tasks 1, 6, and a stub form of 9.
173
173
  > - **Shape B-1 — server-core extraction** ✅ (PR #3, `9.1.0`): Tasks 2, 4, 9 (runtime deps), partial 10. See the "Cite-don't-copy" subsection below.
174
174
  > - **Shape B-2 — real CLI + `protocol.py` + `client.py`** ✅ (PR-B2, `9.2.0`): Tasks 5, 7, plus vendoring `culture.pidfile`, `culture.cli.shared` (subset), and `culture/agentirc/client.py`. The bootstrap spec previously said `client.py` "stays in culture"; that decision was reversed in PR-B2 because (a) `agentirc/ircd.py:580`'s runtime `from agentirc.client import Client` was a guaranteed `ImportError` without it, and (b) `client.py` only imports already-vendored support modules — no backend-SDK pull-through.
175
- > - **Shape B-3 — test suite migration** (next PR): Task 8.
176
- > - **Remaining**: 10 (pre-commit + CI), 11–12 (docs), 13–18 (test run, acceptance, tag, publish, report-back).
175
+ > - **Shape B-3 — test suite migration** (PR-B3, `9.3.0`): Task 8 + Task 13. 36 tests vendored from `culture@df50942` (~6.5kloc), 315 tests run under `pytest -n auto` in ~29s. `tests/conftest.py` adapted (paraphrase) to drop bot-loader sandboxing and bot-fixture definitions. Three telemetry/test_bot_*.py files plus `test_welcome_bot.py` stay in culture (BotManager-coupled). Bucket-C tests stay in culture indefinitely.
176
+ > - **Remaining**: 11–12 (docs — `api-stability.md`, `cli.md`, `deployment.md`, ship in PR-B4), 14 (acceptance criteria spot-check), 16–18 (tag, publish, report-back to culture).
177
177
  >
178
178
  > Task 3 (`Copy protocol/extensions/ wholesale`) is dropped: that path doesn't exist in culture. Re-add only if/when culture creates it.
179
179
 
@@ -219,6 +219,15 @@ Tests from culture are sorted into three buckets:
219
219
 
220
220
  When in doubt, prefer moving tests *here* over leaving them in culture: this repo owns the IRCd, the client transport, and IRCd-internal tests should run in this repo's CI.
221
221
 
222
+ **Realised in PR-B3 (9.3.0):**
223
+
224
+ - 21 root server-core tests + 15 telemetry tests = 36 files (~6.5kloc) vendored verbatim with mechanical import rewrites (`culture.agentirc.X` → `agentirc.X`, `culture.protocol.message` → `agentirc._internal.protocol.message`, `culture.telemetry.X` → `agentirc._internal.telemetry.X`, `culture.agentirc.client` → `agentirc.client`).
225
+ - `tests/conftest.py` adapted as paraphrase: dropped the `_BOTS_DIR_*` `unittest.mock.patch` calls (no-op against agentirc's no-op `_internal/bots/` stubs) and the `server_with_bot` / `server_with_bots` fixtures (`culture.bots.*` is forbidden).
226
+ - `tests/telemetry/test_tracing.py` paraphrase: one `unittest.mock.patch` target rewritten from `"culture.telemetry.tracing.OTLPSpanExporter"` to `"agentirc._internal.telemetry.tracing.OTLPSpanExporter"`. OTEL service-name strings (`"culture.agentirc"`) and OTEL attribute keys (`"culture.s2s.*"`, `"culture.federation.peer"`, `"culture.dev/traceparent"`) preserved verbatim — they are public observability identifiers downstream consumers grep for.
227
+ - `tests/test_events_basic.py` paraphrase: `with patch("culture.bots.bot_manager.BOTS_DIR", …)` block removed (same dead-weight rationale as the conftest patches).
228
+ - `agentirc/server_link.py:_replay_event` parameter renamed from `_seq` (PR-B1's unused-arg compliance variant) back to `seq` to match the upstream signature culture's tests assume; signature carries a `# noqa: ARG002` to keep the linter quiet.
229
+ - **Stayed in culture:** `test_bot_event_dispatch_span.py`, `test_bot_run_span.py`, `test_metrics_bots.py` (need real `BotManager` for the bot-event path), `test_welcome_bot.py` (inspects `bot_manager.bots`), and the 57-file bucket-C surface (cli, console, daemon, clients, credentials, mesh_config). `tests/telemetry/conftest.py` not migrated — its only consumer was the deferred bot tests above.
230
+
222
231
  ## Acceptance criteria
223
232
 
224
233
  - `pip install agentirc-cli==9.0.0` from PyPI produces working `agentirc` and `agentirc-cli` binaries on a clean venv (both pointing at `agentirc.cli:main`).
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "agentirc-cli"
3
- version = "9.2.0"
3
+ version = "9.3.0"
4
4
  description = "Agent-friendly IRCd: server core for AI agent meshes"
5
5
  readme = "README.md"
6
- requires-python = ">=3.10"
6
+ requires-python = ">=3.11"
7
7
  authors = [
8
8
  { name = "Ori Nachum" },
9
9
  ]
@@ -19,9 +19,9 @@ classifiers = [
19
19
  "Intended Audience :: Developers",
20
20
  "License :: OSI Approved :: MIT License",
21
21
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.10",
23
22
  "Programming Language :: Python :: 3.11",
24
23
  "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
25
  "Topic :: Communications :: Chat :: Internet Relay Chat",
26
26
  ]
27
27
  dependencies = [
@@ -245,7 +245,7 @@ sha256 = "477760b85e4acf861a92aaf0ea73550c07267e3c33740a412a3075598bfb68aa"
245
245
 
246
246
  [tool.citation.packages.culture-agentirc-server-core.files."server_link.py"]
247
247
  status = "paraphrase"
248
- sha256 = "709a8a7d2f7836b9bf382f7fb4d5046a61f8eac06e27323d87835615314d6bbe"
248
+ sha256 = "b7c59d64c61bab8ce93595b9a406c7a6d5107ab4361a42f8ccd7ad0f5f7004e7"
249
249
 
250
250
  [tool.citation.packages.culture-agentirc-server-core.files."channel.py"]
251
251
  status = "paraphrase"
@@ -302,3 +302,181 @@ sha256 = "574682bfc9f042e8c1a304db17cfb4b1e1034f384241b83aaaecfb64decb2271"
302
302
  [tool.citation.packages.culture-agentirc-server-core.files."skills/icon.py"]
303
303
  status = "paraphrase"
304
304
  sha256 = "5ef97657599a37476b96aa59968373aed8c82024027297f8300be9886dc18d4a"
305
+
306
+ [tool.citation.packages.culture-tests-conftest]
307
+ schema = 2
308
+ source = "https://github.com/OriNachum/culture/blob/df50942/tests/conftest.py"
309
+ version = "df50942"
310
+ target = "tests/"
311
+ cited = "2026-05-01"
312
+
313
+ [tool.citation.packages.culture-tests-conftest.files."conftest.py"]
314
+ status = "paraphrase"
315
+ sha256 = "3ee6fd43e2833f28a75363503519d63a16fe6f89843471707bf684e544cac360"
316
+
317
+ [tool.citation.packages.culture-tests-server-core]
318
+ schema = 2
319
+ source = "https://github.com/OriNachum/culture/tree/df50942/tests"
320
+ version = "df50942"
321
+ target = "tests/"
322
+ cited = "2026-05-01"
323
+
324
+ [tool.citation.packages.culture-tests-server-core.files."test_channel.py"]
325
+ status = "quote"
326
+ sha256 = "ad335460aa043e9a9e3cce3e70d3ffef4ed0f8ae98138c6b8ec26e16044b1820"
327
+
328
+ [tool.citation.packages.culture-tests-server-core.files."test_connection.py"]
329
+ status = "paraphrase"
330
+ sha256 = "243205febd7cfdca5b129b1ce4e1952802aa68f6999e0dc807ce15ac068dc996"
331
+
332
+ [tool.citation.packages.culture-tests-server-core.files."test_discovery.py"]
333
+ status = "quote"
334
+ sha256 = "50d0dcad3f30f8b01215264147690786013e1753fae01cd682e6a415601394c8"
335
+
336
+ [tool.citation.packages.culture-tests-server-core.files."test_events_basic.py"]
337
+ status = "paraphrase"
338
+ sha256 = "bf1e9175a6167b24c13c0f9da2eff923e7e557a3cb99606d7829a14e63596187"
339
+
340
+ [tool.citation.packages.culture-tests-server-core.files."test_events_catalog.py"]
341
+ status = "quote"
342
+ sha256 = "4e986c18b98c6bd05232c7197c81873554225e3e7492db1b99986ac3039fec37"
343
+
344
+ [tool.citation.packages.culture-tests-server-core.files."test_events_federation.py"]
345
+ status = "quote"
346
+ sha256 = "f79cd2caf57c264243734bc3ce9cd1cd5543afc46ee28b0a1a6c94ae50efa86a"
347
+
348
+ [tool.citation.packages.culture-tests-server-core.files."test_events_history.py"]
349
+ status = "quote"
350
+ sha256 = "2148d1fbb951d5cf7f43d18088a92c268416ef5481837c37ca27cce3367e21db"
351
+
352
+ [tool.citation.packages.culture-tests-server-core.files."test_events_lifecycle.py"]
353
+ status = "quote"
354
+ sha256 = "b60dc1d4eed3c6c9d689ad2a9958eee8f095d1864fbe2a3bbd3dceea52a84728"
355
+
356
+ [tool.citation.packages.culture-tests-server-core.files."test_events_reserved_nick.py"]
357
+ status = "quote"
358
+ sha256 = "f387befe4df163861ec4b597c0fd330745107b3e36ba5604b69d872329418f50"
359
+
360
+ [tool.citation.packages.culture-tests-server-core.files."test_federation.py"]
361
+ status = "paraphrase"
362
+ sha256 = "1a1812cdbbf3adf026dc52c791ceb45b076b4eea7f0aa5f9f048a631628aee1f"
363
+
364
+ [tool.citation.packages.culture-tests-server-core.files."test_history.py"]
365
+ status = "quote"
366
+ sha256 = "066e674c847a54ec62fccd5bbaf1c524883e47e3867f9889279d48603b83ce4d"
367
+
368
+ [tool.citation.packages.culture-tests-server-core.files."test_link_reconnect.py"]
369
+ status = "paraphrase"
370
+ sha256 = "876330d0fa8db1dcabcc352c362e0b59c7b6a02bb689e61a8e1e464af3ecf058"
371
+
372
+ [tool.citation.packages.culture-tests-server-core.files."test_mentions.py"]
373
+ status = "quote"
374
+ sha256 = "2a0ec31bbace495fbee41813c319a3965396faaab24974fede2e8931d835b51e"
375
+
376
+ [tool.citation.packages.culture-tests-server-core.files."test_messaging.py"]
377
+ status = "quote"
378
+ sha256 = "0e6d500f721d5ee5eba0616caeb4822ffbd7bb6f7eb9287be2aa0fea7d7293c5"
379
+
380
+ [tool.citation.packages.culture-tests-server-core.files."test_modes.py"]
381
+ status = "quote"
382
+ sha256 = "35cd36a0171b824d638c89e021283843739eac8afc3dd846ec7e01115e6fd0e6"
383
+
384
+ [tool.citation.packages.culture-tests-server-core.files."test_room_persistence.py"]
385
+ status = "quote"
386
+ sha256 = "3c4e320dc459faccdf86ac67e5a33134fa5a14cc283fe1f6e79a8c09d281cd8c"
387
+
388
+ [tool.citation.packages.culture-tests-server-core.files."test_rooms_federation.py"]
389
+ status = "paraphrase"
390
+ sha256 = "d59f7c9cc95944d1e237fff99ee795f6281dcdccfefb39f4719f43f322085148"
391
+
392
+ [tool.citation.packages.culture-tests-server-core.files."test_rooms_integration.py"]
393
+ status = "quote"
394
+ sha256 = "9dcfb16639533c44bf3633c61b19f3cd75c549cea3cdac9b5624ecf7196ab3eb"
395
+
396
+ [tool.citation.packages.culture-tests-server-core.files."test_server_icon_skill.py"]
397
+ status = "quote"
398
+ sha256 = "c39526445af4861cab4b238ba27b00cc598cd0cf0312cea7e6d558eac62f6bd5"
399
+
400
+ [tool.citation.packages.culture-tests-server-core.files."test_skills.py"]
401
+ status = "quote"
402
+ sha256 = "55b4d18ec526282d87055409bcc6a497a8766d2cad28b67bd5d21df9af95149f"
403
+
404
+ [tool.citation.packages.culture-tests-server-core.files."test_threads.py"]
405
+ status = "quote"
406
+ sha256 = "0e31f65beea7521536c599b51fac36122eb9b42d50918d8c0bcb1d04d7f763bc"
407
+
408
+
409
+ [tool.citation.packages.culture-tests-telemetry]
410
+ schema = 2
411
+ source = "https://github.com/OriNachum/culture/tree/df50942/tests/telemetry"
412
+ version = "df50942"
413
+ target = "tests/telemetry/"
414
+ cited = "2026-05-01"
415
+
416
+ [tool.citation.packages.culture-tests-telemetry.files."_fakes.py"]
417
+ status = "quote"
418
+ sha256 = "8358c340572006317fddadde2a07da75075f1426a3ded4cf04e4aa0b7c0f8b08"
419
+
420
+ [tool.citation.packages.culture-tests-telemetry.files."_metrics_helpers.py"]
421
+ status = "quote"
422
+ sha256 = "1f6a01ecaa8c3577ebdd58eedb7146532688e602a2e39f3a4f323f10b6aa6c4f"
423
+
424
+ [tool.citation.packages.culture-tests-telemetry.files."test_audit_emit.py"]
425
+ status = "quote"
426
+ sha256 = "7b2c618443d5a5ff899ee830453537fb17256c5156c4a7242eee8017d557a2ed"
427
+
428
+ [tool.citation.packages.culture-tests-telemetry.files."test_audit_lifecycle.py"]
429
+ status = "quote"
430
+ sha256 = "24e17aac1d31530ed77e69f59391c30f48c69427ded4a1b28330e04464e7f872"
431
+
432
+ [tool.citation.packages.culture-tests-telemetry.files."test_audit_module.py"]
433
+ status = "quote"
434
+ sha256 = "e88c118abdf7f7f5241919a780417466ecf4f43180ee6dbf5253c9c05dece081"
435
+
436
+ [tool.citation.packages.culture-tests-telemetry.files."test_audit_parse_error.py"]
437
+ status = "quote"
438
+ sha256 = "34cd91ef77811c5024f1534700b005605e706aaa7234bc22524aab85c272df3c"
439
+
440
+ [tool.citation.packages.culture-tests-telemetry.files."test_config.py"]
441
+ status = "paraphrase"
442
+ sha256 = "150f9cb11d5737f154f0f85f8e7e65135482ad86010ca2386215f9a462ad2474"
443
+
444
+ [tool.citation.packages.culture-tests-telemetry.files."test_dispatch_span.py"]
445
+ status = "quote"
446
+ sha256 = "a5116e92d1d88da18f2cd5d026b69c0efbcdf3845c3da4c5f3177dfa46ade7fb"
447
+
448
+ [tool.citation.packages.culture-tests-telemetry.files."test_emit_event_span.py"]
449
+ status = "quote"
450
+ sha256 = "5fb0cf247669717f1add5392d18d3a5639d35d3350982b521a40e3ad031bc7ab"
451
+
452
+ [tool.citation.packages.culture-tests-telemetry.files."test_metrics_init.py"]
453
+ status = "quote"
454
+ sha256 = "bfe2b1166032ec027a0ab06b3124d31a9b9deccb431d0550e14fb83f0a778f21"
455
+
456
+ [tool.citation.packages.culture-tests-telemetry.files."test_metrics_s2s.py"]
457
+ status = "paraphrase"
458
+ sha256 = "b07815fba360afda871fb2ed2996087b5778d912027751da264e188e87a7b41f"
459
+
460
+ [tool.citation.packages.culture-tests-telemetry.files."test_outbound_inject.py"]
461
+ status = "quote"
462
+ sha256 = "1b2a5132dc1b02cb19ca9aa3edc951c2d0431ff9a26e649175eef72faafb7168"
463
+
464
+ [tool.citation.packages.culture-tests-telemetry.files."test_parse_error.py"]
465
+ status = "quote"
466
+ sha256 = "5cfeffe6ccb3b18e1d78669086cc9e6e1ac473f8929eb1d1bcd15523851dd284"
467
+
468
+ [tool.citation.packages.culture-tests-telemetry.files."test_s2s_relay_span.py"]
469
+ status = "paraphrase"
470
+ sha256 = "a2802230c8742b4a94e27f4854b81803119a41a5ca245d92d67001c0e2356489"
471
+
472
+ [tool.citation.packages.culture-tests-telemetry.files."test_server_init.py"]
473
+ status = "quote"
474
+ sha256 = "16ad131fdb48a8ff0162227f68d0b50ac4f9dc783dbd0d8061112613efff72f5"
475
+
476
+ [tool.citation.packages.culture-tests-telemetry.files."test_server_link_inject.py"]
477
+ status = "paraphrase"
478
+ sha256 = "ce2de77e1002758976e7ac50bd61cbc8a551e128b285eb01e4a1cf23ce3aa148"
479
+
480
+ [tool.citation.packages.culture-tests-telemetry.files."test_tracing.py"]
481
+ status = "paraphrase"
482
+ sha256 = "1847662286c1d15848ae3624f61c88f78f1f5d16ab30af3e77981c94e9e5c86d"
@@ -0,0 +1,96 @@
1
+ """Shared test helpers, agentirc-native (NOT vendored from culture).
2
+
3
+ Extracted to address SonarCloud's >3% duplication threshold. The
4
+ boot-linked-pair pattern repeats verbatim across the cited
5
+ ``test_federation.py`` (5 sites), ``test_link_reconnect.py`` (3
6
+ sites), and the ``linked_servers`` conftest fixture; CPD flags it as
7
+ ~22 lines × 9 = ~200 lines of duplicated test scaffold.
8
+
9
+ Why these tests can't share the existing ``linked_servers`` pytest
10
+ fixture: each one needs to drive the boot/link/teardown lifecycle
11
+ itself — duplicate-link rejection, slow-link teardown, mid-test
12
+ peer kill, backfill replay — and the function-scoped fixture starts
13
+ both servers + completes the handshake before the test body runs.
14
+
15
+ Re-snapshotting from a future culture commit means re-running the
16
+ same extraction: replace each ~22-line inline boot block with a
17
+ single ``boot_linked_pair(tmp_path)`` call. Mechanical sed pattern.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+
24
+ from agentirc.config import LinkConfig, ServerConfig, TelemetryConfig
25
+ from agentirc.ircd import IRCd
26
+
27
+ from tests.conftest import TEST_LINK_PASSWORD
28
+
29
+
30
+ async def boot_linked_pair(
31
+ tmp_path,
32
+ *,
33
+ link_password: str = TEST_LINK_PASSWORD,
34
+ with_audit: bool = False,
35
+ webhook_port: int | None = None,
36
+ ) -> tuple[IRCd, IRCd]:
37
+ """Boot two IRCd instances pre-wired with link configs.
38
+
39
+ Returns ``(server_a, server_b)`` after both ``start()`` calls
40
+ have returned with OS-assigned ports populated. Does NOT drive
41
+ the S2S handshake — caller decides whether to call
42
+ :func:`link_pair` or invoke ``connect_to_peer`` manually.
43
+
44
+ ``with_audit=True`` plumbs ``TelemetryConfig(audit_dir=...)``
45
+ onto each side, isolating audit JSONL writes to ``tmp_path``.
46
+ ``webhook_port=0`` suppresses the webhook listener — the
47
+ ``linked_servers`` fixture sets this; the inline sites do not.
48
+ """
49
+ kwargs_a: dict = {"links": [
50
+ LinkConfig(name="beta", host="127.0.0.1", port=0, password=link_password)
51
+ ]}
52
+ kwargs_b: dict = {"links": [
53
+ LinkConfig(name="alpha", host="127.0.0.1", port=0, password=link_password)
54
+ ]}
55
+ if with_audit:
56
+ kwargs_a["telemetry"] = TelemetryConfig(audit_dir=str(tmp_path / "audit_alpha"))
57
+ kwargs_b["telemetry"] = TelemetryConfig(audit_dir=str(tmp_path / "audit_beta"))
58
+ if webhook_port is not None:
59
+ kwargs_a["webhook_port"] = webhook_port
60
+ kwargs_b["webhook_port"] = webhook_port
61
+
62
+ config_a = ServerConfig(name="alpha", host="127.0.0.1", port=0, **kwargs_a)
63
+ config_b = ServerConfig(name="beta", host="127.0.0.1", port=0, **kwargs_b)
64
+ server_a = IRCd(config_a)
65
+ server_b = IRCd(config_b)
66
+ await server_a.start()
67
+ await server_b.start()
68
+ # `_server` is set by `start()` — assert for type-checker / SonarCloud
69
+ # S2259, since the IRCd type carries `_server: asyncio.AbstractServer | None`.
70
+ assert server_a._server is not None
71
+ assert server_b._server is not None
72
+ server_a.config.port = server_a._server.sockets[0].getsockname()[1]
73
+ server_b.config.port = server_b._server.sockets[0].getsockname()[1]
74
+ return server_a, server_b
75
+
76
+
77
+ async def link_pair(
78
+ server_a: IRCd,
79
+ server_b: IRCd,
80
+ *,
81
+ link_password: str = TEST_LINK_PASSWORD,
82
+ ) -> None:
83
+ """Drive the S2S handshake on a booted linked-pair.
84
+
85
+ Updates each side's ``LinkConfig`` with the actual OS-assigned
86
+ port (the ``links=[…port=0]`` placeholder gets resolved here),
87
+ calls ``connect_to_peer`` from A → B, and polls up to ~2.5s for
88
+ both sides to see the link before returning.
89
+ """
90
+ server_a.config.links[0].port = server_b.config.port
91
+ server_b.config.links[0].port = server_a.config.port
92
+ await server_a.connect_to_peer("127.0.0.1", server_b.config.port, link_password)
93
+ for _ in range(50):
94
+ if "beta" in server_a.links and "alpha" in server_b.links:
95
+ break
96
+ await asyncio.sleep(0.05)