mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(tg): Mini App V6 — premium overhaul (design system, Chat parity, Metrics drilldown, CEO verbs) Design system: native type with tabular-numeral heroes (mono demoted to the wordmark), borderless elevated cards, floating dock, Telegram window-chrome painting via the theme bridge; Inbox moves behind a header bell with humanized notifications (UUIDs resolve to task names). Chat: honest Mine/Fleet split — participant-scoped CEO threads with real unread counts and mark-read, watched fleet threads with reply-as-CEO on task-linked conversations (watch-only otherwise), markdown transcripts, live pulse flashes, and a pinned Secretary live chat on the panel's SSE session runtime. Metrics: new tab with period-segmented spend hero, by-agent/team/model breakdowns, delivery + efficiency health, and a per-agent drilldown over usage time-series (agent_slug) + member scorecard. Board: tg-native grouped pipeline replacing the MobileTaskBoard wrapper; task sheet gains the CEO decide verbs (approve / request changes / unblock). Security: /api/dashboard router now require_panel_token-gated at router level (mirrors /api/usage), closing unauthenticated metrics exposure. * fix(tg): restore Share Tech Mono brand voice, Phosphor icon set, borderless avatars The mono returns as the numeral/brand voice (.tg-display — heroes, stat values, wordmark) while labels stay native sentence case. The hand-drawn duotone glyphs and lucide feature icons are replaced by Phosphor (MIT): duotone at rest via an IconContext at the shell, filled weight on the dock's active tab; row glyph maps (board statuses, inbox kinds, approval kinds, quick actions) all move over. Team avatar tiles drop their borders — tint-only squircles. * fix(tg): fleet avatar strip breathes — spaced tiles instead of overlap * polish(tg): taste-skill audit pass — em-dash purge, one icon family, separator rationing Applied the design-taste audit against the cockpit: every em-dash in visible UI copy rewritten (periods/commas/colons), the remaining lucide chrome (carets, arrows, send, close, spinners) moved to Phosphor so the tg tree ships one icon family (send is the native paper-plane, carets bold), the hand-rolled chevron SVG deleted, and metadata lines rationed to a single middle-dot separator. * polish(tg): pipeline chip strip scrolls without a visible scrollbar * fix(tests): metrics observability fixture uses a relative timestamp The hardcoded _T0 (2026-06-20) aged out of the service's 30-day window exactly 30 days later, detonating the suite on every branch. Two days back from now() stays inside every window (30d metrics, 7d scorecards) permanently. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
187 lines
6.3 KiB
TypeScript
187 lines
6.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
|
|
const { waitForTelegramWebApp } = vi.hoisted(() => ({
|
|
waitForTelegramWebApp: vi.fn(),
|
|
}));
|
|
// Keep the real dev-mock helpers (createDevMockWebApp / isDevMockWebApp) —
|
|
// only the bridge resolver is faked.
|
|
vi.mock("@/lib/telegram/webapp", async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
waitForTelegramWebApp,
|
|
}));
|
|
|
|
const { startTelegramThemeSync } = vi.hoisted(() => ({
|
|
startTelegramThemeSync: vi.fn(() => () => undefined),
|
|
}));
|
|
vi.mock("@/lib/telegram/theme", () => ({ startTelegramThemeSync }));
|
|
|
|
const { post } = vi.hoisted(() => ({ post: vi.fn() }));
|
|
vi.mock("@/lib/api/client", () => ({
|
|
default: { post },
|
|
getErrorMessage: (err: unknown) =>
|
|
(err as { message?: string } | undefined)?.message ?? "Unknown error",
|
|
}));
|
|
|
|
// The cockpit tabs each fetch their own data (queue cards, tasks,
|
|
// notifications, A2A) — stubbed out here since this test only exercises the
|
|
// bootstrap state machine, not tab content (each tab gets its own coverage).
|
|
vi.mock("@/components/tg/tg-tab-bar", () => ({
|
|
TgTabBar: () => <div data-testid="tg-tab-bar" />,
|
|
}));
|
|
vi.mock("@/components/tg/tg-today-tab", () => ({
|
|
TgTodayTab: () => <div data-testid="tg-today-tab" />,
|
|
}));
|
|
vi.mock("@/components/tg/tg-approvals-tab", () => ({
|
|
TgApprovalsTab: () => <div data-testid="tg-approvals-tab" />,
|
|
}));
|
|
vi.mock("@/components/tg/tg-inbox-tab", () => ({
|
|
TgInboxTab: () => <div data-testid="tg-inbox-tab" />,
|
|
}));
|
|
vi.mock("@/components/tg/tg-board-tab", () => ({
|
|
TgBoardTab: () => <div data-testid="tg-board-tab" />,
|
|
}));
|
|
vi.mock("@/components/tg/tg-chat-tab", () => ({
|
|
TgChatTab: () => <div data-testid="tg-chat-tab" />,
|
|
}));
|
|
vi.mock("@/components/tg/tg-metrics-tab", () => ({
|
|
TgMetricsTab: () => <div data-testid="tg-metrics-tab" />,
|
|
}));
|
|
// The shell's bell badge count — stubbed so the bootstrap test needs no
|
|
// QueryClientProvider.
|
|
vi.mock("@/hooks/use-notifications", () => ({
|
|
useNotifications: () => ({ data: undefined }),
|
|
}));
|
|
|
|
import TelegramMiniAppPage from "../page";
|
|
|
|
function mockWebApp(initData = "abc123") {
|
|
return {
|
|
ready: vi.fn(),
|
|
expand: vi.fn(),
|
|
disableVerticalSwipes: vi.fn(),
|
|
initData,
|
|
};
|
|
}
|
|
|
|
describe("TelegramMiniAppPage — auth bootstrap", () => {
|
|
beforeEach(() => {
|
|
waitForTelegramWebApp.mockReset();
|
|
startTelegramThemeSync.mockClear();
|
|
post.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("shows a spinner while validating", () => {
|
|
waitForTelegramWebApp.mockReturnValue(new Promise(() => {}));
|
|
render(<TelegramMiniAppPage />);
|
|
expect(screen.getByText(/connecting/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the not-inside-Telegram screen when no WebApp object exists", async () => {
|
|
waitForTelegramWebApp.mockResolvedValue(null);
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/open from telegram/i)).toBeInTheDocument(),
|
|
);
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows the Open-from-Telegram wall in production when the CDN bridge has empty initData", async () => {
|
|
// A plain browser at /tg: the telegram.org script defines WebApp but
|
|
// with no initData. Production must not post the empty payload (422) —
|
|
// it shows the wall, same as no bridge at all.
|
|
waitForTelegramWebApp.mockResolvedValue(mockWebApp(""));
|
|
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/open from telegram/i)).toBeInTheDocument(),
|
|
);
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls ready/expand, posts initData, and renders the cockpit on success", async () => {
|
|
const webApp = mockWebApp("real-init-data");
|
|
waitForTelegramWebApp.mockResolvedValue(webApp);
|
|
post.mockResolvedValue({ data: { ok: true } });
|
|
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByTestId("tg-tab-bar")).toBeInTheDocument(),
|
|
);
|
|
expect(webApp.ready).toHaveBeenCalledTimes(1);
|
|
expect(webApp.expand).toHaveBeenCalledTimes(1);
|
|
expect(webApp.disableVerticalSwipes).toHaveBeenCalledTimes(1);
|
|
expect(post).toHaveBeenCalledWith("/telegram/webapp-auth", {
|
|
init_data: "real-init-data",
|
|
});
|
|
// Default tab is Today.
|
|
expect(screen.getByTestId("tg-today-tab")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders an error screen with the server's message when auth is refused", async () => {
|
|
waitForTelegramWebApp.mockResolvedValue(mockWebApp());
|
|
post.mockRejectedValue({ message: "Mini App disabled" });
|
|
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/couldn.t sign in/i)).toBeInTheDocument(),
|
|
);
|
|
expect(screen.getByText("Mini App disabled")).toBeInTheDocument();
|
|
expect(screen.queryByTestId("tg-tab-bar")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("falls back to the dev mock outside Telegram in development — no auth POST", async () => {
|
|
vi.stubEnv("NODE_ENV", "development");
|
|
waitForTelegramWebApp.mockResolvedValue(null);
|
|
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByTestId("tg-tab-bar")).toBeInTheDocument(),
|
|
);
|
|
expect(post).not.toHaveBeenCalled();
|
|
expect(screen.queryByText(/open from telegram/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("dev mock also engages when the CDN bridge loaded with empty initData", async () => {
|
|
// A bare browser tab still loads telegram-web-app.js, so the bridge
|
|
// object exists — only a real Telegram launch carries initData.
|
|
vi.stubEnv("NODE_ENV", "development");
|
|
waitForTelegramWebApp.mockResolvedValue(mockWebApp(""));
|
|
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByTestId("tg-tab-bar")).toBeInTheDocument(),
|
|
);
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("starts Telegram theme sync against the #tg-shell element once ready", async () => {
|
|
const shell = document.createElement("div");
|
|
shell.id = "tg-shell";
|
|
document.body.appendChild(shell);
|
|
try {
|
|
const webApp = mockWebApp();
|
|
waitForTelegramWebApp.mockResolvedValue(webApp);
|
|
post.mockResolvedValue({ data: { ok: true } });
|
|
|
|
render(<TelegramMiniAppPage />);
|
|
|
|
await waitFor(() =>
|
|
expect(startTelegramThemeSync).toHaveBeenCalledWith(webApp, shell),
|
|
);
|
|
} finally {
|
|
shell.remove();
|
|
}
|
|
});
|
|
});
|