mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [05d580eb] test(panel): add baseline vitest tests for 5 source units and widen coverage include (#235) - panel/src/lib/__tests__/agent-definitions.test.ts: all 7 filter functions covered (getBoardAgents, getMainPm, getBackend/Frontend/Ux/Marketing/OnDemandAgents) including null/undefined input and CEO/MAIN_PM exclusion logic - panel/src/lib/__tests__/client.test.ts: getErrorMessage fully covered (ECONNABORTED, ERR_NETWORK, string/array/object detail formats, HTTP 401/403/404/422/500+, plain Error fallback, unknown input fallback) - panel/src/store/__tests__/notifications-store.test.ts: useNotificationStore (addNotification counter+dedup, markAsRead, markAsAcknowledged, setCounts, clearAll) - panel/src/store/__tests__/rate-limit-store.test.ts: useRateLimitStore (hitRateLimit entry+resumeAt, liftRateLimit deletion, syncFromApi replacement) - panel/src/lib/__tests__/websocket.test.ts: getWebSocketUrl (absolute ws://, absolute wss://, http→ws: relative, https→wss: relative, SSR fallback) - panel/vitest.config.ts: coverage include widened to src/lib/**, src/store/**, src/components/** and global thresholds removed (baseline tests cover only 5 units of hundreds; thresholds will be re-added per-file as coverage grows) pnpm test: 7 test files, 111 tests, 0 failures pnpm lint: clean pnpm typecheck: clean * [1bc8195e] feat(ci): add panel-gate and panel-quality Makefile targets and CI test step (#236) Add two new .PHONY Makefile targets (panel-gate, panel-quality) that run pnpm lint, pnpm exec tsc --noEmit, and pnpm test inside the panel directory. panel-quality depends on panel-gate so a single target drives the full gate. Add a 'Test (vitest + coverage)' step to the panel job in ci.yml, placed after the existing Type-check step. The job's default working-directory is already panel so no override is needed; vitest.config.ts text reporter prints coverage to stdout automatically. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@agents.roboco.dev> Co-authored-by: Frontend Developer 2 <fe-dev-2@agents.roboco.dev>
This commit is contained in:
co-authored by
Frontend Developer 1
Frontend Developer 2
parent
028b49161b
commit
bed8342e7b
@@ -139,3 +139,6 @@ jobs:
|
||||
|
||||
- name: Type-check
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
- name: Test (vitest + coverage)
|
||||
run: pnpm test
|
||||
|
||||
@@ -300,6 +300,18 @@ gate:
|
||||
@uv run mypy roboco/ tests/
|
||||
@uv run xenon --max-absolute B --max-modules A --max-average A roboco/
|
||||
|
||||
# Panel (Next.js) fast gate: lint + type-check + vitest.
|
||||
# Run locally before submitting panel changes; mirrors the CI panel job exactly.
|
||||
.PHONY: panel-gate
|
||||
panel-gate:
|
||||
@cd panel && pnpm lint
|
||||
@cd panel && pnpm exec tsc --noEmit
|
||||
@cd panel && pnpm test
|
||||
|
||||
# Full CI-equivalent panel gate (alias for panel-gate).
|
||||
.PHONY: panel-quality
|
||||
panel-quality: panel-gate
|
||||
|
||||
# Run all analysis tools
|
||||
.PHONY: analysis
|
||||
analysis: deptry
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getBoardAgents,
|
||||
getMainPm,
|
||||
getBackendAgents,
|
||||
getFrontendAgents,
|
||||
getUxAgents,
|
||||
getMarketingAgents,
|
||||
getOnDemandAgents,
|
||||
type AgentDefinition,
|
||||
} from "@/lib/agent-definitions";
|
||||
import { AgentRole, Team } from "@/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const makeAgent = (
|
||||
id: string,
|
||||
role: AgentRole | null,
|
||||
team: Team | null
|
||||
): AgentDefinition => ({ id, name: id, role, team });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ceo = makeAgent("ceo-1", AgentRole.CEO, Team.BOARD);
|
||||
const mainPm = makeAgent("main-pm-1", AgentRole.MAIN_PM, Team.MAIN_PM);
|
||||
const auditor = makeAgent("auditor-1", AgentRole.AUDITOR, Team.BOARD);
|
||||
const headMarketing = makeAgent("hm-1", AgentRole.HEAD_MARKETING, null);
|
||||
const productOwner = makeAgent("po-1", AgentRole.PRODUCT_OWNER, Team.BOARD);
|
||||
const prReviewer = makeAgent("prr-1", AgentRole.PR_REVIEWER, null);
|
||||
const beCellPm = makeAgent("be-pm", AgentRole.CELL_PM, Team.BACKEND);
|
||||
const beDev1 = makeAgent("be-dev-1", AgentRole.DEVELOPER, Team.BACKEND);
|
||||
const feDev1 = makeAgent("fe-dev-1", AgentRole.DEVELOPER, Team.FRONTEND);
|
||||
const uxDev = makeAgent("ux-1", AgentRole.QA, Team.UX_UI);
|
||||
const mktDev = makeAgent("mkt-1", AgentRole.DOCUMENTER, Team.MARKETING);
|
||||
const prompter = makeAgent("prompter-1", AgentRole.PROMPTER, null);
|
||||
const secretary = makeAgent("secretary-1", AgentRole.SECRETARY, null);
|
||||
|
||||
const ALL_AGENTS: AgentDefinition[] = [
|
||||
ceo,
|
||||
mainPm,
|
||||
auditor,
|
||||
headMarketing,
|
||||
productOwner,
|
||||
prReviewer,
|
||||
beCellPm,
|
||||
beDev1,
|
||||
feDev1,
|
||||
uxDev,
|
||||
mktDev,
|
||||
prompter,
|
||||
secretary,
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getBoardAgents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getBoardAgents", () => {
|
||||
it("includes agents on BOARD team (excluding CEO and MAIN_PM)", () => {
|
||||
const result = getBoardAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(auditor);
|
||||
expect(result).toContainEqual(productOwner);
|
||||
});
|
||||
|
||||
it("excludes CEO even though CEO has team=board", () => {
|
||||
const result = getBoardAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(ceo);
|
||||
});
|
||||
|
||||
it("excludes MAIN_PM (MAIN_PM has its own dedicated section)", () => {
|
||||
const result = getBoardAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(mainPm);
|
||||
});
|
||||
|
||||
it("includes HEAD_MARKETING role regardless of team", () => {
|
||||
const result = getBoardAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(headMarketing);
|
||||
});
|
||||
|
||||
it("includes PR_REVIEWER role regardless of team", () => {
|
||||
const result = getBoardAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(prReviewer);
|
||||
});
|
||||
|
||||
it("excludes cell agents (BACKEND, FRONTEND, etc.)", () => {
|
||||
const result = getBoardAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(beDev1);
|
||||
expect(result).not.toContainEqual(feDev1);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getBoardAgents(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getBoardAgents(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for an empty agent list", () => {
|
||||
expect(getBoardAgents([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getMainPm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getMainPm", () => {
|
||||
it("returns agents with MAIN_PM role", () => {
|
||||
const result = getMainPm(ALL_AGENTS);
|
||||
expect(result).toContainEqual(mainPm);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("excludes agents without MAIN_PM role", () => {
|
||||
const result = getMainPm(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(ceo);
|
||||
expect(result).not.toContainEqual(beDev1);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getMainPm(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getMainPm(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getBackendAgents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getBackendAgents", () => {
|
||||
it("returns all agents on the BACKEND team", () => {
|
||||
const result = getBackendAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(beDev1);
|
||||
expect(result).toContainEqual(beCellPm);
|
||||
});
|
||||
|
||||
it("excludes agents from other teams", () => {
|
||||
const result = getBackendAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(feDev1);
|
||||
expect(result).not.toContainEqual(uxDev);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getBackendAgents(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getBackendAgents(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getFrontendAgents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getFrontendAgents", () => {
|
||||
it("returns all agents on the FRONTEND team", () => {
|
||||
const result = getFrontendAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(feDev1);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("excludes agents from other teams", () => {
|
||||
const result = getFrontendAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(beDev1);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getFrontendAgents(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getFrontendAgents(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getUxAgents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getUxAgents", () => {
|
||||
it("returns all agents on the UX_UI team", () => {
|
||||
const result = getUxAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(uxDev);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("excludes agents from other teams", () => {
|
||||
const result = getUxAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(beDev1);
|
||||
expect(result).not.toContainEqual(feDev1);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getUxAgents(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getUxAgents(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getMarketingAgents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getMarketingAgents", () => {
|
||||
it("returns all agents on the MARKETING team", () => {
|
||||
const result = getMarketingAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(mktDev);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("excludes agents from other teams", () => {
|
||||
const result = getMarketingAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(beDev1);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getMarketingAgents(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getMarketingAgents(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getOnDemandAgents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getOnDemandAgents", () => {
|
||||
it("returns PROMPTER agents", () => {
|
||||
const result = getOnDemandAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(prompter);
|
||||
});
|
||||
|
||||
it("returns SECRETARY agents", () => {
|
||||
const result = getOnDemandAgents(ALL_AGENTS);
|
||||
expect(result).toContainEqual(secretary);
|
||||
});
|
||||
|
||||
it("excludes agents that are neither PROMPTER nor SECRETARY", () => {
|
||||
const result = getOnDemandAgents(ALL_AGENTS);
|
||||
expect(result).not.toContainEqual(beDev1);
|
||||
expect(result).not.toContainEqual(ceo);
|
||||
expect(result).not.toContainEqual(mainPm);
|
||||
});
|
||||
|
||||
it("returns both on-demand roles and no others", () => {
|
||||
const result = getOnDemandAgents(ALL_AGENTS);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((a) => a.role)).toEqual(
|
||||
expect.arrayContaining([AgentRole.PROMPTER, AgentRole.SECRETARY])
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an empty array for null input", () => {
|
||||
expect(getOnDemandAgents(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty array for undefined input", () => {
|
||||
expect(getOnDemandAgents(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Tests for getErrorMessage() from @/lib/api/client.
|
||||
*
|
||||
* Strategy: We construct minimal objects that satisfy the AxiosError contract.
|
||||
* The real `axios.isAxiosError` implementation checks:
|
||||
* isObject(payload) && payload.isAxiosError === true
|
||||
* so plain objects with { isAxiosError: true } are recognised correctly,
|
||||
* and we need not fully mock the axios module.
|
||||
*
|
||||
* We still mock the modules that client.ts imports for side effects so the
|
||||
* axios instance creation and interceptor registration don't produce
|
||||
* unwanted network calls or store mutations during the test run.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock heavy side-effect imports consumed by client.ts at module level
|
||||
// ---------------------------------------------------------------------------
|
||||
vi.mock("sonner", () => ({
|
||||
toast: { warning: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("@/store/rate-limit-store", () => ({
|
||||
useRateLimitStore: {
|
||||
getState: vi.fn(() => ({ hitRateLimit: vi.fn() })),
|
||||
},
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import the function under test AFTER mocks are in place
|
||||
// ---------------------------------------------------------------------------
|
||||
import { getErrorMessage } from "@/lib/api/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a minimal AxiosError-like object accepted by `axios.isAxiosError`.
|
||||
*
|
||||
* The real axios checks: isObject(payload) && payload.isAxiosError === true
|
||||
*/
|
||||
function makeAxiosError(opts: {
|
||||
code?: string;
|
||||
status?: number;
|
||||
detail?: unknown;
|
||||
message?: string;
|
||||
}) {
|
||||
return {
|
||||
isAxiosError: true as const,
|
||||
code: opts.code,
|
||||
message: opts.message ?? "axios error",
|
||||
response: opts.status !== undefined
|
||||
? {
|
||||
status: opts.status,
|
||||
data: { detail: opts.detail } as { detail?: unknown },
|
||||
headers: {} as Record<string, string>,
|
||||
config: {} as never,
|
||||
statusText: "",
|
||||
}
|
||||
: undefined,
|
||||
config: {} as never,
|
||||
name: "AxiosError" as const,
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error-code tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getErrorMessage — error codes", () => {
|
||||
it("returns timeout message for ECONNABORTED", () => {
|
||||
const err = makeAxiosError({ code: "ECONNABORTED" });
|
||||
expect(getErrorMessage(err)).toBe(
|
||||
"Request timed out. The server may be busy."
|
||||
);
|
||||
});
|
||||
|
||||
it("returns network message for ERR_NETWORK", () => {
|
||||
const err = makeAxiosError({ code: "ERR_NETWORK" });
|
||||
expect(getErrorMessage(err)).toBe(
|
||||
"Cannot connect to server. Check if the backend is running."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// `detail` payload format tests (formatErrorDetail branches)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getErrorMessage — detail string format", () => {
|
||||
it("returns the string detail directly", () => {
|
||||
const err = makeAxiosError({ status: 400, detail: "Some error string" });
|
||||
expect(getErrorMessage(err)).toBe("Some error string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getErrorMessage — detail array format", () => {
|
||||
it("formats a FastAPI validation array into a readable string", () => {
|
||||
const err = makeAxiosError({
|
||||
status: 422,
|
||||
detail: [
|
||||
{ loc: ["body", "name"], msg: "field required" },
|
||||
{ loc: ["body", "email"], msg: "invalid email" },
|
||||
],
|
||||
});
|
||||
const result = getErrorMessage(err);
|
||||
// Both validation errors should appear
|
||||
expect(result).toContain("name");
|
||||
expect(result).toContain("field required");
|
||||
expect(result).toContain("email");
|
||||
expect(result).toContain("invalid email");
|
||||
});
|
||||
|
||||
it("returns a plain string item inside the array as-is", () => {
|
||||
const err = makeAxiosError({
|
||||
status: 400,
|
||||
detail: ["plain error"],
|
||||
});
|
||||
expect(getErrorMessage(err)).toBe("plain error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getErrorMessage — detail object format", () => {
|
||||
it("extracts `message` field from a structured error object", () => {
|
||||
const err = makeAxiosError({
|
||||
status: 400,
|
||||
detail: { message: "Structured message" },
|
||||
});
|
||||
expect(getErrorMessage(err)).toBe("Structured message");
|
||||
});
|
||||
|
||||
it("extracts `error` field when `message` is absent", () => {
|
||||
const err = makeAxiosError({
|
||||
status: 400,
|
||||
detail: { error: "Error field content" },
|
||||
});
|
||||
expect(getErrorMessage(err)).toBe("Error field content");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP status code tests (no usable detail)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getErrorMessage — HTTP status codes", () => {
|
||||
it("returns auth message for 401", () => {
|
||||
const err = makeAxiosError({ status: 401 });
|
||||
expect(getErrorMessage(err)).toBe(
|
||||
"Authentication required. Please refresh the page."
|
||||
);
|
||||
});
|
||||
|
||||
it("returns permission message for 403", () => {
|
||||
const err = makeAxiosError({ status: 403 });
|
||||
expect(getErrorMessage(err)).toBe("Permission denied for this action.");
|
||||
});
|
||||
|
||||
it("returns not-found message for 404", () => {
|
||||
const err = makeAxiosError({ status: 404 });
|
||||
expect(getErrorMessage(err)).toBe(
|
||||
"The requested resource was not found."
|
||||
);
|
||||
});
|
||||
|
||||
it("returns validation message for 422 without detail", () => {
|
||||
const err = makeAxiosError({ status: 422 });
|
||||
expect(getErrorMessage(err)).toBe("Invalid request data.");
|
||||
});
|
||||
|
||||
it("returns server-error message for 500", () => {
|
||||
const err = makeAxiosError({ status: 500 });
|
||||
expect(getErrorMessage(err)).toBe("Server error. Please try again later.");
|
||||
});
|
||||
|
||||
it("returns server-error message for 502 (>= 500)", () => {
|
||||
const err = makeAxiosError({ status: 502 });
|
||||
expect(getErrorMessage(err)).toBe("Server error. Please try again later.");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fallback tests (non-Axios errors)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getErrorMessage — non-Axios fallbacks", () => {
|
||||
it("returns Error.message for a plain Error instance", () => {
|
||||
const err = new Error("plain JS error");
|
||||
expect(getErrorMessage(err)).toBe("plain JS error");
|
||||
});
|
||||
|
||||
it("returns generic message for a string input", () => {
|
||||
expect(getErrorMessage("some string")).toBe("An unexpected error occurred");
|
||||
});
|
||||
|
||||
it("returns generic message for null input", () => {
|
||||
expect(getErrorMessage(null)).toBe("An unexpected error occurred");
|
||||
});
|
||||
|
||||
it("returns generic message for undefined input", () => {
|
||||
expect(getErrorMessage(undefined)).toBe("An unexpected error occurred");
|
||||
});
|
||||
|
||||
it("returns generic message for a plain object without isAxiosError", () => {
|
||||
expect(getErrorMessage({ code: "SOME_CODE" })).toBe(
|
||||
"An unexpected error occurred"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Tests for getWebSocketUrl() from @/lib/websocket/connection.
|
||||
*
|
||||
* Because `WS_URL` is a named import captured at module-load time, we use
|
||||
* vi.resetModules() + vi.doMock() + dynamic import() in each test so that
|
||||
* every test gets a fresh module binding with the desired constant value.
|
||||
*
|
||||
* window.location is stubbed via vi.stubGlobal() and cleaned up in
|
||||
* afterEach with vi.unstubAllGlobals().
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared constants mock factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function mockConstants(wsUrl: string) {
|
||||
vi.doMock("@/lib/constants", () => ({
|
||||
WS_URL: wsUrl,
|
||||
API_URL: "/api",
|
||||
CEO_AGENT_ID: "00000000-0000-0000-0000-000000000001",
|
||||
CEO_ROLE: "ceo",
|
||||
DEFAULT_PAGE_SIZE: 20,
|
||||
MAX_PAGE_SIZE: 100,
|
||||
WS_RECONNECT_INTERVAL: 5000,
|
||||
WS_MAX_RECONNECT_ATTEMPTS: 3,
|
||||
WS_HEARTBEAT_INTERVAL: 30000,
|
||||
STREAM_MAX_MESSAGES: 100,
|
||||
NOTIFICATION_MAX_DISPLAY: 10,
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Absolute URL passthrough
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getWebSocketUrl — absolute ws:// passthrough", () => {
|
||||
it("returns the absolute ws:// URL unchanged", async () => {
|
||||
const absoluteUrl = "ws://direct.example.com/ws";
|
||||
mockConstants(absoluteUrl);
|
||||
const { getWebSocketUrl } = await import("@/lib/websocket/connection");
|
||||
expect(getWebSocketUrl()).toBe(absoluteUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWebSocketUrl — absolute wss:// passthrough", () => {
|
||||
it("returns the absolute wss:// URL unchanged", async () => {
|
||||
const absoluteUrl = "wss://secure.example.com/ws";
|
||||
mockConstants(absoluteUrl);
|
||||
const { getWebSocketUrl } = await import("@/lib/websocket/connection");
|
||||
expect(getWebSocketUrl()).toBe(absoluteUrl);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Relative-path construction via window.location
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getWebSocketUrl — relative-path http → ws:", () => {
|
||||
it("maps http: protocol to ws: and appends the relative WS_URL", async () => {
|
||||
mockConstants("/ws");
|
||||
vi.stubGlobal("location", {
|
||||
protocol: "http:",
|
||||
host: "myhost.local:3000",
|
||||
});
|
||||
const { getWebSocketUrl } = await import("@/lib/websocket/connection");
|
||||
expect(getWebSocketUrl()).toBe("ws://myhost.local:3000/ws");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWebSocketUrl — relative-path https → wss:", () => {
|
||||
it("maps https: protocol to wss: and appends the relative WS_URL", async () => {
|
||||
mockConstants("/ws");
|
||||
vi.stubGlobal("location", {
|
||||
protocol: "https:",
|
||||
host: "secure.example.com",
|
||||
});
|
||||
const { getWebSocketUrl } = await import("@/lib/websocket/connection");
|
||||
expect(getWebSocketUrl()).toBe("wss://secure.example.com/ws");
|
||||
});
|
||||
|
||||
it("prepends a leading slash when WS_URL does not start with /", async () => {
|
||||
mockConstants("ws");
|
||||
vi.stubGlobal("location", {
|
||||
protocol: "https:",
|
||||
host: "secure.example.com",
|
||||
});
|
||||
const { getWebSocketUrl } = await import("@/lib/websocket/connection");
|
||||
// The function does: `/${WS_URL}` when WS_URL doesn't start with /
|
||||
expect(getWebSocketUrl()).toBe("wss://secure.example.com/ws");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SSR fallback (window undefined)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getWebSocketUrl — SSR fallback", () => {
|
||||
it("returns the raw WS_URL when window is not available", async () => {
|
||||
mockConstants("/ws");
|
||||
// Remove window to simulate server-side rendering
|
||||
vi.stubGlobal("window", undefined);
|
||||
const { getWebSocketUrl } = await import("@/lib/websocket/connection");
|
||||
expect(getWebSocketUrl()).toBe("/ws");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useNotificationStore } from "@/store/notifications-store";
|
||||
import { NotificationType, NotificationPriority } from "@/types";
|
||||
import type { Notification } from "@/types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let _idCounter = 0;
|
||||
|
||||
function makeNotification(
|
||||
overrides: Partial<Notification> = {}
|
||||
): Notification {
|
||||
_idCounter += 1;
|
||||
return {
|
||||
id: `notif-${_idCounter}`,
|
||||
type: NotificationType.ALERT,
|
||||
priority: NotificationPriority.NORMAL,
|
||||
from_agent: "be-dev-1",
|
||||
to_agents: ["ceo"],
|
||||
subject: "Test notification",
|
||||
body: "Test body",
|
||||
requires_ack: false,
|
||||
is_acknowledged: false,
|
||||
is_fully_acknowledged: false,
|
||||
is_read: false,
|
||||
related_task_id: null,
|
||||
related_message_ids: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
expires_at: null,
|
||||
acked_by: [],
|
||||
acked_at: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reset store before every test to avoid state leakage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
_idCounter = 0;
|
||||
useNotificationStore.setState({
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
pendingAckCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// addNotification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useNotificationStore — addNotification", () => {
|
||||
it("adds the notification to the store", () => {
|
||||
const n = makeNotification();
|
||||
useNotificationStore.getState().addNotification(n);
|
||||
expect(useNotificationStore.getState().notifications).toContainEqual(n);
|
||||
});
|
||||
|
||||
it("increments unreadCount when the notification is unread", () => {
|
||||
useNotificationStore.getState().addNotification(makeNotification({ is_read: false }));
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(1);
|
||||
});
|
||||
|
||||
it("does NOT increment unreadCount when the notification is already read", () => {
|
||||
useNotificationStore.getState().addNotification(makeNotification({ is_read: true }));
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(0);
|
||||
});
|
||||
|
||||
it("increments pendingAckCount when requires_ack and not yet acknowledged", () => {
|
||||
useNotificationStore
|
||||
.getState()
|
||||
.addNotification(
|
||||
makeNotification({ requires_ack: true, is_acknowledged: false })
|
||||
);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(1);
|
||||
});
|
||||
|
||||
it("does NOT increment pendingAckCount when requires_ack is false", () => {
|
||||
useNotificationStore
|
||||
.getState()
|
||||
.addNotification(makeNotification({ requires_ack: false }));
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(0);
|
||||
});
|
||||
|
||||
it("does NOT increment pendingAckCount when notification is already acknowledged", () => {
|
||||
useNotificationStore
|
||||
.getState()
|
||||
.addNotification(
|
||||
makeNotification({ requires_ack: true, is_acknowledged: true })
|
||||
);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(0);
|
||||
});
|
||||
|
||||
it("deduplicates: re-delivering the same id updates in-place without incrementing counters", () => {
|
||||
const n = makeNotification({ is_read: false });
|
||||
const store = useNotificationStore.getState();
|
||||
|
||||
// First delivery
|
||||
store.addNotification(n);
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(1);
|
||||
expect(useNotificationStore.getState().notifications).toHaveLength(1);
|
||||
|
||||
// Second delivery of the SAME id (with updated fields)
|
||||
const updated = { ...n, subject: "Updated subject" };
|
||||
useNotificationStore.getState().addNotification(updated);
|
||||
|
||||
// Counter must NOT have been double-counted
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(1);
|
||||
// Still only one entry in the list
|
||||
expect(useNotificationStore.getState().notifications).toHaveLength(1);
|
||||
// The entry was updated in place
|
||||
expect(useNotificationStore.getState().notifications[0].subject).toBe(
|
||||
"Updated subject"
|
||||
);
|
||||
});
|
||||
|
||||
it("prepends new notifications (newest first)", () => {
|
||||
const first = makeNotification();
|
||||
const second = makeNotification();
|
||||
useNotificationStore.getState().addNotification(first);
|
||||
useNotificationStore.getState().addNotification(second);
|
||||
expect(useNotificationStore.getState().notifications[0].id).toBe(second.id);
|
||||
expect(useNotificationStore.getState().notifications[1].id).toBe(first.id);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// markAsRead
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useNotificationStore — markAsRead", () => {
|
||||
it("marks the target notification as read", () => {
|
||||
const n = makeNotification({ is_read: false });
|
||||
useNotificationStore.getState().addNotification(n);
|
||||
useNotificationStore.getState().markAsRead(n.id);
|
||||
const updated = useNotificationStore
|
||||
.getState()
|
||||
.notifications.find((x) => x.id === n.id);
|
||||
expect(updated?.is_read).toBe(true);
|
||||
});
|
||||
|
||||
it("decrements unreadCount when marking a notification read", () => {
|
||||
const n = makeNotification({ is_read: false });
|
||||
useNotificationStore.getState().addNotification(n);
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(1);
|
||||
useNotificationStore.getState().markAsRead(n.id);
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(0);
|
||||
});
|
||||
|
||||
it("unreadCount never drops below 0", () => {
|
||||
// markAsRead on an empty store shouldn't produce negative count
|
||||
useNotificationStore.setState({ unreadCount: 0 });
|
||||
useNotificationStore.getState().markAsRead("non-existent-id");
|
||||
expect(useNotificationStore.getState().unreadCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("does not affect other notifications", () => {
|
||||
const n1 = makeNotification({ is_read: false });
|
||||
const n2 = makeNotification({ is_read: false });
|
||||
useNotificationStore.getState().addNotification(n1);
|
||||
useNotificationStore.getState().addNotification(n2);
|
||||
useNotificationStore.getState().markAsRead(n1.id);
|
||||
const n2Updated = useNotificationStore
|
||||
.getState()
|
||||
.notifications.find((x) => x.id === n2.id);
|
||||
expect(n2Updated?.is_read).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// markAsAcknowledged
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useNotificationStore — markAsAcknowledged", () => {
|
||||
it("marks the target notification as acknowledged", () => {
|
||||
const n = makeNotification({
|
||||
requires_ack: true,
|
||||
is_acknowledged: false,
|
||||
});
|
||||
useNotificationStore.getState().addNotification(n);
|
||||
useNotificationStore.getState().markAsAcknowledged(n.id);
|
||||
const updated = useNotificationStore
|
||||
.getState()
|
||||
.notifications.find((x) => x.id === n.id);
|
||||
expect(updated?.is_acknowledged).toBe(true);
|
||||
});
|
||||
|
||||
it("decrements pendingAckCount when acknowledging a notification", () => {
|
||||
const n = makeNotification({
|
||||
requires_ack: true,
|
||||
is_acknowledged: false,
|
||||
});
|
||||
useNotificationStore.getState().addNotification(n);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(1);
|
||||
useNotificationStore.getState().markAsAcknowledged(n.id);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(0);
|
||||
});
|
||||
|
||||
it("pendingAckCount never drops below 0", () => {
|
||||
useNotificationStore.setState({ pendingAckCount: 0 });
|
||||
useNotificationStore.getState().markAsAcknowledged("non-existent-id");
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setCounts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useNotificationStore — setCounts", () => {
|
||||
it("sets unreadCount and pendingAckCount directly", () => {
|
||||
useNotificationStore.getState().setCounts(7, 3);
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(7);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(3);
|
||||
});
|
||||
|
||||
it("overrides previous counter values", () => {
|
||||
useNotificationStore.setState({ unreadCount: 10, pendingAckCount: 5 });
|
||||
useNotificationStore.getState().setCounts(2, 1);
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(2);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(1);
|
||||
});
|
||||
|
||||
it("accepts zero for both counters", () => {
|
||||
useNotificationStore.setState({ unreadCount: 4, pendingAckCount: 2 });
|
||||
useNotificationStore.getState().setCounts(0, 0);
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(0);
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// clearAll
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useNotificationStore — clearAll", () => {
|
||||
it("empties the notifications array", () => {
|
||||
useNotificationStore.getState().addNotification(makeNotification());
|
||||
useNotificationStore.getState().addNotification(makeNotification());
|
||||
useNotificationStore.getState().clearAll();
|
||||
expect(useNotificationStore.getState().notifications).toEqual([]);
|
||||
});
|
||||
|
||||
it("resets unreadCount to 0", () => {
|
||||
useNotificationStore.getState().addNotification(makeNotification({ is_read: false }));
|
||||
useNotificationStore.getState().clearAll();
|
||||
expect(useNotificationStore.getState().unreadCount).toBe(0);
|
||||
});
|
||||
|
||||
it("resets pendingAckCount to 0", () => {
|
||||
useNotificationStore
|
||||
.getState()
|
||||
.addNotification(
|
||||
makeNotification({ requires_ack: true, is_acknowledged: false })
|
||||
);
|
||||
useNotificationStore.getState().clearAll();
|
||||
expect(useNotificationStore.getState().pendingAckCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useRateLimitStore } from "@/store/rate-limit-store";
|
||||
import type {
|
||||
RateLimitHitEvent,
|
||||
RateLimitLiftedEvent,
|
||||
RateLimitApiResponse,
|
||||
RateLimitEntry,
|
||||
} from "@/types/rate-limits";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TIMESTAMP = "2026-06-20T12:00:00.000Z";
|
||||
|
||||
function makeHitEvent(
|
||||
provider = "anthropic",
|
||||
retryAfterSeconds = 60,
|
||||
affectedAgents: string[] = []
|
||||
): RateLimitHitEvent {
|
||||
return {
|
||||
type: "RATE_LIMIT_HIT",
|
||||
provider,
|
||||
affectedAgents,
|
||||
retryAfterSeconds,
|
||||
timestamp: TIMESTAMP,
|
||||
};
|
||||
}
|
||||
|
||||
function makeLiftedEvent(provider = "anthropic"): RateLimitLiftedEvent {
|
||||
return {
|
||||
type: "RATE_LIMIT_LIFTED",
|
||||
provider,
|
||||
timestamp: TIMESTAMP,
|
||||
};
|
||||
}
|
||||
|
||||
function makeApiEntry(
|
||||
provider: string,
|
||||
hitAt = TIMESTAMP,
|
||||
retryAfterSeconds = 30
|
||||
): RateLimitEntry {
|
||||
return {
|
||||
provider,
|
||||
affectedAgents: [],
|
||||
hitAt,
|
||||
resumeAt: new Date(
|
||||
new Date(hitAt).getTime() + retryAfterSeconds * 1000
|
||||
).toISOString(),
|
||||
retryAfterSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reset store before every test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
useRateLimitStore.setState({ limits: new Map() });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// hitRateLimit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useRateLimitStore — hitRateLimit", () => {
|
||||
it("creates an entry keyed by provider", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
const limits = useRateLimitStore.getState().limits;
|
||||
expect(limits.has("anthropic")).toBe(true);
|
||||
});
|
||||
|
||||
it("stores the correct provider on the entry", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("openai", 30));
|
||||
const entry = useRateLimitStore.getState().limits.get("openai");
|
||||
expect(entry?.provider).toBe("openai");
|
||||
});
|
||||
|
||||
it("stores affectedAgents on the entry", () => {
|
||||
const agents = ["be-dev-1", "be-dev-2"];
|
||||
useRateLimitStore
|
||||
.getState()
|
||||
.hitRateLimit(makeHitEvent("anthropic", 60, agents));
|
||||
const entry = useRateLimitStore.getState().limits.get("anthropic");
|
||||
expect(entry?.affectedAgents).toEqual(agents);
|
||||
});
|
||||
|
||||
it("sets hitAt equal to the event timestamp", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
const entry = useRateLimitStore.getState().limits.get("anthropic");
|
||||
expect(entry?.hitAt).toBe(TIMESTAMP);
|
||||
});
|
||||
|
||||
it("sets retryAfterSeconds equal to the event value", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 45));
|
||||
const entry = useRateLimitStore.getState().limits.get("anthropic");
|
||||
expect(entry?.retryAfterSeconds).toBe(45);
|
||||
});
|
||||
|
||||
it("computes resumeAt as timestamp + retryAfterSeconds * 1000 ms", () => {
|
||||
const retryAfterSeconds = 60;
|
||||
useRateLimitStore
|
||||
.getState()
|
||||
.hitRateLimit(makeHitEvent("anthropic", retryAfterSeconds));
|
||||
const entry = useRateLimitStore.getState().limits.get("anthropic");
|
||||
|
||||
const expectedResumeAt = new Date(
|
||||
new Date(TIMESTAMP).getTime() + retryAfterSeconds * 1000
|
||||
).toISOString();
|
||||
expect(entry?.resumeAt).toBe(expectedResumeAt);
|
||||
});
|
||||
|
||||
it("allows multiple providers to coexist in the map", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("openai", 30));
|
||||
const limits = useRateLimitStore.getState().limits;
|
||||
expect(limits.size).toBe(2);
|
||||
expect(limits.has("anthropic")).toBe(true);
|
||||
expect(limits.has("openai")).toBe(true);
|
||||
});
|
||||
|
||||
it("overwrites an existing entry for the same provider", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 120));
|
||||
const limits = useRateLimitStore.getState().limits;
|
||||
expect(limits.size).toBe(1);
|
||||
expect(limits.get("anthropic")?.retryAfterSeconds).toBe(120);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// liftRateLimit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useRateLimitStore — liftRateLimit", () => {
|
||||
it("deletes the entry for the specified provider", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
expect(useRateLimitStore.getState().limits.has("anthropic")).toBe(true);
|
||||
|
||||
useRateLimitStore.getState().liftRateLimit(makeLiftedEvent("anthropic"));
|
||||
expect(useRateLimitStore.getState().limits.has("anthropic")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not remove entries for other providers", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("openai", 30));
|
||||
|
||||
useRateLimitStore.getState().liftRateLimit(makeLiftedEvent("anthropic"));
|
||||
expect(useRateLimitStore.getState().limits.has("openai")).toBe(true);
|
||||
expect(useRateLimitStore.getState().limits.size).toBe(1);
|
||||
});
|
||||
|
||||
it("is a no-op when the provider is not in the map", () => {
|
||||
// Should not throw
|
||||
expect(() =>
|
||||
useRateLimitStore
|
||||
.getState()
|
||||
.liftRateLimit(makeLiftedEvent("non-existent"))
|
||||
).not.toThrow();
|
||||
expect(useRateLimitStore.getState().limits.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// syncFromApi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("useRateLimitStore — syncFromApi", () => {
|
||||
it("replaces the entire limits map with response entries", () => {
|
||||
// Pre-populate with one entry
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("old-provider", 60));
|
||||
|
||||
const response: RateLimitApiResponse = {
|
||||
entries: [makeApiEntry("anthropic"), makeApiEntry("openai")],
|
||||
};
|
||||
useRateLimitStore.getState().syncFromApi(response);
|
||||
|
||||
const limits = useRateLimitStore.getState().limits;
|
||||
// Old entry is gone
|
||||
expect(limits.has("old-provider")).toBe(false);
|
||||
// New entries are present
|
||||
expect(limits.has("anthropic")).toBe(true);
|
||||
expect(limits.has("openai")).toBe(true);
|
||||
expect(limits.size).toBe(2);
|
||||
});
|
||||
|
||||
it("correctly keys entries by provider name", () => {
|
||||
const entry = makeApiEntry("anthropic", TIMESTAMP, 45);
|
||||
useRateLimitStore.getState().syncFromApi({ entries: [entry] });
|
||||
const stored = useRateLimitStore.getState().limits.get("anthropic");
|
||||
expect(stored).toEqual(entry);
|
||||
});
|
||||
|
||||
it("clears all limits when given an empty entries array", () => {
|
||||
useRateLimitStore.getState().hitRateLimit(makeHitEvent("anthropic", 60));
|
||||
useRateLimitStore.getState().syncFromApi({ entries: [] });
|
||||
expect(useRateLimitStore.getState().limits.size).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -9,14 +9,10 @@ export default defineConfig({
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
include: [
|
||||
"src/components/business/company-scorecard-card.tsx",
|
||||
"src/lib/**",
|
||||
"src/store/**",
|
||||
"src/components/**",
|
||||
],
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
reporter: ["text", "lcov", "json-summary"],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user