Skip to content

Commit 9e160d5

Browse files
committed
fix(cron): make delivery previews dry-run safe
1 parent 4f2d24f commit 9e160d5

4 files changed

Lines changed: 66 additions & 12 deletions

File tree

src/cli/cron-cli/shared.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ describe("printCronList", () => {
145145

146146
expect(logs[0]).toContain("Delivery");
147147
expect(logs[1]).toContain("announce -> telegram:-100");
148+
expect(logs[1]).toContain("resolved from last");
148149
});
149150

150151
it("shows dash in Model column for systemEvent jobs", () => {

src/cli/cron-cli/shared.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const CRON_NEXT_PAD = 10;
152152
const CRON_LAST_PAD = 10;
153153
const CRON_STATUS_PAD = 9;
154154
const CRON_TARGET_PAD = 9;
155-
const CRON_DELIVERY_PAD = 42;
155+
const CRON_DELIVERY_PAD = 64;
156156
const CRON_AGENT_PAD = 10;
157157
const CRON_MODEL_PAD = 20;
158158

@@ -278,13 +278,18 @@ export async function resolveCronDeliveryPreview(job: CronJob): Promise<CronDeli
278278
]);
279279
const cfg = loadConfig();
280280
const agentId = job.agentId?.trim() || resolveDefaultAgentId(cfg);
281-
const resolved = await resolveDeliveryTarget(cfg, agentId, {
282-
channel: requestedChannel,
283-
to: plan.to,
284-
threadId: plan.threadId,
285-
accountId: plan.accountId,
286-
sessionKey: job.sessionKey,
287-
});
281+
const resolved = await resolveDeliveryTarget(
282+
cfg,
283+
agentId,
284+
{
285+
channel: requestedChannel,
286+
to: plan.to,
287+
threadId: plan.threadId,
288+
accountId: plan.accountId,
289+
sessionKey: job.sessionKey,
290+
},
291+
{ dryRun: true },
292+
);
288293
if (!resolved.ok) {
289294
return {
290295
label: `${plan.mode} -> ${formatTarget(requestedChannel, plan.to ?? null)}`,
@@ -358,10 +363,10 @@ export function printCronList(
358363
const statusLabel = pad(statusRaw, CRON_STATUS_PAD);
359364
const targetLabel = pad(job.sessionTarget ?? "-", CRON_TARGET_PAD);
360365
const deliveryPreview = opts?.deliveryPreviews?.get(job.id);
361-
const deliveryLabel = pad(
362-
truncate(deliveryPreview?.label ?? "-", CRON_DELIVERY_PAD),
363-
CRON_DELIVERY_PAD,
364-
);
366+
const deliveryText = deliveryPreview
367+
? `${deliveryPreview.label} (${deliveryPreview.detail})`
368+
: "-";
369+
const deliveryLabel = pad(truncate(deliveryText, CRON_DELIVERY_PAD), CRON_DELIVERY_PAD);
365370
const agentLabel = pad(truncate(job.agentId ?? "-", CRON_AGENT_PAD), CRON_AGENT_PAD);
366371
const modelLabel = pad(
367372
truncate(

src/cron/isolated-agent/delivery-target.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,25 @@ describe("resolveDeliveryTarget", () => {
335335
);
336336
});
337337

338+
it("skips id-like target normalization for dry-run delivery previews", async () => {
339+
setMainSessionEntry(undefined);
340+
vi.mocked(maybeResolveIdLikeTarget).mockClear();
341+
342+
const result = await resolveDeliveryTarget(
343+
makeCfg({ bindings: [] }),
344+
AGENT_ID,
345+
{
346+
channel: "forum",
347+
to: "123456789",
348+
},
349+
{ dryRun: true },
350+
);
351+
352+
expect(result.ok).toBe(true);
353+
expect(result.to).toBe("123456789");
354+
expect(maybeResolveIdLikeTarget).not.toHaveBeenCalled();
355+
});
356+
338357
it("falls back to the runtime target resolver when the channel plugin is not already loaded", async () => {
339358
setMainSessionEntry(undefined);
340359
setActivePluginRegistry(

src/cron/isolated-agent/delivery-target.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export async function resolveDeliveryTarget(
7676
accountId?: string;
7777
sessionKey?: string;
7878
},
79+
options?: { dryRun?: boolean },
7980
): Promise<DeliveryTargetResolution> {
8081
const requestedChannel = typeof jobPayload.channel === "string" ? jobPayload.channel : "last";
8182
const explicitTo = typeof jobPayload.to === "string" ? jobPayload.to : undefined;
@@ -177,6 +178,34 @@ export async function resolveDeliveryTarget(
177178
};
178179
}
179180

181+
if (options?.dryRun) {
182+
const { getLoadedChannelPluginForRead } = await loadDeliveryTargetRuntime();
183+
const defaultTo = getLoadedChannelPluginForRead(channel)?.config.resolveDefaultTo?.({
184+
cfg,
185+
accountId,
186+
});
187+
const previewTo = toCandidate ?? defaultTo;
188+
if (!previewTo) {
189+
return {
190+
ok: false,
191+
channel,
192+
to: undefined,
193+
accountId,
194+
threadId,
195+
mode,
196+
error: new Error("Target is required for delivery preview."),
197+
};
198+
}
199+
return {
200+
ok: true,
201+
channel,
202+
to: previewTo,
203+
accountId,
204+
threadId,
205+
mode,
206+
};
207+
}
208+
180209
let effectiveAllowFrom: string[] | undefined;
181210
if (mode === "implicit") {
182211
const {

0 commit comments

Comments
 (0)