import { describe, expect, it } from "@effect/vitest";import { EnvironmentId, ThreadId } from "@t3tools/contracts";import type { RelayAgentActivityState } from "@t3tools/contracts/relay";import { makeAggregateState } from "./agentActivityAggregate.ts";import { attentionTransitionRows, terminalTransitionRows, shouldAlertForActivity,} from "./agentActivityAlerts.ts"; const state: RelayAgentActivityState = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread"), projectTitle: "Project", threadTitle: "Thread", modelTitle: "Model", headline: "Working", phase: "running", updatedAt: "1970-01-01T00:00:00.000Z", deepLink: "/threads/env/thread",};const preferences = { notificationsEnabled: true, liveActivitiesEnabled: true, notifyOnApproval: true, notifyOnInput: true, notifyOnCompletion: true, notifyOnFailure: true,};const aggregate = (states: RelayAgentActivityState[]) => makeAggregateState({ activeStates: states, terminalState: null, nowMs: 0 })!; describe("shared agent activity policy", () => { it.each(["waiting_for_approval", "waiting_for_input"] as const)( "keeps an older %s ahead of five running rows", (phase) => { const running = Array.from({ length: 5 }, (_, i) => ({ ...state, threadId: ThreadId.make(`running-${i}`), })); const waiting = { ...state, phase, updatedAt: "1969-12-31T23:59:00.000Z" }; const next = aggregate([...running, waiting]); expect(next.activities[0]?.threadId).toBe(state.threadId); expect(next.activeCount).toBe(6); expect(next.activities).toHaveLength(5); expect( attentionTransitionRows({ previousAggregate: aggregate(running), nextAggregate: next, preferences, }), ).toMatchObject([{ threadId: state.threadId }]); }, ); it("allows fresh unobserved completions for events, but not reconciliation or repeats", () => { const previousAggregate = aggregate([ { ...state, threadId: ThreadId.make("old"), phase: "completed" }, ]); const nextAggregate = aggregate([{ ...state, phase: "completed" }]); const input = { previousAggregate, nextAggregate, preferences, nowMs: 0 }; expect(terminalTransitionRows(input)).toEqual([]); expect(terminalTransitionRows({ ...input, includeUnobserved: true })).toHaveLength(1); expect( terminalTransitionRows({ ...input, includeUnobserved: true, previousAggregate: nextAggregate, }), ).toEqual([]); expect(terminalTransitionRows({ ...input, includeUnobserved: true, nowMs: 180_000 })).toEqual( [], ); }); it("distinguishes equal thread IDs across environments", () => { const waiting = { ...state, phase: "waiting_for_input" as const }; const other = { ...waiting, environmentId: EnvironmentId.make("other") }; expect( attentionTransitionRows({ previousAggregate: aggregate([waiting]), nextAggregate: aggregate([waiting, other]), preferences, }), ).toMatchObject([{ environmentId: other.environmentId }]); }); it("checks current permission, event preferences, and freshness together", () => { const input = { ...state, phase: "completed" as const, preferences, nowMs: 0 }; expect(shouldAlertForActivity(input)).toBe(true); expect( shouldAlertForActivity({ ...input, preferences: { ...preferences, notificationsEnabled: false }, }), ).toBe(false); expect( shouldAlertForActivity({ ...input, preferences: { ...preferences, notifyOnCompletion: false }, }), ).toBe(false); expect(shouldAlertForActivity({ ...input, nowMs: 180_000 })).toBe(false); });});
import { describe, expect, it } from "@effect/vitest";
import { EnvironmentId, ThreadId } from "@t3tools/contracts";
import type { RelayAgentActivityState } from "@t3tools/contracts/relay";
import { makeAggregateState } from "./agentActivityAggregate.ts";
import {
attentionTransitionRows,
terminalTransitionRows,
shouldAlertForActivity,
} from "./agentActivityAlerts.ts";
const state: RelayAgentActivityState = {
environmentId: EnvironmentId.make("env"),
threadId: ThreadId.make("thread"),
projectTitle: "Project",
threadTitle: "Thread",
modelTitle: "Model",
headline: "Working",
phase: "running",
updatedAt: "1970-01-01T00:00:00.000Z",
deepLink: "/threads/env/thread",
};
const preferences = {
notificationsEnabled: true,
liveActivitiesEnabled: true,
notifyOnApproval: true,
notifyOnInput: true,
notifyOnCompletion: true,
notifyOnFailure: true,
};
const aggregate = (states: RelayAgentActivityState[]) =>
makeAggregateState({ activeStates: states, terminalState: null, nowMs: 0 })!;
describe("shared agent activity policy", () => {
it.each(["waiting_for_approval", "waiting_for_input"] as const)(
"keeps an older %s ahead of five running rows",
(phase) => {
const running = Array.from({ length: 5 }, (_, i) => ({
...state,
threadId: ThreadId.make(`running-${i}`),
}));
const waiting = { ...state, phase, updatedAt: "1969-12-31T23:59:00.000Z" };
const next = aggregate([...running, waiting]);
expect(next.activities[0]?.threadId).toBe(state.threadId);
expect(next.activeCount).toBe(6);
expect(next.activities).toHaveLength(5);
expect(
attentionTransitionRows({
previousAggregate: aggregate(running),
nextAggregate: next,
preferences,
}),
).toMatchObject([{ threadId: state.threadId }]);
},
);
it("allows fresh unobserved completions for events, but not reconciliation or repeats", () => {
const previousAggregate = aggregate([
{ ...state, threadId: ThreadId.make("old"), phase: "completed" },
]);
const nextAggregate = aggregate([{ ...state, phase: "completed" }]);
const input = { previousAggregate, nextAggregate, preferences, nowMs: 0 };
expect(terminalTransitionRows(input)).toEqual([]);
expect(terminalTransitionRows({ ...input, includeUnobserved: true })).toHaveLength(1);
expect(
terminalTransitionRows({
...input,
includeUnobserved: true,
previousAggregate: nextAggregate,
}),
).toEqual([]);
expect(terminalTransitionRows({ ...input, includeUnobserved: true, nowMs: 180_000 })).toEqual(
[],
);
});
it("distinguishes equal thread IDs across environments", () => {
const waiting = { ...state, phase: "waiting_for_input" as const };
const other = { ...waiting, environmentId: EnvironmentId.make("other") };
expect(
attentionTransitionRows({
previousAggregate: aggregate([waiting]),
nextAggregate: aggregate([waiting, other]),
preferences,
}),
).toMatchObject([{ environmentId: other.environmentId }]);
});
it("checks current permission, event preferences, and freshness together", () => {
const input = { ...state, phase: "completed" as const, preferences, nowMs: 0 };
expect(shouldAlertForActivity(input)).toBe(true);
expect(
shouldAlertForActivity({
...input,
preferences: { ...preferences, notificationsEnabled: false },
}),
).toBe(false);
expect(
shouldAlertForActivity({
...input,
preferences: { ...preferences, notifyOnCompletion: false },
}),
).toBe(false);
expect(shouldAlertForActivity({ ...input, nowMs: 180_000 })).toBe(false);
});
});