yaic 0.1.0 → 0.2.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/ralph-qa.md +101 -0
  3. data/.claude/ralph/bin/dump-pid.sh +3 -0
  4. data/.claude/ralph/bin/kill-claude +6 -0
  5. data/.claude/ralph/bin/start-ralph +44 -0
  6. data/.claude/ralph/bin/stop-hook.sh +9 -0
  7. data/.claude/ralph/bin/stop-ralph +17 -0
  8. data/.claude/ralph/prompt.md +218 -0
  9. data/.claude/settings.json +26 -0
  10. data/.gitmodules +3 -0
  11. data/CLAUDE.md +65 -0
  12. data/README.md +106 -17
  13. data/Rakefile +8 -0
  14. data/devenv.nix +1 -0
  15. data/docs/agents/data-model.md +150 -0
  16. data/docs/agents/ralph/features/01-message-parsing.md.done +160 -0
  17. data/docs/agents/ralph/features/01-tcpsocket-refactor.md.done +109 -0
  18. data/docs/agents/ralph/features/02-connection-socket.md.done +138 -0
  19. data/docs/agents/ralph/features/02-simplified-client-api.md.done +306 -0
  20. data/docs/agents/ralph/features/03-registration.md.done +147 -0
  21. data/docs/agents/ralph/features/04-ping-pong.md.done +109 -0
  22. data/docs/agents/ralph/features/05-event-system.md.done +167 -0
  23. data/docs/agents/ralph/features/06-privmsg-notice.md.done +163 -0
  24. data/docs/agents/ralph/features/07-join-part.md.done +190 -0
  25. data/docs/agents/ralph/features/08-quit.md.done +118 -0
  26. data/docs/agents/ralph/features/09-nick-change.md.done +109 -0
  27. data/docs/agents/ralph/features/10-topic.md.done +145 -0
  28. data/docs/agents/ralph/features/11-kick.md.done +122 -0
  29. data/docs/agents/ralph/features/12-names.md.done +124 -0
  30. data/docs/agents/ralph/features/13-mode.md.done +174 -0
  31. data/docs/agents/ralph/features/14-who-whois.md.done +188 -0
  32. data/docs/agents/ralph/features/15-client-api.md.done +180 -0
  33. data/docs/agents/ralph/features/16-ssl-test-infrastructure.md.done +50 -0
  34. data/docs/agents/ralph/features/17-github-actions-ci.md.done +70 -0
  35. data/docs/agents/ralph/features/18-brakeman-security-scanning.md.done +67 -0
  36. data/docs/agents/ralph/features/19-fix-qa.md.done +73 -0
  37. data/docs/agents/ralph/features/20-test-optimization.md.done +70 -0
  38. data/docs/agents/ralph/features/21-test-parallelization.md.done +56 -0
  39. data/docs/agents/ralph/features/22-wait-until-pattern.md.done +90 -0
  40. data/docs/agents/ralph/features/23-ping-test-optimization.md.done +46 -0
  41. data/docs/agents/ralph/features/24-blocking-who-whois.md.done +159 -0
  42. data/docs/agents/ralph/features/25-verbose-mode.md.done +166 -0
  43. data/docs/agents/ralph/plans/test-optimization-plan.md +172 -0
  44. data/docs/agents/ralph/progress.md +731 -0
  45. data/docs/agents/todo.md +5 -0
  46. data/lib/yaic/channel.rb +22 -0
  47. data/lib/yaic/client.rb +821 -0
  48. data/lib/yaic/event.rb +35 -0
  49. data/lib/yaic/message.rb +119 -0
  50. data/lib/yaic/registration.rb +17 -0
  51. data/lib/yaic/socket.rb +120 -0
  52. data/lib/yaic/source.rb +39 -0
  53. data/lib/yaic/version.rb +1 -1
  54. data/lib/yaic/who_result.rb +17 -0
  55. data/lib/yaic/whois_result.rb +20 -0
  56. data/lib/yaic.rb +13 -1
  57. metadata +51 -1
