evOo-local/packages/core/src/routine-events.test.ts
cynarAI dbf37f86a8
feat: show routines the bot sets up from chat as one quiet line (#133)
* 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>
2026-10-04 20:36:53 +02:00

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" });
}
});
});