1- import { spawnSync } from "node:child_process" ;
1+ import { spawn , spawnSync } from "node:child_process" ;
2+ import http from "node:http" ;
23import { describe , expect , it } from "vitest" ;
34
5+ type CliResult = {
6+ status : number | null ;
7+ stdout : string ;
8+ stderr : string ;
9+ } ;
10+
11+ async function withApiServer (
12+ handler : ( request : http . IncomingMessage , response : http . ServerResponse ) => void ,
13+ callback : ( baseUrl : string ) => Promise < void > ,
14+ ) {
15+ const server = http . createServer ( handler ) ;
16+ await new Promise < void > ( ( resolve ) => {
17+ server . listen ( 0 , "127.0.0.1" , resolve ) ;
18+ } ) ;
19+ const address = server . address ( ) ;
20+ if ( ! address || typeof address === "string" ) {
21+ server . close ( ) ;
22+ throw new Error ( "Expected TCP server address." ) ;
23+ }
24+ try {
25+ await callback ( `http://127.0.0.1:${ address . port } ` ) ;
26+ } finally {
27+ await new Promise < void > ( ( resolve ) => server . close ( ( ) => resolve ( ) ) ) ;
28+ }
29+ }
30+
31+ async function runCli ( args : string [ ] , apiBase : string ) : Promise < CliResult > {
32+ const child = spawn ( process . execPath , [ "scripts/agent-comms.mjs" , ...args ] , {
33+ cwd : process . cwd ( ) ,
34+ env : {
35+ PATH : process . env . PATH ?? "" ,
36+ AGENT_COMMS_API_BASE : apiBase ,
37+ AGENT_COMMS_TOKEN : "test-token" ,
38+ } ,
39+ } ) ;
40+ let stdout = "" ;
41+ let stderr = "" ;
42+ child . stdout . setEncoding ( "utf8" ) ;
43+ child . stderr . setEncoding ( "utf8" ) ;
44+ child . stdout . on ( "data" , ( chunk ) => {
45+ stdout += chunk ;
46+ } ) ;
47+ child . stderr . on ( "data" , ( chunk ) => {
48+ stderr += chunk ;
49+ } ) ;
50+ const timeout = setTimeout ( ( ) => {
51+ child . kill ( "SIGKILL" ) ;
52+ } , 5_000 ) ;
53+ const status = await new Promise < number | null > ( ( resolve ) => {
54+ child . on ( "close" , resolve ) ;
55+ } ) ;
56+ clearTimeout ( timeout ) ;
57+ return { status, stdout, stderr } ;
58+ }
59+
60+ function sendJson ( response : http . ServerResponse , payload : unknown ) {
61+ response . writeHead ( 200 , { "content-type" : "application/json" } ) ;
62+ response . end ( JSON . stringify ( payload ) ) ;
63+ }
64+
465describe ( "CLI" , ( ) => {
566 it ( "reports invalid mark-read target types before requiring API configuration" , ( ) => {
667 const result = spawnSync ( process . execPath , [ "scripts/agent-comms.mjs" , "mark-read" , "channel" , "dm_project_peer" , "dm_msg_123" ] , {
@@ -22,4 +83,161 @@ describe("CLI", () => {
2283 expect ( payload . validTargetTypes ) . toEqual ( [ "thread" , "conversation" , "suggestion" , "mention" , "todo" ] ) ;
2384 expect ( payload . acceptedAliases ?. conversation ) . toContain ( "dm" ) ;
2485 } ) ;
86+
87+ it ( "reports only peer messages created during the live-watch window as newMessages" , async ( ) => {
88+ const oldMessage = {
89+ id : "dm_msg_old" ,
90+ body : "Already handled." ,
91+ createdAt : "2026-01-01T00:00:00.000Z" ,
92+ senderAgentId : "agent_peer" ,
93+ } ;
94+ const newMessage = {
95+ id : "dm_msg_new" ,
96+ body : "Fresh during watch." ,
97+ createdAt : new Date ( Date . now ( ) + 1_000 ) . toISOString ( ) ,
98+ senderAgentId : "agent_peer" ,
99+ } ;
100+ let directMessageReads = 0 ;
101+
102+ await withApiServer ( ( request , response ) => {
103+ const url = request . url ?? "" ;
104+ if ( url . startsWith ( "/api/agent/context/agent_test" ) ) {
105+ sendJson ( response , {
106+ liveConversationSessions : [
107+ {
108+ id : "live_1" ,
109+ conversationId : "dm_1" ,
110+ status : "active" ,
111+ receipts : [ { agentId : "agent_test" , lastSeenMessageId : null } ] ,
112+ } ,
113+ ] ,
114+ } ) ;
115+ return ;
116+ }
117+ if ( url . startsWith ( "/api/agent/direct-messages/dm_1" ) ) {
118+ directMessageReads += 1 ;
119+ sendJson ( response , {
120+ messages : directMessageReads === 1 ? [ ] : [ oldMessage , newMessage ] ,
121+ } ) ;
122+ return ;
123+ }
124+ response . writeHead ( 404 , { "content-type" : "application/json" } ) ;
125+ response . end ( JSON . stringify ( { error : `Unexpected ${ url } ` } ) ) ;
126+ } , async ( apiBase ) => {
127+ const result = await runCli ( [
128+ "live-watch" ,
129+ "agent_test" ,
130+ "--timeout-seconds" ,
131+ "2" ,
132+ "--interval-seconds" ,
133+ "0.01" ,
134+ ] , apiBase ) ;
135+
136+ expect ( result . status ) . toBe ( 0 ) ;
137+ expect ( result . stderr ) . toBe ( "" ) ;
138+ const payload = JSON . parse ( result . stdout ) as {
139+ latestActionableMessage ?: { id ?: string } ;
140+ newMessages ?: Array < { id ?: string } > ;
141+ } ;
142+ expect ( payload . latestActionableMessage ?. id ) . toBe ( "dm_msg_new" ) ;
143+ expect ( payload . newMessages ?. map ( ( message ) => message . id ) ) . toEqual ( [ "dm_msg_new" ] ) ;
144+ } ) ;
145+ } ) ;
146+
147+ it ( "returns an empty newMessages array for pre-existing live-watch actionable state" , async ( ) => {
148+ await withApiServer ( ( request , response ) => {
149+ const url = request . url ?? "" ;
150+ if ( url . startsWith ( "/api/agent/context/agent_test" ) ) {
151+ sendJson ( response , {
152+ liveConversationSessions : [
153+ {
154+ id : "live_1" ,
155+ conversationId : "dm_1" ,
156+ status : "active" ,
157+ receipts : [ { agentId : "agent_test" , lastSeenMessageId : null } ] ,
158+ } ,
159+ ] ,
160+ } ) ;
161+ return ;
162+ }
163+ if ( url . startsWith ( "/api/agent/direct-messages/dm_1" ) ) {
164+ sendJson ( response , {
165+ messages : [
166+ {
167+ id : "dm_msg_old" ,
168+ body : "Already waiting." ,
169+ createdAt : "2026-01-01T00:00:00.000Z" ,
170+ senderAgentId : "agent_peer" ,
171+ } ,
172+ ] ,
173+ } ) ;
174+ return ;
175+ }
176+ response . writeHead ( 404 , { "content-type" : "application/json" } ) ;
177+ response . end ( JSON . stringify ( { error : `Unexpected ${ url } ` } ) ) ;
178+ } , async ( apiBase ) => {
179+ const result = await runCli ( [
180+ "live-watch" ,
181+ "agent_test" ,
182+ "--timeout-seconds" ,
183+ "2" ,
184+ "--interval-seconds" ,
185+ "0.01" ,
186+ ] , apiBase ) ;
187+
188+ expect ( result . status ) . toBe ( 0 ) ;
189+ expect ( result . stderr ) . toBe ( "" ) ;
190+ const payload = JSON . parse ( result . stdout ) as {
191+ latestActionableMessage ?: { id ?: string } ;
192+ newMessages ?: Array < { id ?: string } > ;
193+ } ;
194+ expect ( payload . latestActionableMessage ?. id ) . toBe ( "dm_msg_old" ) ;
195+ expect ( payload . newMessages ) . toEqual ( [ ] ) ;
196+ } ) ;
197+ } ) ;
198+
199+ it ( "includes newMessages on timed-out live-watch responses" , async ( ) => {
200+ await withApiServer ( ( request , response ) => {
201+ const url = request . url ?? "" ;
202+ if ( url . startsWith ( "/api/agent/context/agent_test" ) ) {
203+ sendJson ( response , {
204+ liveConversationSessions : [
205+ {
206+ id : "live_1" ,
207+ conversationId : "dm_1" ,
208+ status : "active" ,
209+ receipts : [ { agentId : "agent_test" , lastSeenMessageId : null } ] ,
210+ } ,
211+ ] ,
212+ } ) ;
213+ return ;
214+ }
215+ if ( url . startsWith ( "/api/agent/direct-messages/dm_1" ) ) {
216+ sendJson ( response , { messages : [ ] } ) ;
217+ return ;
218+ }
219+ response . writeHead ( 404 , { "content-type" : "application/json" } ) ;
220+ response . end ( JSON . stringify ( { error : `Unexpected ${ url } ` } ) ) ;
221+ } , async ( apiBase ) => {
222+ const result = await runCli ( [
223+ "live-watch" ,
224+ "agent_test" ,
225+ "--timeout-seconds" ,
226+ "0.05" ,
227+ "--interval-seconds" ,
228+ "0.01" ,
229+ ] , apiBase ) ;
230+
231+ expect ( result . status ) . toBe ( 0 ) ;
232+ expect ( result . stderr ) . toBe ( "" ) ;
233+ const payload = JSON . parse ( result . stdout ) as {
234+ timedOut ?: boolean ;
235+ newMessages ?: unknown [ ] ;
236+ latest ?: { conversations ?: Array < { newMessages ?: unknown [ ] } > } ;
237+ } ;
238+ expect ( payload . timedOut ) . toBe ( true ) ;
239+ expect ( payload . newMessages ) . toEqual ( [ ] ) ;
240+ expect ( payload . latest ?. conversations ?. [ 0 ] ?. newMessages ) . toEqual ( [ ] ) ;
241+ } ) ;
242+ } ) ;
25243} ) ;
0 commit comments