0cb5e41faf
- Fix: an errored session alerted on every update, and Clear alerts only dropped it in memory, so its next update (or a reload) brought the alert straight back. An error now alerts on the transition into error only; cleared sessions are remembered (agenthub.pet.dismissedErrors) and stay quiet until they recover and fail anew. - Quips, greetings, notifications and Ask answers move to i18n (EN/VI); Ask understands Vietnamese keywords and names the sessions it counts. - The pet goes by the worn pet's name; click it in the panel to rename (agenthub.pet.names, synced to the account). - Bubbles name the session they are about, with an Open button (never a resume); a burst of the same event coalesces into one count. - Moments: a new session waves, a finish hops, a subagent peeks, a wait taps, an error flinches — on Sagi, Codex sprites and still images. Idle fidgets, a once-per-session greeting, a "Needs you" list, an amber waiting dot. - Pat (heart button, Ask, or stroking the pet) reacts on the pet itself, so it shows while the panel or chat is open. - Panel drops Run Claude / Activity / Sessions / Errored. - Ask hands off to a chat window (PetChat) on the default AI tool (Claude/Codex): transcript as bubbles, follow-ups and CLI menus through an off-screen attached terminal; no new server route. Verified on HEAD + this diff in a clean worktree: tsc -b, the full client suite (1334/1335; the one failure, ConversationView.follow, is flaky under load and passes 3/3 alone) and server me-prefs.test.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
/**
|
|
* @file intents.test.ts
|
|
* @description Unit tests for Sagi's intent matcher — natural-language queries mapped to dashboard navigation and status intents.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { matchIntent } from "../intents";
|
|
import type { SagiStatus } from "../brain";
|
|
|
|
const status = (over: Partial<SagiStatus> = {}): SagiStatus => ({
|
|
liveCount: 0,
|
|
waitingCount: 0,
|
|
errorCount: 0,
|
|
connected: true,
|
|
...over,
|
|
});
|
|
|
|
describe("matchIntent", () => {
|
|
it("reports live sessions", () => {
|
|
const r = matchIntent("what's running?", status({ liveCount: 3 }));
|
|
expect(r).toEqual({ kind: "answer", text: expect.stringContaining("3 sessions live") });
|
|
});
|
|
|
|
it("singularizes correctly", () => {
|
|
const r = matchIntent("anything active", status({ liveCount: 1 }));
|
|
expect(r.kind).toBe("answer");
|
|
if (r.kind === "answer") expect(r.text).toContain("1 session live");
|
|
});
|
|
|
|
it("says all quiet when nothing live", () => {
|
|
const r = matchIntent("what is running", status());
|
|
if (r.kind === "answer") expect(r.text).toContain("nothing's running");
|
|
});
|
|
|
|
it("reports errors and prioritizes error intent over live", () => {
|
|
const r = matchIntent("any failed runs?", status({ liveCount: 2, errorCount: 1 }));
|
|
if (r.kind === "answer") expect(r.text).toContain("1 session errored");
|
|
});
|
|
|
|
it("clean when no errors", () => {
|
|
const r = matchIntent("are there errors", status({ liveCount: 2 }));
|
|
if (r.kind === "answer") expect(r.text).toContain("all clean");
|
|
});
|
|
|
|
it("gives a combined status summary", () => {
|
|
const r = matchIntent(
|
|
"status",
|
|
status({ liveCount: 2, waitingCount: 1, errorCount: 1, connected: true })
|
|
);
|
|
if (r.kind === "answer") expect(r.text).toBe("2 live · 1 waiting · 1 errored · connected.");
|
|
});
|
|
|
|
it("reports sessions waiting on the user", () => {
|
|
const r = matchIntent("anything waiting on me?", status({ liveCount: 2, waitingCount: 1 }));
|
|
if (r.kind === "answer") expect(r.text).toContain("1 session waiting on you");
|
|
});
|
|
|
|
it("reflects offline in summary", () => {
|
|
const r = matchIntent("overview", status({ connected: false }));
|
|
if (r.kind === "answer") expect(r.text).toContain("offline");
|
|
});
|
|
|
|
it("explains itself on help", () => {
|
|
const r = matchIntent("help", status());
|
|
if (r.kind === "answer") expect(r.text.toLowerCase()).toContain("watch your sessions");
|
|
});
|
|
|
|
it("empty query nudges the user", () => {
|
|
const r = matchIntent(" ", status());
|
|
expect(r.kind).toBe("answer");
|
|
});
|
|
|
|
it("hands unknown questions to Claude, preserving original casing", () => {
|
|
const r = matchIntent("Refactor my auth module", status());
|
|
expect(r).toEqual({ kind: "handoff", prompt: "Refactor my auth module" });
|
|
});
|
|
|
|
it("understands Vietnamese keywords", () => {
|
|
const r = matchIntent("có lỗi nào không?", status({ errorCount: 2 }));
|
|
if (r.kind === "answer") expect(r.text).toContain("2 sessions errored");
|
|
expect(matchIntent("đang chạy gì vậy", status({ liveCount: 1 })).kind).toBe("answer");
|
|
expect(matchIntent("tình hình sao rồi", status()).kind).toBe("answer");
|
|
});
|
|
|
|
it("names the sessions it reports when it knows them", () => {
|
|
const r = matchIntent("any errors?", status({ errorCount: 1 }), {
|
|
attention: [{ id: "a", kind: "error", name: "Fix login", cwd: null, lane_id: null }],
|
|
});
|
|
if (r.kind === "answer") expect(r.text).toContain("errored: Fix login");
|
|
});
|
|
|
|
it("introduces itself by the pet's name and reacts to a pat", () => {
|
|
const who = matchIntent("who are you", status(), { name: "Mochi" });
|
|
expect(who).toMatchObject({ kind: "answer", moment: "wave" });
|
|
if (who.kind === "answer") expect(who.text).toContain("Mochi");
|
|
expect(matchIntent("pat", status())).toMatchObject({ kind: "answer", moment: "jump" });
|
|
// "pat" inside another word is not a pat.
|
|
expect(matchIntent("update the path", status()).kind).toBe("handoff");
|
|
});
|
|
});
|