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,167 @@
1
+ # Event System
2
+
3
+ ## Description
4
+
5
+ Implement the event callback system that allows user code to react to IRC events. This is the primary interface for users of the library.
6
+
7
+ ## Behavior
8
+
9
+ ### Registering Handlers
10
+
11
+ ```ruby
12
+ client.on(:message) { |event| ... }
13
+ client.on(:join) { |event| ... }
14
+ client.on(:raw) { |message| ... }
15
+ ```
16
+
17
+ Multiple handlers can be registered for the same event. They are called in order of registration.
18
+
19
+ ### Event Dispatch
20
+
21
+ When an IRC message is received:
22
+ 1. Parse the message
23
+ 2. Determine event type(s) to emit
24
+ 3. Build event payload
25
+ 4. Call each registered handler with payload
26
+ 5. Optionally emit `:raw` for every message
27
+
28
+ ### Event Types
29
+
30
+ | Event | Trigger | Payload |
31
+ |-------|---------|---------|
32
+ | `:connect` | 001 RPL_WELCOME | `{server:}` |
33
+ | `:disconnect` | Connection closed | `{reason:}` |
34
+ | `:message` | PRIVMSG | `{source:, target:, text:}` |
35
+ | `:notice` | NOTICE | `{source:, target:, text:}` |
36
+ | `:join` | JOIN | `{channel:, user:}` |
37
+ | `:part` | PART | `{channel:, user:, reason:}` |
38
+ | `:quit` | QUIT | `{user:, reason:}` |
39
+ | `:kick` | KICK | `{channel:, user:, by:, reason:}` |
40
+ | `:nick` | NICK | `{old_nick:, new_nick:}` |
41
+ | `:topic` | TOPIC / 332 | `{channel:, topic:, setter:}` |
42
+ | `:mode` | MODE | `{target:, modes:, params:}` |
43
+ | `:error` | 4xx/5xx | `{numeric:, message:}` |
44
+ | `:raw` | Any message | `{message:}` |
45
+
46
+ ### Handler Signature
47
+
48
+ ```ruby
49
+ client.on(:message) do |event|
50
+ event.source # Yaic::Source - who sent it
51
+ event.target # String - channel or nick
52
+ event.text # String - message content
53
+ end
54
+ ```
55
+
56
+ ### Error Handling in Handlers
57
+
58
+ If a handler raises an exception:
59
+ - Log the error
60
+ - Continue calling remaining handlers
61
+ - Do not crash the event loop
62
+
63
+ ## Models
64
+
65
+ ```ruby
66
+ Yaic::Event
67
+ - type: Symbol
68
+ - attributes: Hash
69
+ - message: Yaic::Message (original parsed message)
70
+ - method_missing for attribute access
71
+ ```
72
+
73
+ ## Tests
74
+
75
+ ### Unit Tests - Handler Registration
76
+
77
+ **Register single handler**
78
+ - Given: New client
79
+ - When: Register handler for :message
80
+ - Then: Handler is stored
81
+
82
+ **Register multiple handlers for same event**
83
+ - Given: New client
84
+ - When: Register 3 handlers for :message
85
+ - Then: All 3 handlers stored in order
86
+
87
+ **Register handlers for different events**
88
+ - Given: New client
89
+ - When: Register handlers for :message, :join, :part
90
+ - Then: Each event has its handlers
91
+
92
+ ### Unit Tests - Event Dispatch
93
+
94
+ **Dispatch calls all handlers**
95
+ - Given: 3 handlers registered for :message
96
+ - When: Dispatch :message event
97
+ - Then: All 3 handlers called in order
98
+
99
+ **Dispatch with correct payload**
100
+ - Given: Handler registered for :message
101
+ - When: PRIVMSG received from "nick!user@host" to "#chan" with text "hello"
102
+ - Then: Handler receives event with source.nick="nick", target="#chan", text="hello"
103
+
104
+ **Handler exception doesn't stop others**
105
+ - Given: 3 handlers, second one raises
106
+ - When: Dispatch event
107
+ - Then: First and third handlers still called
108
+
109
+ **Unknown event type**
110
+ - Given: No handlers registered for :foo
111
+ - When: Dispatch :foo event
112
+ - Then: No error, silently ignored
113
+
114
+ ### Unit Tests - Event Type Detection
115
+
116
+ **PRIVMSG triggers :message**
117
+ - Given: PRIVMSG message received
118
+ - When: Determine event type
119
+ - Then: Returns :message
120
+
121
+ **NOTICE triggers :notice**
122
+ - Given: NOTICE message received
123
+ - When: Determine event type
124
+ - Then: Returns :notice
125
+
126
+ **JOIN triggers :join**
127
+ - Given: JOIN message received
128
+ - When: Determine event type
129
+ - Then: Returns :join
130
+
131
+ **001 triggers :connect**
132
+ - Given: 001 numeric received
133
+ - When: Determine event type
134
+ - Then: Returns :connect
135
+
136
+ **4xx/5xx triggers :error**
137
+ - Given: 433 numeric received
138
+ - When: Determine event type
139
+ - Then: Returns :error with numeric in payload
140
+
141
+ ### Integration Tests
142
+
143
+ **End-to-end message event**
144
+ - Given: Connected client with :message handler
145
+ - When: Another client sends PRIVMSG
146
+ - Then: Handler called with correct source, target, text
147
+
148
+ **End-to-end join event**
149
+ - Given: Connected client with :join handler
150
+ - When: Client joins #test
151
+ - Then: Handler called with channel="#test", user=self
152
+
153
+ **Multiple events from one message**
154
+ - Given: Connected client with :raw and :message handlers
155
+ - When: PRIVMSG received
156
+ - Then: Both :raw and :message handlers called
157
+
158
+ ## Implementation Notes
159
+
160
+ - Use simple array of [event_type, block] pairs
161
+ - Consider using a Queue for thread-safe dispatch
162
+ - Events should be fired synchronously (blocking until all handlers complete)
163
+ - Provide `client.off(:event)` to remove handlers if needed
164
+
165
+ ## Dependencies
166
+
167
+ - Requires `01-message-parsing.md` (to parse incoming messages)
@@ -0,0 +1,163 @@
1
+ # PRIVMSG and NOTICE
2
+
3
+ ## Description
4
+
5
+ Implement sending and receiving private messages and notices. This is the core messaging functionality of IRC.
6
+
7
+ ## Behavior
8
+
9
+ ### Sending PRIVMSG
10
+
11
+ ```ruby
12
+ client.privmsg("#channel", "Hello everyone!")
13
+ client.privmsg("nickname", "Hello you!")
14
+ ```
15
+
16
+ Format: `PRIVMSG <target> :<text>`
17
+
18
+ Target can be:
19
+ - Channel name (e.g., `#ruby`)
20
+ - Nickname (e.g., `dan`)
21
+
22
+ ### Sending NOTICE
23
+
24
+ ```ruby
25
+ client.notice("#channel", "Announcement!")
26
+ client.notice("nickname", "FYI...")
27
+ ```
28
+
29
+ Format: `NOTICE <target> :<text>`
30
+
31
+ Same targets as PRIVMSG. Key difference: bots should use NOTICE to avoid loops.
32
+
33
+ ### Receiving PRIVMSG
34
+
35
+ Incoming format: `:nick!user@host PRIVMSG <target> :<text>`
36
+
37
+ Parse and emit `:message` event with:
38
+ - `source` - Yaic::Source of sender
39
+ - `target` - Channel or own nick
40
+ - `text` - Message content
41
+
42
+ ### Receiving NOTICE
43
+
44
+ Incoming format: `:nick!user@host NOTICE <target> :<text>`
45
+
46
+ Parse and emit `:notice` event with same fields as :message.
47
+
48
+ ### Error Responses
49
+
50
+ - 401 ERR_NOSUCHNICK - Target user doesn't exist
51
+ - 404 ERR_CANNOTSENDTOCHAN - Cannot send to channel (banned, moderated, not joined)
52
+ - 407 ERR_TOOMANYTARGETS - Too many targets
53
+
54
+ ### Message Length
55
+
56
+ - Messages over 512 bytes (total) will be truncated by server
57
+ - Client should warn or split long messages
58
+ - Usable text length ≈ 400-450 chars depending on nick/channel length
59
+
60
+ ## Models
61
+
62
+ No new models. Uses existing Message and Event.
63
+
64
+ ## Tests
65
+
66
+ ### Integration Tests - Sending
67
+
68
+ **Send PRIVMSG to channel**
69
+ - Given: Client joined to #test
70
+ - When: `client.privmsg("#test", "Hello")`
71
+ - Then: Server receives "PRIVMSG #test :Hello"
72
+
73
+ **Send PRIVMSG to user**
74
+ - Given: Connected client
75
+ - When: `client.privmsg("othernick", "Hello")`
76
+ - Then: Server receives "PRIVMSG othernick :Hello"
77
+
78
+ **Send NOTICE to channel**
79
+ - Given: Client joined to #test
80
+ - When: `client.notice("#test", "Announcement")`
81
+ - Then: Server receives "NOTICE #test :Announcement"
82
+
83
+ **Send message with special characters**
84
+ - Given: Connected client
85
+ - When: `client.privmsg("#test", "Hello :) world")`
86
+ - Then: Server receives "PRIVMSG #test :Hello :) world" (colon preserved)
87
+
88
+ ### Integration Tests - Receiving
89
+
90
+ **Receive PRIVMSG from user**
91
+ - Given: Client with :message handler
92
+ - When: Other user sends PRIVMSG to client's nick
93
+ - Then: Handler receives event with source, target=own_nick, text
94
+
95
+ **Receive PRIVMSG in channel**
96
+ - Given: Client joined #test with :message handler
97
+ - When: Other user sends PRIVMSG to #test
98
+ - Then: Handler receives event with source, target="#test", text
99
+
100
+ **Receive NOTICE**
101
+ - Given: Client with :notice handler
102
+ - When: Server sends NOTICE
103
+ - Then: Handler receives event with source, target, text
104
+
105
+ **Distinguish channel from private message**
106
+ - Given: :message handler that tracks target
107
+ - When: Receive PRIVMSG to "#chan" vs PRIVMSG to "mynick"
108
+ - Then: target reflects correct destination
109
+
110
+ ### Unit Tests - Message Formatting
111
+
112
+ **Format PRIVMSG**
113
+ - Given: target = "#test", text = "Hello"
114
+ - When: Build PRIVMSG
115
+ - Then: Output = "PRIVMSG #test :Hello\r\n"
116
+
117
+ **Format PRIVMSG with colon in text**
118
+ - Given: target = "#test", text = ":smile:"
119
+ - When: Build PRIVMSG
120
+ - Then: Output = "PRIVMSG #test ::smile:\r\n"
121
+
122
+ **Format NOTICE**
123
+ - Given: target = "nick", text = "Info"
124
+ - When: Build NOTICE
125
+ - Then: Output = "NOTICE nick :Info\r\n"
126
+
127
+ ### Unit Tests - Event Parsing
128
+
129
+ **Parse PRIVMSG event**
130
+ - Given: `:dan!d@host PRIVMSG #ruby :Hello everyone`
131
+ - When: Parse and create event
132
+ - Then: event.type = :message, source.nick = "dan", target = "#ruby", text = "Hello everyone"
133
+
134
+ **Parse NOTICE event**
135
+ - Given: `:server NOTICE * :Looking up hostname`
136
+ - When: Parse and create event
137
+ - Then: event.type = :notice, source = server, target = "*", text = "Looking up hostname"
138
+
139
+ ### Error Handling Tests
140
+
141
+ **Send to non-existent nick**
142
+ - Given: Connected client
143
+ - When: `client.privmsg("nonexistent", "Hello")`
144
+ - Then: Receive 401 ERR_NOSUCHNICK, :error event emitted
145
+
146
+ **Send to channel not joined**
147
+ - Given: Connected client, not in #secret
148
+ - When: `client.privmsg("#secret", "Hello")`
149
+ - Then: Receive 404 ERR_CANNOTSENDTOCHAN
150
+
151
+ ## Implementation Notes
152
+
153
+ - PRIVMSG text always needs trailing colon prefix (may contain spaces)
154
+ - Consider convenience method: `client.msg(target, text)` as alias for privmsg
155
+ - Bot authors should use notice() for automatic responses
156
+ - Track own nick to detect private vs channel messages
157
+
158
+ ## Dependencies
159
+
160
+ - Requires `01-message-parsing.md`
161
+ - Requires `02-connection-socket.md`
162
+ - Requires `03-registration.md` (must be registered to send)
163
+ - Requires `05-event-system.md`
@@ -0,0 +1,190 @@
1
+ # JOIN and PART
2
+
3
+ ## Description
4
+
5
+ Implement joining and leaving IRC channels. This is essential for participating in channel conversations.
6
+
7
+ ## Behavior
8
+
9
+ ### JOIN Command
10
+
11
+ ```ruby
12
+ client.join("#ruby")
13
+ client.join("#ruby", "channelkey") # For key-protected channels
14
+ client.join("#a,#b,#c") # Multiple channels
15
+ ```
16
+
17
+ Format: `JOIN <channel>[,<channel>...] [<key>[,<key>...]]`
18
+
19
+ Special case: `JOIN 0` leaves all channels.
20
+
21
+ ### Server Response to JOIN
22
+
23
+ On successful join, server sends:
24
+ 1. `:yournick!user@host JOIN #channel` - Confirmation
25
+ 2. `332 RPL_TOPIC` - Channel topic (if set)
26
+ 3. `333 RPL_TOPICWHOTIME` - Who set topic and when (optional)
27
+ 4. `353 RPL_NAMREPLY` - List of users in channel (may be multiple)
28
+ 5. `366 RPL_ENDOFNAMES` - End of names list
29
+
30
+ ### JOIN Errors
31
+
32
+ - 403 ERR_NOSUCHCHANNEL - Invalid channel name
33
+ - 405 ERR_TOOMANYCHANNELS - Client limit reached
34
+ - 471 ERR_CHANNELISFULL - Channel is full (+l mode)
35
+ - 473 ERR_INVITEONLYCHAN - Channel is invite-only (+i mode)
36
+ - 474 ERR_BANNEDFROMCHAN - Banned from channel
37
+ - 475 ERR_BADCHANNELKEY - Wrong or missing key (+k mode)
38
+
39
+ ### PART Command
40
+
41
+ ```ruby
42
+ client.part("#ruby")
43
+ client.part("#ruby", "Goodbye!") # With reason
44
+ client.part("#a,#b") # Multiple channels
45
+ ```
46
+
47
+ Format: `PART <channel>[,<channel>...] [:<reason>]`
48
+
49
+ ### Server Response to PART
50
+
51
+ Server sends: `:yournick!user@host PART #channel :reason`
52
+
53
+ ### PART Errors
54
+
55
+ - 403 ERR_NOSUCHCHANNEL - Channel doesn't exist
56
+ - 442 ERR_NOTONCHANNEL - Not in that channel
57
+
58
+ ### Tracking Other Users
59
+
60
+ When other users JOIN/PART:
61
+ - `:othernick!user@host JOIN #channel`
62
+ - `:othernick!user@host PART #channel :reason`
63
+
64
+ Emit appropriate events for these.
65
+
66
+ ## Models
67
+
68
+ Track joined channels:
69
+ ```ruby
70
+ client.channels # => {"#ruby" => Yaic::Channel, ...}
71
+ ```
72
+
73
+ ## Tests
74
+
75
+ ### Integration Tests - JOIN
76
+
77
+ **Join single channel**
78
+ - Given: Connected client
79
+ - When: `client.join("#test")`
80
+ - Then: Receive JOIN confirmation, RPL_NAMREPLY, RPL_ENDOFNAMES
81
+
82
+ **Join channel with topic**
83
+ - Given: Connected client, #test has topic
84
+ - When: `client.join("#test")`
85
+ - Then: Receive RPL_TOPIC with topic text
86
+
87
+ **Join key-protected channel**
88
+ - Given: Connected client, #secret requires key "pass123"
89
+ - When: `client.join("#secret", "pass123")`
90
+ - Then: Successfully joins
91
+
92
+ **Join key-protected with wrong key**
93
+ - Given: Connected client, #secret requires key
94
+ - When: `client.join("#secret", "wrongkey")`
95
+ - Then: Receive 475 ERR_BADCHANNELKEY
96
+
97
+ **Join multiple channels**
98
+ - Given: Connected client
99
+ - When: `client.join("#a,#b,#c")`
100
+ - Then: Joined to all three channels
101
+
102
+ **Join non-existent channel creates it**
103
+ - Given: Connected client
104
+ - When: `client.join("#newchannel")`
105
+ - Then: Channel created, client is operator
106
+
107
+ ### Integration Tests - PART
108
+
109
+ **Part single channel**
110
+ - Given: Client in #test
111
+ - When: `client.part("#test")`
112
+ - Then: Receive PART confirmation, channel removed from tracking
113
+
114
+ **Part with reason**
115
+ - Given: Client in #test
116
+ - When: `client.part("#test", "Going home")`
117
+ - Then: Reason included in PART message
118
+
119
+ **Part channel not in**
120
+ - Given: Client not in #other
121
+ - When: `client.part("#other")`
122
+ - Then: Receive 442 ERR_NOTONCHANNEL
123
+
124
+ ### Integration Tests - Events
125
+
126
+ **Emit :join event on self join**
127
+ - Given: Client with :join handler
128
+ - When: Join #test
129
+ - Then: Handler called with channel="#test", user=self
130
+
131
+ **Emit :join event on other join**
132
+ - Given: Client in #test with :join handler
133
+ - When: Other user joins #test
134
+ - Then: Handler called with channel="#test", user=other
135
+
136
+ **Emit :part event on self part**
137
+ - Given: Client in #test with :part handler
138
+ - When: Part #test
139
+ - Then: Handler called with channel="#test", user=self
140
+
141
+ **Emit :part event on other part**
142
+ - Given: Client in #test with :part handler
143
+ - When: Other user parts #test
144
+ - Then: Handler called with channel="#test", user=other, reason
145
+
146
+ ### Unit Tests
147
+
148
+ **Format JOIN**
149
+ - Given: channel = "#test"
150
+ - When: Build JOIN
151
+ - Then: Output = "JOIN #test\r\n"
152
+
153
+ **Format JOIN with key**
154
+ - Given: channel = "#test", key = "secret"
155
+ - When: Build JOIN
156
+ - Then: Output = "JOIN #test secret\r\n"
157
+
158
+ **Format PART**
159
+ - Given: channel = "#test"
160
+ - When: Build PART
161
+ - Then: Output = "PART #test\r\n"
162
+
163
+ **Format PART with reason**
164
+ - Given: channel = "#test", reason = "Bye all"
165
+ - When: Build PART
166
+ - Then: Output = "PART #test :Bye all\r\n"
167
+
168
+ **Parse JOIN event**
169
+ - Given: `:nick!u@h JOIN #test`
170
+ - When: Parse
171
+ - Then: event.channel = "#test", event.user.nick = "nick"
172
+
173
+ **Parse PART event**
174
+ - Given: `:nick!u@h PART #test :Later`
175
+ - When: Parse
176
+ - Then: event.channel = "#test", event.user.nick = "nick", event.reason = "Later"
177
+
178
+ ## Implementation Notes
179
+
180
+ - Track which channels client is in via `client.channels` hash
181
+ - Update channel user list on JOIN/PART from others
182
+ - RPL_NAMREPLY may come in multiple messages - collect until RPL_ENDOFNAMES
183
+ - Channel names start with # or & (check CHANTYPES from ISUPPORT)
184
+
185
+ ## Dependencies
186
+
187
+ - Requires `01-message-parsing.md`
188
+ - Requires `02-connection-socket.md`
189
+ - Requires `03-registration.md`
190
+ - Requires `05-event-system.md`
@@ -0,0 +1,118 @@
1
+ # QUIT Command
2
+
3
+ ## Description
4
+
5
+ Implement the QUIT command for gracefully disconnecting from the server, and handle QUIT messages from other users.
6
+
7
+ ## Behavior
8
+
9
+ ### Sending QUIT
10
+
11
+ ```ruby
12
+ client.quit
13
+ client.quit("Gone for lunch")
14
+ ```
15
+
16
+ Format: `QUIT [:<reason>]`
17
+
18
+ After sending QUIT:
19
+ 1. Server sends ERROR message
20
+ 2. Server closes connection
21
+ 3. Client should close socket and clean up
22
+
23
+ ### Server Response to QUIT
24
+
25
+ Server sends: `ERROR :Closing Link: nick[host] (Quit: reason)`
26
+
27
+ Then closes the connection.
28
+
29
+ ### Other Users Quitting
30
+
31
+ When another user quits: `:nick!user@host QUIT :reason`
32
+
33
+ Server prepends "Quit: " to reason when distributing.
34
+
35
+ Netsplits appear as: `:nick!user@host QUIT :server1 server2`
36
+
37
+ ### Events
38
+
39
+ Emit `:quit` event with:
40
+ - `user` - Source of the quit
41
+ - `reason` - Quit message
42
+
43
+ Emit `:disconnect` event when own connection closes.
44
+
45
+ ## Models
46
+
47
+ No new models.
48
+
49
+ ## Tests
50
+
51
+ ### Integration Tests
52
+
53
+ **Quit without reason**
54
+ - Given: Connected client
55
+ - When: `client.quit`
56
+ - Then: Server receives "QUIT", connection closes
57
+
58
+ **Quit with reason**
59
+ - Given: Connected client
60
+ - When: `client.quit("Bye!")`
61
+ - Then: Server receives "QUIT :Bye!", connection closes
62
+
63
+ **Receive other user quit**
64
+ - Given: Client in #test with :quit handler, other user in #test
65
+ - When: Other user quits
66
+ - Then: Handler called with user=other, reason
67
+
68
+ **Detect netsplit quit**
69
+ - Given: Client with :quit handler
70
+ - When: Receive QUIT with reason "*.net *.split"
71
+ - Then: Handler receives netsplit-style reason
72
+
73
+ ### Unit Tests
74
+
75
+ **Format QUIT**
76
+ - Given: No reason
77
+ - When: Build QUIT
78
+ - Then: Output = "QUIT\r\n"
79
+
80
+ **Format QUIT with reason**
81
+ - Given: reason = "Going away"
82
+ - When: Build QUIT
83
+ - Then: Output = "QUIT :Going away\r\n"
84
+
85
+ **Parse QUIT event**
86
+ - Given: `:nick!u@h QUIT :Quit: Leaving`
87
+ - When: Parse
88
+ - Then: event.user.nick = "nick", event.reason = "Quit: Leaving"
89
+
90
+ **Parse netsplit QUIT**
91
+ - Given: `:nick!u@h QUIT :hub.net leaf.net`
92
+ - When: Parse
93
+ - Then: event.reason = "hub.net leaf.net"
94
+
95
+ ### State Tests
96
+
97
+ **State after quit**
98
+ - Given: Connected client
99
+ - When: Quit
100
+ - Then: State = :disconnected, channels cleared
101
+
102
+ **Cleanup after quit**
103
+ - Given: Client in multiple channels
104
+ - When: Quit
105
+ - Then: All channel tracking cleared
106
+
107
+ ## Implementation Notes
108
+
109
+ - QUIT is the graceful way to disconnect
110
+ - Wait briefly for ERROR response before closing socket
111
+ - Remove quitting users from all tracked channels
112
+ - Consider implementing `client.disconnect` as alias
113
+
114
+ ## Dependencies
115
+
116
+ - Requires `01-message-parsing.md`
117
+ - Requires `02-connection-socket.md`
118
+ - Requires `05-event-system.md`