evOo-local/apps/web/src/lib/heute-brief.test.ts
cynarAI 5ae8f15931 feat(web): Ingo UX — Proaktivität, Oo-Gesicht, dezentes Audio (#76)
* feat(web): Ingo UX — proactivity signals, Oo face richness, soft audio

Priority S: Heute Brief buckets (Needs you ≤3 / Doing / Done), roster doing-verbs
and Braucht-dich chips, sidebar badge on Heute, need-you ping on waiting states.

Priority A/B: Web Audio cues (app-start shimmer, interact tick, success-resolve,
need-you ouh), reaction vocals with cooldown, optional eOo cold-open voice mark.
Oo eyes gain iris/specular detail and asymmetric blink; hands reduced to rest
except punctuation poses; speaking mouth animates.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): biome format and unused import in HeuteBrief

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): a11y and import order for Heute badge

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* docs+motion: Motion God craft — pose stagger, audio bus, ducking

Add MOTION-EVOO-CHARACTER.md (Brand Soul brief): eyes lead, mouth ~80%,
hands punctuation, 40–80 ms eye→mouth lag, 220–320 ms springs, SFX LUFS
and duck-under-TTS/PTT, nod mh/Mhh, reduced-motion rules.

Implement motion-tokens, staggered mouthPose in EvooMascot, shared
oo-audio-bus with voice-lane ducking, mint→gold success, hm nod vocal.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* fix(web): align conversation roster with bot-only sidebar groups after rebase

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 14:39:40 +02:00

138 lines
3.6 KiB
TypeScript

import type { Bot, Group, RunActivityRow } from "@rakazo/contracts";
import { describe, expect, it } from "vitest";
import { buildHeuteBrief, HEUTE_NEEDS_YOU_CAP, isToday } from "./heute-brief";
const now = new Date("2026-09-22T12:00:00.000Z");
function bot(overrides: Partial<Bot> = {}): Bot {
return {
id: "bot-1",
spaceId: "space-1",
name: "Chief",
title: "",
description: "",
instructions: "",
color: "#000",
notifyOnFinish: false,
pinned: false,
sectionId: null,
archivedAt: null,
unread: false,
parentBotId: null,
memoryScope: null,
threadId: "thread-1",
preview: "",
status: "idle",
computerMode: "team",
updatedAt: "2026-09-22T10:00:00.000Z",
createdAt: "2026-09-01T00:00:00.000Z",
voiceId: null,
autoSpeak: false,
modelProvider: null,
modelId: null,
thinkingLevel: null,
teamChatAmbientEnabled: false,
teamChatRules: "",
webhookConfigured: false,
spawnKey: null,
...overrides,
};
}
function group(overrides: Partial<Group> = {}): Group {
return {
id: "group-1",
spaceId: "space-1",
name: "Launch",
pinned: false,
sectionId: null,
archivedAt: null,
members: [],
threadId: "thread-g-1",
preview: "",
unread: false,
updatedAt: "2026-09-22T09:00:00.000Z",
createdAt: "2026-09-01T00:00:00.000Z",
...overrides,
};
}
function run(overrides: Partial<RunActivityRow> = {}): RunActivityRow {
return {
runId: "run-1",
botId: "bot-1",
botName: "Chief",
groupId: null,
groupName: null,
threadId: "thread-1",
status: "waiting_input",
trigger: "user",
notificationsEnabled: true,
promptSnippet: "Need approval",
updatedAt: "2026-09-22T11:00:00.000Z",
...overrides,
};
}
describe("isToday", () => {
it("matches same calendar day", () => {
expect(isToday("2026-09-22T08:30:00.000Z", now)).toBe(true);
expect(isToday("2026-09-21T23:59:00.000Z", now)).toBe(false);
});
});
describe("buildHeuteBrief", () => {
it("collects needs-you, doing, and done buckets", () => {
const brief = buildHeuteBrief({
bots: [
bot({ id: "bot-1", unread: true }),
bot({ id: "bot-2", unread: true, updatedAt: "2026-09-22T08:00:00.000Z", name: "Steve" }),
],
groups: [group({ updatedAt: "2026-09-22T09:30:00.000Z" })],
activeRuns: [
run({ runId: "run-1", botId: "bot-1" }),
run({ runId: "run-3", botId: "bot-4", botName: "Worker", status: "running" }),
],
recentRuns: [
run({
runId: "run-2",
botId: "bot-3",
botName: "Ops",
status: "failed",
trigger: "routine",
}),
run({
runId: "run-4",
botId: "bot-5",
botName: "DoneBot",
status: "completed",
updatedAt: "2026-09-22T09:00:00.000Z",
}),
],
now,
});
expect(brief.actionCount).toBe(3);
expect(brief.needsYou.map((item) => item.kind)).toEqual(["run", "run", "unread"]);
expect(brief.doing).toHaveLength(1);
expect(brief.doing[0]?.run.runId).toBe("run-3");
expect(brief.done).toHaveLength(1);
expect(brief.done[0]?.run.runId).toBe("run-4");
});
it("caps needs-you at three items", () => {
const bots = Array.from({ length: 5 }, (_, index) =>
bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }),
);
const brief = buildHeuteBrief({
bots,
groups: [],
activeRuns: [],
recentRuns: [],
now,
});
expect(brief.needsYou).toHaveLength(HEUTE_NEEDS_YOU_CAP);
expect(brief.actionCount).toBe(5);
});
});