@@ -0,0 +1,56 @@
1
+ # Test Parallelization
2
+
3
+ ## Description
4
+
5
+ Enable minitest's built-in parallel test execution to run tests concurrently, reducing total test time by utilizing multiple CPU cores.
6
+
7
+ ## Behavior
8
+
9
+ ### Enable Parallel Execution
10
+
11
+ Add `parallelize_me!` to test classes or enable globally in test_helper.rb.
12
+
13
+ ### Fix Unique Identifiers for Thread Safety
14
+
15
+ Tests currently use `Process.pid` for unique nicks/channels, but with thread-based parallelism all threads share the same PID. Update identifier generation to include thread ID:
16
+
17
+ ```ruby
18
+ # Before
19
+ @test_nick = "t#{Process.pid}#{Time.now.to_i % 10000}"
20
+
21
+ # After
22
+ @test_nick = "t#{Process.pid}_#{Thread.current.object_id % 10000}_#{rand(10000)}"
23
+ ```
24
+
25
+ ### Verify Test Isolation
26
+
27
+ Ensure tests don't share state:
28
+ - Each test creates its own IRC client connections
29
+ - Each test uses unique channel names
30
+ - No global variables or class variables modified during tests
31
+
32
+ ## Tests
33
+
34
+ Run the full test suite and verify:
35
+ - All 267 tests pass
36
+ - All 575 assertions pass
37
+ - No new skips introduced
38
+ - Test time is reduced (target: 3-4x speedup)
39
+
40
+ ## Implementation Notes
41
+
42
+ - Use `bundle exec rake test` to run tests
43
+ - Use `bundle exec standardrb -A` for linting
44
+ - The IRC server can handle multiple concurrent connections
45
+
46
+ Minitest parallel options:
47
+ 1. `parallelize_me!` in individual test classes
48
+ 2. Global enable in test_helper.rb via `Minitest::Test.parallelize_me!`
49
+
50
+ ## Dependencies
51
+
52
+ - 20-test-optimization.md (planning complete)
53
+
54
+ ## Reference
55
+
56
+ See `docs/agents/ralph/plans/test-optimization-plan.md` for full analysis.
@@ -0,0 +1,90 @@
1
+ # Wait Until Pattern
2
+
3
+ ## Description
4
+
5
+ Replace `sleep 0.5`, `read_multiple`, `read_until_pong`, and similar patterns with a consistent `wait_until` helper that returns as soon as a condition is met.
6
+
7
+ ## Behavior
8
+
9
+ ### Create wait_until Helper
10
+
11
+ Add a `wait_until` helper to test_helper.rb:
12
+
13
+ ```ruby
14
+ def wait_until(timeout: 2)
15
+ deadline = Time.now + timeout
16
+ until Time.now > deadline
17
+ result = yield
18
+ return result if result
19
+ sleep 0.01
20
+ end
21
+ nil
22
+ end
23
+ ```
24
+
25
+ ### Replace sleep Statements
26
+
27
+ Replace `sleep 0.5` with `wait_until { condition }`:
28
+
29
+ ```ruby
30
+ # Before
31
+ client1.kick(@test_channel, @test_nick2)
32
+ sleep 0.5
33
+ assert kick_received
34
+
35
+ # After
36
+ client1.kick(@test_channel, @test_nick2)
37
+ wait_until { kick_received }
38
+ assert kick_received
39
+ ```
40
+
41
+ ### Replace read_multiple Pattern
42
+
43
+ Replace `read_multiple(socket, 5)` with inline `wait_until`:
44
+
45
+ ```ruby
46
+ # Before
47
+ messages = read_multiple(socket, 5)
48
+ erroneous = messages.find { |m| m.command == "432" }
49
+
50
+ # After
51
+ erroneous = nil
52
+ wait_until(timeout: 2) do
53
+ raw = socket.read
54
+ if raw
55
+ msg = Yaic::Message.parse(raw)
56
+ erroneous = msg if msg&.command == "432"
57
+ end
58
+ erroneous
59
+ end
60
+ ```
61
+
62
+ ### Remove Old Helpers
63
+
64
+ Remove these helpers from test files:
65
+ - `read_multiple`
66
+ - `read_until_pong`
67
+ - `read_until_welcome`
68
+ - `read_with_timeout`
69
+
70
+ ## Tests
71
+
72
+ Run the full test suite and verify:
73
+ - All tests pass
74
+ - No assertions removed or weakened
75
+ - Test time is reduced
76
+
77
+ ## Implementation Notes
78
+
79
+ - Use `bundle exec rake test` to run tests
80
+ - Use `bundle exec standardrb -A` for linting
81
+ - Focus on integration tests first (they have the most sleeps)
82
+
83
+ ## Dependencies
84
+
85
+ - 20-test-optimization.md (planning complete)
86
+ - 21-test-parallelization.md (optional, can be done in parallel)
87
+
88
+ ## Reference
89
+
90
+ See `docs/agents/ralph/plans/test-optimization-plan.md` for full analysis.
@@ -0,0 +1,46 @@
1
+ # Ping Test Optimization
2
+
3
+ ## Description
4
+
5
+ Optimize the `test_client_automatically_responds_to_server_ping` test which currently takes 7 seconds due to a `sleep 6` waiting for the server's PING.
6
+
7
+ ## Behavior
8
+
9
+ ### Reduce Server Ping Frequency
10
+
11
+ Update the InspIRCd test config (`test/fixtures/inspircd.conf`) to reduce ping frequency from 5 seconds to 3 seconds.
12
+
13
+ ### Use wait_until Instead of Fixed Sleep
14
+
15
+ Replace the `sleep 6` with a `wait_until` that detects when a PING/PONG cycle has occurred:
16
+
17
+ ```ruby
18
+ # Before
19
+ sleep 6
20
+ assert client.last_received_at > initial_last_received
21
+
22
+ # After
23
+ wait_until(timeout: 5) { client.last_received_at > initial_last_received }
24
+ assert client.last_received_at > initial_last_received
25
+ ```
26
+
27
+ ## Tests
28
+
29
+ Run the ping pong integration tests and verify:
30
+ - All tests pass
31
+ - `test_client_automatically_responds_to_server_ping` completes in ~3-4 seconds instead of 7
32
+
33
+ ## Implementation Notes
34
+
35
+ - Use `bundle exec m test/integration/ping_pong_test.rb` to run just ping tests
36
+ - Use `bundle exec standardrb -A` for linting
37
+ - After changing server config, restart the IRC server: `bin/stop-irc-server && bin/start-irc-server`
38
+
39
+ ## Dependencies
40
+
41
+ - 20-test-optimization.md (planning complete)
42
+ - 22-wait-until-pattern.md (wait_until helper must exist)
43
+
44
+ ## Reference
45
+
46
+ See `docs/agents/ralph/plans/test-optimization-plan.md` for full analysis.
@@ -0,0 +1,159 @@
1
+ # Blocking WHO/WHOIS
2
+
3
+ ## Description
4
+
5
+ Convert `who` and `whois` methods from fire-and-forget to blocking calls that return results directly. This follows the same pattern already used by `join`, `part`, and `nick` methods.
6
+
7
+ ## Behavior
8
+
9
+ ### WHO Command
10
+
11
+ `who(mask, timeout: DEFAULT_OPERATION_TIMEOUT)` sends WHO command and blocks until END OF WHO (315) is received.
12
+
13
+ - Returns an array of `WhoResult` objects (one per user matching the mask)
14
+ - Returns empty array if no matches
15
+ - Raises `Yaic::TimeoutError` if timeout exceeded
16
+ - Events (`:who`) still fire for each reply (maintains backward compatibility)
17
+
18
+ ### WHOIS Command
19
+
20
+ `whois(nick, timeout: DEFAULT_OPERATION_TIMEOUT)` sends WHOIS command and blocks until END OF WHOIS (318) is received.
21
+
22
+ - Returns `WhoisResult` object if user found
23
+ - Returns `nil` if user not found (401 ERR_NOSUCHNICK)
24
+ - Raises `Yaic::TimeoutError` if timeout exceeded
25
+ - Event (`:whois`) still fires (maintains backward compatibility)
26
+
27
+ ### WhoResult Class
28
+
29
+ Create `lib/yaic/who_result.rb` with attributes matching the `:who` event data:
30
+
31
+ - `channel` - channel name (or "*" for non-channel queries)
32
+ - `user` - username
33
+ - `host` - hostname
34
+ - `server` - server name
35
+ - `nick` - nickname
36
+ - `away` - boolean, true if user is away
37
+ - `realname` - real name
38
+
39
+ ## Models
40
+
41
+ New file: `lib/yaic/who_result.rb`
42
+
43
+ ```ruby
44
+ module Yaic
45
+ class WhoResult
46
+ attr_reader :channel, :user, :host, :server, :nick, :away, :realname
47
+
48
+ def initialize(channel:, user:, host:, server:, nick:, away:, realname:)
49
+ # ...
50
+ end
51
+ end
52
+ end
53
+ ```
54
+
55
+ ## Implementation Notes
56
+
57
+ ### Tracking Pending Operations
58
+
59
+ Use instance variables to track pending blocking operations:
60
+
61
+ - `@pending_who_results` - Hash of mask => array of WhoResult objects being collected
62
+ - `@pending_who_complete` - Hash of mask => boolean (true when 315 received)
63
+
64
+ The existing `@pending_whois` already tracks WHOIS, just need to add completion tracking.
65
+
66
+ ### Handle Interleaving
67
+
68
+ Multiple WHO/WHOIS requests could be in flight. Track by mask/nick to handle interleaving correctly. The existing WHOIS implementation already handles this.
69
+
70
+ ### Modify Handlers
71
+
72
+ - `handle_rpl_whoreply` (352): Create WhoResult, add to pending array
73
+ - `handle_rpl_endofwho` (315): Mark pending WHO complete
74
+ - `handle_rpl_endofwhois` (318): Already handled, add completion flag
75
+
76
+ ### Waiting Pattern
77
+
78
+ Follow the existing pattern from `join`:
79
+
80
+ ```ruby
81
+ def who(mask, timeout: DEFAULT_OPERATION_TIMEOUT)
82
+ @pending_who_results[mask] = []
83
+ @pending_who_complete[mask] = false
84
+
85
+ message = Message.new(command: "WHO", params: [mask])
86
+ @socket.write(message.to_s)
87
+
88
+ wait_until(timeout: timeout) { @pending_who_complete[mask] }
89
+
90
+ @pending_who_results.delete(mask)
91
+ ensure
92
+ @pending_who_complete.delete(mask)
93
+ end
94
+ ```
95
+
96
+ ## Tests
97
+
98
+ ### Unit Tests
99
+
100
+ **who returns array of WhoResult objects**
101
+ - Given: Mock socket returns 352 replies followed by 315
102
+ - When: `client.who("#channel")`
103
+ - Then: Returns array of WhoResult with correct attributes
104
+
105
+ **who returns empty array when no matches**
106
+ - Given: Mock socket returns only 315 (no 352 replies)
107
+ - When: `client.who("nobody")`
108
+ - Then: Returns empty array
109
+
110
+ **who raises TimeoutError on timeout**
111
+ - Given: Mock socket never returns 315
112
+ - When: `client.who("#channel", timeout: 0.1)`
113
+ - Then: Raises Yaic::TimeoutError
114
+
115
+ **who still emits :who events**
116
+ - Given: Mock socket returns 352 replies
117
+ - When: `client.who("#channel")` with event listener attached
118
+ - Then: Events fire AND method returns results
119
+
120
+ **whois returns WhoisResult**
121
+ - Given: Mock socket returns WHOIS numerics followed by 318
122
+ - When: `client.whois("nick")`
123
+ - Then: Returns WhoisResult with correct attributes
124
+
125
+ **whois returns nil for unknown nick**
126
+ - Given: Mock socket returns 401 ERR_NOSUCHNICK then 318
127
+ - When: `client.whois("nobody")`
128
+ - Then: Returns nil
129
+
130
+ **whois raises TimeoutError on timeout**
131
+ - Given: Mock socket never returns 318
132
+ - When: `client.whois("nick", timeout: 0.1)`
133
+ - Then: Raises Yaic::TimeoutError
134
+
135
+ ### Integration Tests
136
+
137
+ **who channel returns all users**
138
+ - Given: Two clients connected to same channel
139
+ - When: `client1.who("#channel")`
140
+ - Then: Returns array with both users' info
141
+
142
+ **who nick returns single user**
143
+ - Given: Two clients connected
144
+ - When: `client1.who(client2_nick)`
145
+ - Then: Returns array with one WhoResult for client2
146
+
147
+ **whois returns user info**
148
+ - Given: Two clients connected
149
+ - When: `client1.whois(client2_nick)`
150
+ - Then: Returns WhoisResult with nick, user, host, realname
151
+
152
+ **whois unknown returns nil**
153
+ - Given: Client connected
154
+ - When: `client.whois("nobody_exists")`
155
+ - Then: Returns nil (not raises)
156
+
157
+ ## Dependencies
158
+
159
+ None - builds on existing WHO/WHOIS handling from 14-who-whois.md
@@ -0,0 +1,166 @@
1
+ # Verbose Mode
2
+
3
+ ## Description
4
+
5
+ Add a `verbose:` option to the Client constructor that outputs debug information about connection state changes and blocking operations. Helps developers understand what the client is doing.
6
+
7
+ ## Behavior
8
+
9
+ ### Enabling Verbose Mode
10
+
11
+ ```ruby
12
+ client = Yaic::Client.new(
13
+ server: "irc.example.com",
14
+ nick: "mynick",
15
+ verbose: true # defaults to false
16
+ )
17
+ ```
18
+
19
+ ### What Gets Logged
20
+
21
+ **Connection State Changes**
22
+ - When connecting starts
23
+ - When connected successfully
24
+ - When disconnected (including reason if available)
25
+ - When connection state changes (registering, etc.)
26
+
27
+ **Blocking Operations**
28
+ - When waiting for an operation to complete (join, part, nick, who, whois)
29
+ - When the wait completes
30
+
31
+ ### Output Format
32
+
33
+ Simple, readable format to STDERR:
34
+
35
+ ```
36
+ [YAIC] Connecting to irc.example.com:6697 (SSL)...
37
+ [YAIC] Connected, registering...
38
+ [YAIC] Registration complete
39
+ [YAIC] Joining #channel...
40
+ [YAIC] Joined #channel
41
+ [YAIC] Sending WHO #channel...
42
+ [YAIC] WHO complete (3 results)
43
+ [YAIC] Sending WHOIS nick...
44
+ [YAIC] WHOIS complete
45
+ [YAIC] Disconnected
46
+ ```
47
+
48
+ ### Output Destination
49
+
50
+ Use `$stderr.puts` for output. This keeps it separate from application output and works well with logging redirection.
51
+
52
+ ## Implementation Notes
53
+
54
+ ### Add verbose attribute
55
+
56
+ ```ruby
57
+ def initialize(server:, port: 6697, nick:, user: nil, realname: nil, ssl: true, verbose: false)
58
+ @verbose = verbose
59
+ # ...
60
+ end
61
+ ```
62
+
63
+ ### Add logging helper
64
+
65
+ ```ruby
66
+ private
67
+
68
+ def log(message)
69
+ return unless @verbose
70
+ $stderr.puts "[YAIC] #{message}"
71
+ end
72
+ ```
73
+
74
+ ### Add logging calls
75
+
76
+ In `connect`:
77
+ ```ruby
78
+ log "Connecting to #{@server}:#{@port}#{@ssl ? ' (SSL)' : ''}..."
79
+ ```
80
+
81
+ After registration complete (in handler for 001):
82
+ ```ruby
83
+ log "Connected"
84
+ ```
85
+
86
+ In `set_state`:
87
+ ```ruby
88
+ log "State: #{state}" # or more friendly messages per state
89
+ ```
90
+
91
+ In `join`:
92
+ ```ruby
93
+ log "Joining #{channel}..."
94
+ # after wait_until
95
+ log "Joined #{channel}"
96
+ ```
97
+
98
+ In `part`:
99
+ ```ruby
100
+ log "Parting #{channel}..."
101
+ log "Parted #{channel}"
102
+ ```
103
+
104
+ In `who`:
105
+ ```ruby
106
+ log "Sending WHO #{mask}..."
107
+ # after wait_until
108
+ log "WHO complete (#{results.size} results)"
109
+ ```
110
+
111
+ In `whois`:
112
+ ```ruby
113
+ log "Sending WHOIS #{nick}..."
114
+ # after wait_until
115
+ log "WHOIS complete"
116
+ ```
117
+
118
+ In disconnect handler:
119
+ ```ruby
120
+ log "Disconnected"
121
+ ```
122
+
123
+ ## Tests
124
+
125
+ ### Unit Tests
126
+
127
+ **verbose false produces no output**
128
+ - Given: Client created with `verbose: false` (default)
129
+ - When: Client connects and performs operations
130
+ - Then: No output to stderr
131
+
132
+ **verbose true logs connection**
133
+ - Given: Client created with `verbose: true`
134
+ - When: Client connects
135
+ - Then: Stderr contains "[YAIC] Connecting to" message
136
+
137
+ **verbose true logs state changes**
138
+ - Given: Client created with `verbose: true`
139
+ - When: Client connects successfully
140
+ - Then: Stderr contains "[YAIC] Connected" message
141
+
142
+ **verbose true logs join**
143
+ - Given: Connected client with `verbose: true`
144
+ - When: `client.join("#channel")`
145
+ - Then: Stderr contains "[YAIC] Joining #channel..." and "[YAIC] Joined #channel"
146
+
147
+ **verbose true logs who**
148
+ - Given: Connected client with `verbose: true`
149
+ - When: `client.who("#channel")`
150
+ - Then: Stderr contains "[YAIC] Sending WHO" and "[YAIC] WHO complete"
151
+
152
+ **verbose true logs whois**
153
+ - Given: Connected client with `verbose: true`
154
+ - When: `client.whois("nick")`
155
+ - Then: Stderr contains "[YAIC] Sending WHOIS" and "[YAIC] WHOIS complete"
156
+
157
+ ### Integration Tests
158
+
159
+ **verbose mode produces expected output sequence**
160
+ - Given: Client with `verbose: true`
161
+ - When: Connect, join channel, who channel, disconnect
162
+ - Then: Stderr output matches expected sequence of log messages
163
+
164
+ ## Dependencies
165
+
166
+ - 24-blocking-who-whois.md (for WHO/WHOIS logging)
@@ -0,0 +1,172 @@
1
+ # Test Optimization Plan
2
+
3
+ ## Profiling Results
4
+
5
+ **Total test time**: 173 seconds
6
+ **Total tests**: 267
7
+ **Tests >= 1 second**: 79 tests (172.51s cumulative)
8
+ **Tests < 1 second**: 188 tests (0.54s cumulative)
9
+
10
+ ### Slowest Tests (> 3 seconds)
11
+
12
+ | Test | Time | Root Cause |
13
+ |------|------|------------|
14
+ | `PingPongIntegrationTest#test_client_automatically_responds_to_server_ping` | 7.01s | Intentional `sleep 6` to wait for server PING |
15
+ | `NamesIntegrationTest#test_multi_message_names` | 5.73s | Creates 5 clients, each taking ~1s to connect |
16
+ | `SocketIntegrationTest#test_ssl_read_write` | 5.01s | `read_multiple(socket, 5)` - full timeout wait |
17
+ | `RegistrationIntegrationTest#test_invalid_nickname` | 5.01s | `read_multiple(socket, 5)` - full timeout wait |
18
+ | `SocketIntegrationTest#test_write_message` | 5.00s | `read_multiple(socket, 5)` - full timeout wait |
19
+ | `RegistrationIntegrationTest#test_empty_nickname` | 5.00s | `read_multiple(socket, 5)` - full timeout wait |
20
+ | `KickIntegrationTest#test_receive_kick_others` | 4.13s | Creates 3 clients + oper setup + sleep 0.5 |
21
+ | `ModeIntegrationTest#test_give_op_to_user` | 3.12s | 2 clients + oper setup + sleep 0.5 |
22
+ | `KickIntegrationTest#test_receive_kick_self` | 3.12s | 2 clients + oper setup + sleep 0.5 |
23
+ | `KickIntegrationTest#test_kick_user` | 3.12s | 2 clients + oper setup + sleep 0.5 |
24
+ | `TopicIntegrationTest#test_receive_topic_change` | 3.06s | 2 clients + sleep 0.5 |
25
+ | `PingPongIntegrationTest#test_respond_to_server_ping_during_registration` | 3.02s | `read_multiple(socket, 3)` timeout |
26
+
27
+ ### Root Causes Identified
28
+
29
+ 1. **Fixed sleep statements**: Many tests use `sleep 0.5` to wait for IRC responses
30
+ 2. **Full timeout waits**: `read_multiple(socket, N)` always waits N seconds even if data arrives immediately
31
+ 3. **Multiple client connections**: Each `client.connect` takes ~1 second due to registration
32
+ 4. **Sequential test execution**: Tests run one at a time despite being independent
33
+
34
+ ## Proposed Optimizations
35
+
36
+ ### Optimization 1: Enable Parallel Test Execution
37
+
38
+ **Description**: Use minitest's built-in parallel executor to run tests concurrently.
39
+
40
+ **Changes**:
41
+ - Add `parallelize_me!` to test classes or enable globally
42
+ - Tests already use unique nicks/channels via `Process.pid` and `Time.now.to_i`
43
+
44
+ **Expected Impact**: 3-4x speedup (utilize multiple CPU cores)
45
+
46
+ **Risk**: Low - tests already use unique identifiers, no shared state between tests
47
+
48
+ **Implementation**:
49
+ ```ruby
50
+ # test/test_helper.rb
51
+ require "minitest/autorun"
52
+ class Minitest::Test
53
+ parallelize_me!
54
+ end
55
+ ```
56
+
57
+ ### Optimization 2: Replace `sleep` with `wait_until` Pattern
58
+
59
+ **Description**: Replace `sleep 0.5` with `wait_until` style helpers that return as soon as condition is met.
60
+
61
+ **Changes**:
62
+ - Create `wait_until(timeout: 2) { condition }` helper in test_helper.rb
63
+ - Replace `sleep 0.5` with targeted `wait_until` calls
64
+
65
+ **Expected Impact**: Each test saves 0.3-0.4s (event typically arrives in 0.1s)
66
+
67
+ **Risk**: Low - uses existing event system, fails fast if event doesn't arrive
68
+
69
+ **Example**:
70
+ ```ruby
71
+ # Before
72
+ client1.kick(@test_channel, @test_nick2)
73
+ sleep 0.5
74
+ assert kick_received
75
+
76
+ # After
77
+ client1.kick(@test_channel, @test_nick2)
78
+ wait_until { kick_received }
79
+ assert kick_received
80
+ ```
81
+
82
+ ### Optimization 3: Replace `read_multiple`/`read_until_*` with `wait_until` Pattern
83
+
84
+ **Description**: Rename and refactor `read_multiple`, `read_until_pong`, etc. to use consistent `wait_until` naming.
85
+
86
+ **Changes**:
87
+ - Replace `read_multiple(socket, 5)` with `wait_until(timeout: 2) { messages.any? { |m| m.command == "432" } }`
88
+ - Remove confusingly-named helpers in favor of inline `wait_until` blocks
89
+
90
+ **Expected Impact**: 4 tests save ~15 seconds total
91
+
92
+ **Risk**: Low - clearer code, faster execution
93
+
94
+ **Example**:
95
+ ```ruby
96
+ # Before (always waits 5 seconds)
97
+ messages = read_multiple(socket, 5)
98
+ erroneous = messages.find { |m| m.command == "432" }
99
+
100
+ # After (returns when error found or timeout)
101
+ erroneous = nil
102
+ wait_until(timeout: 2) do
103
+ raw = socket.read
104
+ if raw
105
+ msg = Yaic::Message.parse(raw)
106
+ erroneous = msg if msg&.command == "432"
107
+ end
108
+ erroneous
109
+ end
110
+ ```
111
+
112
+ ### Optimization 4: Optimize Ping Test with Faster Server Config
113
+
114
+ **Description**: The `test_client_automatically_responds_to_server_ping` waits 6 seconds for server PING.
115
+
116
+ **Changes**:
117
+ - Reduce server's ping frequency in test config from 5s to 3s
118
+ - Use `wait_until` to detect when PING/PONG cycle completes instead of fixed sleep
119
+
120
+ **Expected Impact**: Save ~4 seconds
121
+
122
+ **Risk**: Low - just reducing wait time and using event-driven approach
123
+
124
+ ## Implementation Order
125
+
126
+ 1. **Parallel execution** (biggest impact, lowest risk)
127
+ 2. **Replace `sleep 0.5` with event waits** (many small wins)
128
+ 3. **Fix `read_multiple` pattern** (4 tests)
129
+ 4. **Optimize ping test** (1 test)
130
+
131
+ ## Parallelization Safety Analysis
132
+
133
+ ### Already Safe (unique identifiers per test)
134
+
135
+ All integration tests generate unique identifiers:
136
+ ```ruby
137
+ @test_nick = "t#{Process.pid}#{Time.now.to_i % 10000}"
138
+ @test_channel = "#test#{Process.pid}#{Time.now.to_i % 10000}"
139
+ ```
140
+
141
+ When running in parallel, each test process/thread has different:
142
+ - `Process.pid` (if forked)
143
+ - `Time.now.to_i % 10000` (high probability of uniqueness)
144
+
145
+ ### Potential Issue: Thread-based parallelism
146
+
147
+ If using threads (not forks), `Process.pid` will be the same for all tests. Need to add thread ID:
148
+ ```ruby
149
+ @test_nick = "t#{Process.pid}_#{Thread.current.object_id % 10000}_#{Time.now.to_i % 10000}"
150
+ ```
151
+
152
+ ### Shared Resources
153
+
154
+ - IRC server: Can handle multiple concurrent connections
155
+ - `become_oper`: Uses single shared oper account - safe, oper can be used from multiple connections
156
+ - No file system or database shared state
157
+
158
+ ## Verification Plan
159
+
160
+ 1. Run `rake test` and verify all tests pass
161
+ 2. Count tests: should be 267
162
+ 3. Count assertions: should be 575
163
+ 4. Time should be significantly reduced (target: < 60s)
164
+
165
+ ## Risk Assessment
166
+
167
+ | Change | Risk | Mitigation |
168
+ |--------|------|------------|
169
+ | Parallel execution | Low | Tests already isolated by design |
170
+ | Event-driven waits | Low | Fallback timeout prevents hangs |
171
+ | read_multiple fix | Low | Backwards compatible |
172
+ | Ping test optimization | Medium | May need server config tuning |