11import { afterEach , beforeEach , describe , expect , it } from "vitest" ;
2+ import type { SkillSnapshot } from "../../agents/skills.js" ;
23import type { CronDeliveryMode } from "../types.js" ;
4+ import type { MutableCronSession } from "./run-session-state.js" ;
35import {
46 clearFastTestEnv ,
57 dispatchCronDeliveryMock ,
68 isHeartbeatOnlyResponseMock ,
79 loadRunCronIsolatedAgentTurn ,
10+ makeCronSession ,
811 mockRunCronFallbackPassthrough ,
912 resetRunCronIsolatedAgentTurnHarness ,
1013 resolveCronDeliveryPlanMock ,
@@ -14,6 +17,7 @@ import {
1417} from "./run.test-harness.js" ;
1518
1619const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn ( ) ;
20+ const { createCronPromptExecutor } = await import ( "./run-executor.js" ) ;
1721
1822function makeParams ( ) {
1923 return {
@@ -73,6 +77,13 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
7377 } ) ;
7478 } ) ;
7579
80+ const emptySkillsSnapshot : SkillSnapshot = {
81+ prompt : "" ,
82+ skills : [ ] ,
83+ resolvedSkills : [ ] ,
84+ version : 1 ,
85+ } ;
86+
7687 afterEach ( ( ) => {
7788 restoreFastTestEnv ( previousFastTestEnv ) ;
7889 } ) ;
@@ -84,6 +95,167 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
8495 } ) ;
8596 } ) ;
8697
98+ it ( "preserves explicit delivery targets for agent-initiated messaging when delivery.mode is none" , async ( ) => {
99+ mockRunCronFallbackPassthrough ( ) ;
100+ resolveCronDeliveryPlanMock . mockReturnValue ( {
101+ requested : false ,
102+ mode : "none" ,
103+ channel : "telegram" ,
104+ to : "123:topic:42" ,
105+ threadId : 42 ,
106+ } ) ;
107+ resolveDeliveryTargetMock . mockResolvedValue ( {
108+ ok : true ,
109+ channel : "telegram" ,
110+ to : "123:topic:42" ,
111+ threadId : 42 ,
112+ accountId : undefined ,
113+ error : undefined ,
114+ } ) ;
115+
116+ await runCronIsolatedAgentTurn ( {
117+ ...makeParams ( ) ,
118+ job : {
119+ id : "message-tool-policy" ,
120+ name : "Message Tool Policy" ,
121+ schedule : { kind : "every" , everyMs : 60_000 } ,
122+ sessionTarget : "isolated" ,
123+ payload : { kind : "agentTurn" , message : "send a message" } ,
124+ delivery : { mode : "none" , channel : "telegram" , to : "123:topic:42" , threadId : 42 } ,
125+ } as never ,
126+ } ) ;
127+
128+ expect ( runEmbeddedPiAgentMock ) . toHaveBeenCalledTimes ( 1 ) ;
129+ expect ( runEmbeddedPiAgentMock . mock . calls [ 0 ] ?. [ 0 ] ) . toMatchObject ( {
130+ disableMessageTool : false ,
131+ messageChannel : "telegram" ,
132+ messageTo : "123:topic:42" ,
133+ messageThreadId : 42 ,
134+ currentChannelId : "123:topic:42" ,
135+ } ) ;
136+ } ) ;
137+
138+ it ( "does not resolve implicit last-target context for bare delivery.mode none" , async ( ) => {
139+ mockRunCronFallbackPassthrough ( ) ;
140+ resolveCronDeliveryPlanMock . mockReturnValue ( {
141+ requested : false ,
142+ mode : "none" ,
143+ channel : "last" ,
144+ } ) ;
145+
146+ await runCronIsolatedAgentTurn ( {
147+ ...makeParams ( ) ,
148+ job : {
149+ id : "message-tool-policy" ,
150+ name : "Message Tool Policy" ,
151+ schedule : { kind : "every" , everyMs : 60_000 } ,
152+ sessionTarget : "isolated" ,
153+ payload : { kind : "agentTurn" , message : "send a message" } ,
154+ delivery : { mode : "none" } ,
155+ } as never ,
156+ } ) ;
157+
158+ expect ( resolveDeliveryTargetMock ) . not . toHaveBeenCalled ( ) ;
159+ expect ( runEmbeddedPiAgentMock ) . toHaveBeenCalledTimes ( 1 ) ;
160+ expect ( runEmbeddedPiAgentMock . mock . calls [ 0 ] ?. [ 0 ] ) . toMatchObject ( {
161+ disableMessageTool : false ,
162+ messageChannel : undefined ,
163+ messageTo : undefined ,
164+ messageThreadId : undefined ,
165+ currentChannelId : undefined ,
166+ } ) ;
167+ } ) ;
168+
169+ it ( "forwards explicit message targets into the embedded run" , async ( ) => {
170+ mockRunCronFallbackPassthrough ( ) ;
171+ const executor = createCronPromptExecutor ( {
172+ cfg : { } ,
173+ cfgWithAgentDefaults : { } ,
174+ job : makeParams ( ) . job ,
175+ agentId : "default" ,
176+ agentDir : "/tmp/agent-dir" ,
177+ agentSessionKey : "cron:message-tool-policy" ,
178+ workspaceDir : "/tmp/workspace" ,
179+ resolvedVerboseLevel : "off" ,
180+ thinkLevel : undefined ,
181+ timeoutMs : 60_000 ,
182+ messageChannel : "telegram" ,
183+ resolvedDelivery : {
184+ accountId : "ops" ,
185+ to : "123:topic:42" ,
186+ threadId : 42 ,
187+ } ,
188+ toolPolicy : {
189+ requireExplicitMessageTarget : false ,
190+ disableMessageTool : false ,
191+ } ,
192+ skillsSnapshot : emptySkillsSnapshot ,
193+ agentPayload : null ,
194+ liveSelection : {
195+ provider : "openai" ,
196+ model : "gpt-5.4" ,
197+ } ,
198+ cronSession : makeCronSession ( ) as MutableCronSession ,
199+ abortReason : ( ) => "aborted" ,
200+ } ) ;
201+
202+ await executor . runPrompt ( "send a message" ) ;
203+
204+ expect ( runEmbeddedPiAgentMock ) . toHaveBeenCalledTimes ( 1 ) ;
205+ expect ( runEmbeddedPiAgentMock . mock . calls [ 0 ] ?. [ 0 ] ) . toMatchObject ( {
206+ messageChannel : "telegram" ,
207+ agentAccountId : "ops" ,
208+ messageTo : "123:topic:42" ,
209+ messageThreadId : 42 ,
210+ currentChannelId : "123:topic:42" ,
211+ } ) ;
212+ } ) ;
213+
214+ it ( "preserves topic routing when inferred currentChannelId is built from split delivery fields" , async ( ) => {
215+ mockRunCronFallbackPassthrough ( ) ;
216+ const executor = createCronPromptExecutor ( {
217+ cfg : { } ,
218+ cfgWithAgentDefaults : { } ,
219+ job : makeParams ( ) . job ,
220+ agentId : "default" ,
221+ agentDir : "/tmp/agent-dir" ,
222+ agentSessionKey : "cron:message-tool-policy" ,
223+ workspaceDir : "/tmp/workspace" ,
224+ resolvedVerboseLevel : "off" ,
225+ thinkLevel : undefined ,
226+ timeoutMs : 60_000 ,
227+ messageChannel : "telegram" ,
228+ resolvedDelivery : {
229+ accountId : "ops" ,
230+ to : "123" ,
231+ threadId : 42 ,
232+ } ,
233+ toolPolicy : {
234+ requireExplicitMessageTarget : false ,
235+ disableMessageTool : false ,
236+ } ,
237+ skillsSnapshot : emptySkillsSnapshot ,
238+ agentPayload : null ,
239+ liveSelection : {
240+ provider : "openai" ,
241+ model : "gpt-5.4" ,
242+ } ,
243+ cronSession : makeCronSession ( ) as MutableCronSession ,
244+ abortReason : ( ) => "aborted" ,
245+ } ) ;
246+
247+ await executor . runPrompt ( "send a message" ) ;
248+
249+ expect ( runEmbeddedPiAgentMock ) . toHaveBeenCalledTimes ( 1 ) ;
250+ expect ( runEmbeddedPiAgentMock . mock . calls [ 0 ] ?. [ 0 ] ) . toMatchObject ( {
251+ messageChannel : "telegram" ,
252+ agentAccountId : "ops" ,
253+ messageTo : "123" ,
254+ messageThreadId : 42 ,
255+ currentChannelId : "123:topic:42" ,
256+ } ) ;
257+ } ) ;
258+
87259 it ( "disables the message tool when cron delivery is active" , async ( ) => {
88260 await expectMessageToolDisabledForPlan ( {
89261 requested : true ,
0 commit comments