feat: Telegram V3 — Mini App cockpit (initData auth + /tg surface) (#554)

* feat(telegram): Mini App auth — initData validation mints the cloud-auth session cookie

* feat(panel): /tg Mini App cockpit — approvals, inbox, read-only board, A2A chat

* fix(telegram,panel): unconditional webapp-auth rate limit, future-dated initData rejection, anchored /tg matcher

* docs(map,rag): Telegram Mini App auth route, initData validator, (tg) surface

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-18 02:47:59 +02:00
committed by GitHub
co-authored by Renn F
parent 3b88c706dd
commit c40a7a39c3
33 changed files with 1725 additions and 27 deletions
@@ -0,0 +1,94 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
const { waitForTelegramWebApp } = vi.hoisted(() => ({
waitForTelegramWebApp: vi.fn(),
}));
vi.mock("@/lib/telegram/webapp", () => ({ waitForTelegramWebApp }));
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-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" />,
}));
import TelegramMiniAppPage from "../page";
function mockWebApp(initData = "abc123") {
return { ready: vi.fn(), expand: vi.fn(), initData };
}
describe("TelegramMiniAppPage — auth bootstrap", () => {
beforeEach(() => {
waitForTelegramWebApp.mockReset();
post.mockReset();
});
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("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(post).toHaveBeenCalledWith("/telegram/webapp-auth", {
init_data: "real-init-data",
});
// Default tab is Approvals.
expect(screen.getByTestId("tg-approvals-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();
});
});