* feat: show routines the bot sets up from chat as one quiet line
When someone mentions something recurring in passing ("every Sunday I plan the
week"), the bot now creates the routine right away and the conversation shows a
single line with the schedule, Edit and Undo, on web and mobile.
- New `event` message block for routine lines, kept in history so later turns
know what was set up or removed.
- schedule_create reuses an active repeating routine of the same name instead
of adding a duplicate, caps a bot at 50 active schedules, and falls back to
default times when none was given.
- Removing a routine (Undo, the editor, or schedule_cancel) marks its lines
removed via the new routines.noticeMessageId column.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
* test: give the routine notice test a complete job publisher
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1
---------
Co-authored-by: Claude <noreply@anthropic.com>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import type { MessageBlock, RoutineEventBlock } from "@rakazo/contracts";
|
|
import { describe, expect, it } from "vitest";
|
|
import { blocksToAgentHistoryText } from "./attachments.js";
|
|
import {
|
|
isRoutineEventMessage,
|
|
markRoutineEventsRemoved,
|
|
routineScheduleShape,
|
|
} from "./routine-events.js";
|
|
|
|
const line: RoutineEventBlock = {
|
|
kind: "event",
|
|
event: "routine.created",
|
|
routineId: "routine-1",
|
|
botId: "bot-1",
|
|
name: "Weekly plan",
|
|
crons: ["0 18 * * 1"],
|
|
timezone: "Europe/Berlin",
|
|
nextRunAt: "2026-10-05T16:00:00.000Z",
|
|
};
|
|
|
|
describe("routine lines", () => {
|
|
it("are messages of their own", () => {
|
|
expect(isRoutineEventMessage([line])).toBe(true);
|
|
expect(isRoutineEventMessage([line, { kind: "text", text: "Done." }])).toBe(false);
|
|
expect(isRoutineEventMessage([])).toBe(false);
|
|
});
|
|
|
|
it("are marked removed once and leave other routines alone", () => {
|
|
const other: MessageBlock = { ...line, routineId: "routine-2" };
|
|
const marked = markRoutineEventsRemoved([line, other], "routine-1");
|
|
expect(marked).toEqual([{ ...line, removed: true }, other]);
|
|
expect(markRoutineEventsRemoved(marked!, "routine-1")).toBeNull();
|
|
expect(markRoutineEventsRemoved([other], "routine-1")).toBeNull();
|
|
});
|
|
|
|
it("tell a later turn what was set up, changed or removed", () => {
|
|
expect(blocksToAgentHistoryText([line])).toBe(
|
|
"[routine set up: Weekly plan (Every Monday at 6:00 PM, Europe/Berlin)]",
|
|
);
|
|
expect(
|
|
blocksToAgentHistoryText([
|
|
{ ...line, event: "routine.updated", crons: ["@once"], name: "Call mum" },
|
|
]),
|
|
).toBe("[routine updated: Call mum (once at 2026-10-05T16:00:00.000Z)]");
|
|
expect(blocksToAgentHistoryText([{ ...line, removed: true }])).toBe(
|
|
"[routine removed: Weekly plan]",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("routineScheduleShape", () => {
|
|
it("reads the schedules people say out loud", () => {
|
|
expect(routineScheduleShape(["@once"])).toEqual({ kind: "once" });
|
|
expect(routineScheduleShape(["0 8 * * *"])).toEqual({ kind: "daily", hour: 8, minute: 0 });
|
|
expect(routineScheduleShape(["30 7 * * 1-5"])).toEqual({
|
|
kind: "weekdays",
|
|
hour: 7,
|
|
minute: 30,
|
|
});
|
|
expect(routineScheduleShape(["0 18 * * 7"])).toEqual({
|
|
kind: "weekly",
|
|
weekday: 0,
|
|
hour: 18,
|
|
minute: 0,
|
|
});
|
|
expect(routineScheduleShape(["0 9 1 * *"])).toEqual({
|
|
kind: "monthly",
|
|
day: 1,
|
|
hour: 9,
|
|
minute: 0,
|
|
});
|
|
expect(routineScheduleShape(["*/15 * * * *"])).toEqual({ kind: "minutes", every: 15 });
|
|
expect(routineScheduleShape(["0 */2 * * *"])).toEqual({ kind: "hours", every: 2 });
|
|
expect(routineScheduleShape(["0 * * * *"])).toEqual({ kind: "hourly" });
|
|
});
|
|
|
|
it("leaves everything else to the next run", () => {
|
|
for (const crons of [["0 9 * * 1,3"], ["0 9 * 1 *"], ["0 9 * * *", "0 18 * * *"], ["nope"]]) {
|
|
expect(routineScheduleShape(crons)).toEqual({ kind: "other" });
|
|
}
|
|
});
|
|
});
|