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,306 @@
1
+ # Simplified Client API
2
+
3
+ ## Description
4
+
5
+ The current Client API is broken. Users must:
6
+ - Call `client.on_socket_connected` manually (insane)
7
+ - Access internal socket via `instance_variable_get(:@socket)`
8
+ - Manually loop calling `socket.read`, `Message.parse`, `client.handle_message`
9
+ - Build custom `wait_for_*` helpers for every operation
10
+
11
+ The spec at `15-client-api.md.done` described the correct interface but the implementation didn't deliver. Fix it.
12
+
13
+ ## Target API
14
+
15
+ ```ruby
16
+ client = Yaic::Client.new(
17
+ server: "irc.libera.chat",
18
+ port: 6697,
19
+ ssl: true,
20
+ nickname: "mynick"
21
+ )
22
+
23
+ client.on(:message) { |event| puts event.text }
24
+ client.connect
25
+
26
+ client.join("#ruby")
27
+ client.privmsg("#ruby", "Hello!")
28
+ client.part("#ruby")
29
+ client.quit
30
+ ```
31
+
32
+ That's it. All methods block until the operation completes. No bangs, no timeout params, no manual socket handling.
33
+
34
+ ## Behavior
35
+
36
+ ### All Operations Block
37
+
38
+ | Method | Blocks until |
39
+ |--------|--------------|
40
+ | `connect` | 001 RPL_WELCOME received |
41
+ | `join(channel)` | Channel appears in `@channels` |
42
+ | `part(channel)` | Channel removed from `@channels` |
43
+ | `nick(new)` | Nick change confirmed |
44
+ | `quit` | Disconnect complete |
45
+
46
+ ### `connect` Method
47
+
48
+ Current (broken):
49
+ ```ruby
50
+ client.connect
51
+ client.on_socket_connected # User must call this!
52
+ socket = client.instance_variable_get(:@socket) # Insane
53
+ # Manual loop...
54
+ ```
55
+
56
+ After:
57
+ ```ruby
58
+ client.connect # Does everything, blocks until registered
59
+ ```
60
+
61
+ Internally `connect` must:
62
+ 1. Create socket and connect (blocking with TCPSocket after feature 01)
63
+ 2. Send NICK/USER registration
64
+ 3. Start read loop (background thread)
65
+ 4. Wait for 001 RPL_WELCOME
66
+ 5. Set state to `:connected`
67
+ 6. Return
68
+
69
+ ### Read Loop
70
+
71
+ Background thread that runs continuously:
72
+ ```ruby
73
+ loop do
74
+ raw = @socket.read
75
+ if raw
76
+ message = Message.parse(raw)
77
+ handle_message(message) if message
78
+ end
79
+ break if @state == :disconnected
80
+ sleep 0.001 # Prevent busy-wait when no data
81
+ end
82
+ ```
83
+
84
+ ### Internal Timeouts
85
+
86
+ Timeouts are an internal concern. Use sensible defaults:
87
+ - `connect`: 30 seconds
88
+ - `join`/`part`/`nick`: 10 seconds
89
+
90
+ Raise an error if exceeded. Users don't need to think about this.
91
+
92
+ ### Thread Safety
93
+
94
+ - `@handlers` hash needs mutex protection for `on`/`off` during loop
95
+ - `@state` writes should be protected
96
+ - `@channels` needs mutex if accessed from handlers
97
+
98
+ ### Remove Exposed Internals
99
+
100
+ - Remove `on_socket_connected` - internal use only
101
+ - `handle_message` can stay public for testing but document as internal
102
+ - Socket should never need to be accessed by users
103
+
104
+ ## Tests
105
+
106
+ ### Integration Tests - The Dream
107
+
108
+ After this feature, integration tests should look like:
109
+
110
+ ```ruby
111
+ def test_two_clients_chat
112
+ client1 = Yaic::Client.new(server: "localhost", port: 6667, nickname: "alice")
113
+ client2 = Yaic::Client.new(server: "localhost", port: 6667, nickname: "bob")
114
+
115
+ received = []
116
+ client2.on(:message) { |e| received << e }
117
+
118
+ client1.connect
119
+ client2.connect
120
+
121
+ client1.join("#test")
122
+ client2.join("#test")
123
+
124
+ client1.privmsg("#test", "Hello Bob!")
125
+ sleep 0.5 # Let message arrive
126
+
127
+ assert_equal 1, received.size
128
+ assert_equal "Hello Bob!", received.first.text
129
+ ensure
130
+ client1&.quit
131
+ client2&.quit
132
+ end
133
+ ```
134
+
135
+ No more:
136
+ - `instance_variable_get`
137
+ - Manual read loops
138
+ - Custom wait helpers
139
+ - `on_socket_connected`
140
+
141
+ ### Unit Tests
142
+
143
+ **connect blocks until registered**
144
+ - Given: Mock socket that returns 001 after NICK/USER
145
+ - When: `client.connect`
146
+ - Then: Returns only after state is `:connected`
147
+
148
+ **connect handles nick collision**
149
+ - Given: Mock socket returns 433 then 001
150
+ - When: `client.connect`
151
+ - Then: Retries with underscore, eventually connects
152
+
153
+ **events fire from background thread**
154
+ - Given: Connected client with :message handler
155
+ - When: Server sends PRIVMSG
156
+ - Then: Handler called without user intervention
157
+
158
+ **quit stops the read loop**
159
+ - Given: Connected client
160
+ - When: `client.quit`
161
+ - Then: Background thread terminates cleanly
162
+
163
+ **on/off are thread-safe**
164
+ - Given: Read loop running
165
+ - When: Add handler from different thread
166
+ - Then: No race condition, handler works
167
+
168
+ **join blocks until confirmed**
169
+ - Given: Connected client
170
+ - When: `client.join("#test")`
171
+ - Then: Returns only after channel appears in `@channels`
172
+
173
+ **part blocks until confirmed**
174
+ - Given: Client in #test
175
+ - When: `client.part("#test")`
176
+ - Then: Returns only after channel removed from `@channels`
177
+
178
+ **nick blocks until confirmed**
179
+ - Given: Connected client
180
+ - When: `client.nick("newnick")`
181
+ - Then: Returns only after `@nick` updated
182
+
183
+ ### Simplify Existing Integration Tests
184
+
185
+ All existing integration tests get dramatically simpler. Delete all the helper methods:
186
+ - `wait_for_connection`
187
+ - `wait_for_join`
188
+ - `wait_for_part`
189
+
190
+ Before:
191
+ ```ruby
192
+ client.connect
193
+ client.on_socket_connected
194
+ socket = client.instance_variable_get(:@socket)
195
+ wait_for_connection(client, socket)
196
+ client.join(@test_channel)
197
+ wait_for_join(client, socket, @test_channel)
198
+ ```
199
+
200
+ After:
201
+ ```ruby
202
+ client.connect
203
+ client.join(@test_channel)
204
+ ```
205
+
206
+ ## Implementation Notes
207
+
208
+ ### Structure
209
+
210
+ ```ruby
211
+ def connect
212
+ @socket = Socket.new(@server, @port, ssl: @ssl)
213
+ @socket.connect
214
+ send_registration
215
+ @state = :registering
216
+ start_read_loop
217
+ wait_until { @state == :connected }
218
+ end
219
+
220
+ def join(channel, key = nil)
221
+ params = key ? [channel, key] : [channel]
222
+ message = Message.new(command: "JOIN", params: params)
223
+ @socket.write(message.to_s)
224
+ wait_until { @channels.key?(channel) }
225
+ end
226
+
227
+ def part(channel, reason = nil)
228
+ params = reason ? [channel, reason] : [channel]
229
+ message = Message.new(command: "PART", params: params)
230
+ @socket.write(message.to_s)
231
+ wait_until { !@channels.key?(channel) }
232
+ end
233
+
234
+ def nick(new_nick)
235
+ message = Message.new(command: "NICK", params: [new_nick])
236
+ @socket.write(message.to_s)
237
+ wait_until { @nick == new_nick }
238
+ end
239
+
240
+ private
241
+
242
+ def wait_until(timeout: 10)
243
+ deadline = Time.now + timeout
244
+ until yield
245
+ raise "Operation timeout" if Time.now > deadline
246
+ sleep 0.01
247
+ end
248
+ end
249
+
250
+ def start_read_loop
251
+ @read_thread = Thread.new do
252
+ loop do
253
+ break if @state == :disconnected
254
+ process_incoming
255
+ end
256
+ end
257
+ end
258
+
259
+ def process_incoming
260
+ raw = @socket.read
261
+ return sleep(0.001) unless raw
262
+
263
+ message = Message.parse(raw)
264
+ handle_message(message) if message
265
+ rescue => e
266
+ emit(:error, nil, exception: e)
267
+ end
268
+ ```
269
+
270
+ ### Cleanup on Quit
271
+
272
+ ```ruby
273
+ def quit(reason = nil)
274
+ params = reason ? [reason] : []
275
+ message = Message.new(command: "QUIT", params: params)
276
+ @socket.write(message.to_s)
277
+ @channels.clear
278
+ @state = :disconnected
279
+ @read_thread&.join(5)
280
+ @socket&.disconnect
281
+ emit(:disconnect, nil)
282
+ end
283
+ ```
284
+
285
+ ### Graceful Error Handling
286
+
287
+ If socket dies unexpectedly:
288
+ - Set state to `:disconnected`
289
+ - Emit `:disconnect` event with error info
290
+ - Stop read loop
291
+
292
+ ## Documentation
293
+
294
+ Create `README.md` with:
295
+
296
+ 1. **Installation** - gem install / Gemfile
297
+ 2. **Quick Start** - 10-line example that works
298
+ 3. **Events** - List all events with their attributes
299
+ 4. **Commands** - All IRC commands with examples
300
+ 5. **Threading** - Brief note that a background thread handles reads
301
+
302
+ Keep it concise. The API should be obvious from the examples.
303
+
304
+ ## Dependencies
305
+
306
+ - Requires `01-tcpsocket-refactor.md` (blocking connect simplifies this)
@@ -0,0 +1,147 @@
1
+ # Connection Registration
2
+
3
+ ## Description
4
+
5
+ Implement the IRC connection registration sequence. After connecting, the client must send NICK and USER commands (optionally PASS) and wait for the server to acknowledge registration.
6
+
7
+ ## Behavior
8
+
9
+ ### Registration Sequence
10
+
11
+ 1. If password provided, send: `PASS <password>`
12
+ 2. Send: `NICK <nickname>`
13
+ 3. Send: `USER <username> 0 * :<realname>`
14
+ 4. Wait for numeric 001 (RPL_WELCOME) to confirm registration
15
+ 5. Transition state to `:connected`
16
+
17
+ ### PASS Command
18
+
19
+ - Format: `PASS <password>`
20
+ - Must be sent before NICK/USER
21
+ - Only last PASS is used if sent multiple times
22
+
23
+ ### NICK Command
24
+
25
+ - Format: `NICK <nickname>`
26
+ - Can be sent during or after registration
27
+ - Server may reject with:
28
+ - 431 ERR_NONICKNAMEGIVEN
29
+ - 432 ERR_ERRONEUSNICKNAME
30
+ - 433 ERR_NICKNAMEINUSE
31
+
32
+ ### USER Command
33
+
34
+ - Format: `USER <username> 0 * :<realname>`
35
+ - The `0` and `*` are required (historical reasons)
36
+ - realname can contain spaces
37
+
38
+ ### Handling Nick Collisions
39
+
40
+ If ERR_NICKNAMEINUSE (433) received during registration:
41
+ - Try alternative nick (append underscore or number)
42
+ - Emit event so user code can provide alternative
43
+
44
+ ### Welcome Numerics
45
+
46
+ After successful registration, server sends:
47
+ - 001 RPL_WELCOME - Registration complete
48
+ - 002 RPL_YOURHOST - Server info
49
+ - 003 RPL_CREATED - Server creation time
50
+ - 004 RPL_MYINFO - Server name and supported modes
51
+ - 005 RPL_ISUPPORT - Server capabilities (multiple lines)
52
+
53
+ ## Models
54
+
55
+ ```ruby
56
+ Yaic::Registration
57
+ - nickname: String
58
+ - username: String
59
+ - realname: String
60
+ - password: String or nil
61
+ - state: :pending, :nick_sent, :user_sent, :complete
62
+ ```
63
+
64
+ ## Tests
65
+
66
+ ### Integration Tests - Successful Registration
67
+
68
+ **Register with nickname and user**
69
+ - Given: Connected to inspircd
70
+ - When: Send NICK and USER
71
+ - Then: Receive 001 RPL_WELCOME, state becomes :connected
72
+
73
+ **Register with password**
74
+ - Given: Connected to inspircd (configured with password)
75
+ - When: Send PASS, NICK, USER
76
+ - Then: Receive 001 RPL_WELCOME
77
+
78
+ ### Integration Tests - Nick Handling
79
+
80
+ **Nick already in use**
81
+ - Given: Connected to inspircd, nick "taken" already connected
82
+ - When: Send NICK taken
83
+ - Then: Receive 433 ERR_NICKNAMEINUSE
84
+
85
+ **Invalid nickname**
86
+ - Given: Connected to inspircd
87
+ - When: Send NICK "#invalid"
88
+ - Then: Receive 432 ERR_ERRONEUSNICKNAME
89
+
90
+ **Empty nickname**
91
+ - Given: Connected to inspircd
92
+ - When: Send NICK with no parameter
93
+ - Then: Receive 431 ERR_NONICKNAMEGIVEN
94
+
95
+ ### Unit Tests - Message Formatting
96
+
97
+ **Format PASS command**
98
+ - Given: password = "secret"
99
+ - When: Build PASS message
100
+ - Then: Output = "PASS secret\r\n"
101
+
102
+ **Format NICK command**
103
+ - Given: nickname = "mynick"
104
+ - When: Build NICK message
105
+ - Then: Output = "NICK mynick\r\n"
106
+
107
+ **Format USER command**
108
+ - Given: username = "myuser", realname = "My Real Name"
109
+ - When: Build USER message
110
+ - Then: Output = "USER myuser 0 * :My Real Name\r\n"
111
+
112
+ **Format USER with empty realname**
113
+ - Given: username = "myuser", realname = ""
114
+ - When: Build USER message
115
+ - Then: Output = "USER myuser 0 * :\r\n"
116
+
117
+ ### State Machine Tests
118
+
119
+ **State transitions**
120
+ - Given: State = :disconnected
121
+ - When: Connect initiated
122
+ - Then: State = :connecting
123
+
124
+ - Given: State = :connecting
125
+ - When: Socket connected
126
+ - Then: State = :registering, NICK/USER sent
127
+
128
+ - Given: State = :registering
129
+ - When: 001 received
130
+ - Then: State = :connected
131
+
132
+ **State on nick collision**
133
+ - Given: State = :registering
134
+ - When: 433 received
135
+ - Then: State remains :registering, retry with alternate nick
136
+
137
+ ## Implementation Notes
138
+
139
+ - Parse RPL_ISUPPORT (005) to learn server capabilities
140
+ - Store ISUPPORT values for later use (CHANTYPES, CHANMODES, etc.)
141
+ - Username may be prefixed with ~ if no ident server
142
+ - Some servers require PING response during registration
143
+
144
+ ## Dependencies
145
+
146
+ - Requires `01-message-parsing.md`
147
+ - Requires `02-connection-socket.md`
@@ -0,0 +1,109 @@
1
+ # PING/PONG Keepalive
2
+
3
+ ## Description
4
+
5
+ Implement PING/PONG handling to maintain connection with the server. This is critical - servers will disconnect clients that don't respond to PING.
6
+
7
+ ## Behavior
8
+
9
+ ### Responding to PING
10
+
11
+ When server sends `PING <token>`:
12
+ 1. Immediately respond with `PONG <token>`
13
+ 2. Token must be identical to what server sent
14
+ 3. Can occur at any time, including during registration
15
+
16
+ ### Server PING Format
17
+
18
+ ```
19
+ PING <token>
20
+ PING :<token>
21
+ ```
22
+
23
+ The token may be with or without the trailing `:` prefix.
24
+
25
+ ### Client PONG Response
26
+
27
+ ```
28
+ PONG <token>
29
+ PONG :<token>
30
+ ```
31
+
32
+ Mirror the token exactly. Do NOT include a server parameter.
33
+
34
+ ### Connection Timeout Detection
35
+
36
+ If no data received from server for extended period (e.g., 180+ seconds):
37
+ - Connection may be dead
38
+ - Client should consider reconnecting
39
+
40
+ ### Latency Measurement (Optional)
41
+
42
+ - Client may send `PING <token>` to server
43
+ - Server responds with `PONG <token>`
44
+ - Measure round-trip time
45
+
46
+ ## Models
47
+
48
+ No new models required. Handled in message dispatch.
49
+
50
+ ## Tests
51
+
52
+ ### Integration Tests
53
+
54
+ **Respond to PING during registration**
55
+ - Given: Connected, registration in progress
56
+ - When: Server sends PING
57
+ - Then: Client responds with PONG, registration continues
58
+
59
+ **Respond to PING when connected**
60
+ - Given: Fully connected
61
+ - When: Server sends "PING :irc.example.com"
62
+ - Then: Client sends "PONG :irc.example.com"
63
+
64
+ **Handle PING without colon**
65
+ - Given: Fully connected
66
+ - When: Server sends "PING token123"
67
+ - Then: Client sends "PONG token123"
68
+
69
+ **Maintain connection over time**
70
+ - Given: Connected to inspircd
71
+ - When: Wait for server to send PING (typically 2-5 minutes)
72
+ - Then: Client responds, connection stays alive
73
+
74
+ ### Unit Tests
75
+
76
+ **Parse PING message**
77
+ - Given: "PING :test.server.com\r\n"
78
+ - When: Parse message
79
+ - Then: command = "PING", params = ["test.server.com"]
80
+
81
+ **Build PONG response**
82
+ - Given: PING with token "abc123"
83
+ - When: Build PONG
84
+ - Then: Output = "PONG abc123\r\n"
85
+
86
+ **Build PONG with spaces in token**
87
+ - Given: PING with token "some server"
88
+ - When: Build PONG
89
+ - Then: Output = "PONG :some server\r\n"
90
+
91
+ ### Timeout Tests
92
+
93
+ **Detect no data timeout**
94
+ - Given: Connected, no data received for 180 seconds
95
+ - When: Check connection health
96
+ - Then: Report connection as potentially dead
97
+
98
+ ## Implementation Notes
99
+
100
+ - PING handling should be automatic, not requiring user code
101
+ - Process PING before other message handling to ensure quick response
102
+ - Log PING/PONG for debugging but don't emit as regular events
103
+ - Consider emitting a `:ping` event for latency tracking
104
+
105
+ ## Dependencies
106
+
107
+ - Requires `01-message-parsing.md`
108
+ - Requires `02-connection-socket.md`
109
+ - Requires `03-registration.md` (PING can occur during registration)