Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions Clients/e2e/global.setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { test as setup, expect } from "@playwright/test";
import { execFileSync } from "child_process";
import dotenv from "dotenv";
import { existsSync, mkdtempSync, readFileSync, unlinkSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { mkdtempSync, readFileSync, unlinkSync } from "fs";
import { tmpdir } from "os";

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -87,7 +88,21 @@ async function createOrganization(page: any): Promise<number> {
return orgId;
}

const E2E_NODE_ENV = process.env.E2E_NODE_ENV || "test";

function seedAdminInOrg(orgId: number): SeedOutput {
const env: NodeJS.ProcessEnv = { ...process.env, NODE_ENV: E2E_NODE_ENV };
if (E2E_NODE_ENV === "test") {
// seedE2EAdmin.ts connects via Servers/database/db.ts. Its config module
// reads process.env at import time, before db.ts's own .env.test override
// runs, so the test DB values must already be in the child env. This
// mirrors the integration-suite convention (tests/integration/globalSetup.js).
const envTestPath = path.resolve(SERVERS_DIR, ".env.test");
if (existsSync(envTestPath)) {
Object.assign(env, dotenv.parse(readFileSync(envTestPath, "utf8")));
}
}

const tmpDir = mkdtempSync(path.join(tmpdir(), "vw-e2e-"));
const credentialsFile = path.join(tmpDir, "e2e-credentials.json");

Expand All @@ -97,7 +112,7 @@ function seedAdminInOrg(orgId: number): SeedOutput {
{
cwd: SERVERS_DIR,
encoding: "utf-8",
env: process.env,
env,
},
);
const lastLine = stdout.trim().split("\n").pop() || "";
Expand Down
14 changes: 11 additions & 3 deletions Clients/src/application/hooks/__tests__/useDashboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderHook, waitFor } from "@testing-library/react";
import { renderHook, waitFor, act } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React from "react";
import { useDashboard } from "../useDashboard";
Expand Down Expand Up @@ -108,9 +108,13 @@ describe("useDashboard", () => {
});

it("should handle fetchDashboard being called before initial data loads", async () => {
// Keep the initial promise pending
// Keep the initial promise pending until resolved manually (no real timers)
let resolveInitial: ((value: unknown) => void) | undefined;
mockGetAllEntities.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({ data: { projects: 1 } }), 100)),
() =>
new Promise((resolve) => {
resolveInitial = resolve;
}),
);

const { result } = renderHook(() => useDashboard(), {
Expand All @@ -121,6 +125,10 @@ describe("useDashboard", () => {
const fetchPromise = result.current.fetchDashboard();
expect(fetchPromise).toBeInstanceOf(Promise);

await act(async () => {
resolveInitial?.({ data: { projects: 1 } });
});

await waitFor(() => {
expect(result.current.loading).toBe(false);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ describe("useDashboardMetrics", () => {
mockGetEntityById.mockResolvedValue({ data: {} });
});

afterEach(() => {
vi.restoreAllMocks();
});

it("should set loading=false after all groups complete", async () => {
const { result } = renderHook(() => useDashboardMetrics());

Expand Down
10 changes: 5 additions & 5 deletions Clients/src/application/hooks/__tests__/useGovernanceOs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe("useCreateScenario", () => {
mockCreateScenario.mockResolvedValue({ data: { id: 3 } });
const { result } = renderHook(() => useCreateScenario(), { wrapper: createWrapper() });
await act(async () => {
result.current.mutateAsync({ name: "New Scenario" });
await result.current.mutateAsync({ name: "New Scenario" });
});
expect(mockCreateScenario).toHaveBeenCalledWith({ body: { name: "New Scenario" } });
});
Expand All @@ -141,7 +141,7 @@ describe("useUpdateScenario", () => {
mockUpdateScenario.mockResolvedValue({ data: { id: 3, name: "Updated" } });
const { result } = renderHook(() => useUpdateScenario(), { wrapper: createWrapper() });
await act(async () => {
result.current.mutateAsync({ id: 3, body: { name: "Updated" } });
await result.current.mutateAsync({ id: 3, body: { name: "Updated" } });
});
expect(mockUpdateScenario).toHaveBeenCalledWith({ id: 3, body: { name: "Updated" } });
});
Expand All @@ -154,7 +154,7 @@ describe("useDeleteScenario", () => {
mockDeleteScenario.mockResolvedValue({ success: true });
const { result } = renderHook(() => useDeleteScenario(), { wrapper: createWrapper() });
await act(async () => {
result.current.mutateAsync(7);
await result.current.mutateAsync(7);
});
expect(mockDeleteScenario).toHaveBeenCalledWith({ id: 7 });
});
Expand Down Expand Up @@ -183,7 +183,7 @@ describe("useRefreshCoverage", () => {
mockRefreshCoverage.mockResolvedValue({ data: { status: "completed" } });
const { result } = renderHook(() => useRefreshCoverage(), { wrapper: createWrapper() });
await act(async () => {
result.current.mutateAsync(1);
await result.current.mutateAsync(1);
});
expect(mockRefreshCoverage).toHaveBeenCalledWith({ projectId: 1 });
});
Expand Down Expand Up @@ -234,7 +234,7 @@ describe("useUpdatePreferences", () => {
mockUpdatePreferences.mockResolvedValue({ data: { id: 1, organization_id: 1 } });
const { result } = renderHook(() => useUpdatePreferences(), { wrapper: createWrapper() });
await act(async () => {
result.current.mutateAsync({ organization_id: 1 });
await result.current.mutateAsync({ organization_id: 1 });
});
expect(mockUpdatePreferences).toHaveBeenCalledWith({ body: { organization_id: 1 } });
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("useLogoFetch", () => {
onerror: (() => void) | null = null;
src = "";
constructor() {
setTimeout(() => this.onload?.(), 0);
queueMicrotask(() => this.onload?.());
}
} as any;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe("useNotifications", () => {
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

Expand Down Expand Up @@ -705,10 +706,12 @@ describe("useNotifications", () => {
expect(global.fetch).toHaveBeenCalledTimes(1);
});

vi.useFakeTimers();
await act(async () => {
result.current.reconnect();
await new Promise((r) => setTimeout(r, 150));
await vi.advanceTimersByTimeAsync(150);
});
vi.useRealTimers();

// disconnect + reconnect via setTimeout(100)
await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("useProfilePhotoFetch", () => {
onerror: (() => void) | null = null;
src = "";
constructor() {
setTimeout(() => this.onload?.(), 0);
queueMicrotask(() => this.onload?.());
}
} as any;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// IMPORTANT: the mock path must be EXACTLY the same string used in the tested file.
vi.mock("../../../domain/models/Common/file/file.model", () => {
Expand All @@ -17,6 +17,11 @@ describe("fileTransform.utils", () => {
vi.clearAllMocks();
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

describe("transformFileData", () => {
it("maps fields correctly using upload_date and full uploader name", () => {
const input = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe("frameworkDataUtils", () => {
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

Expand Down
1 change: 1 addition & 0 deletions Clients/src/application/utils/tests/tableExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe("tableExport", () => {

afterEach(() => {
globalThis.alert = originalAlert;
vi.useRealTimers();
});

const columns = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderWithProviders } from "../../../../test/renderWithProviders";
import { act } from "@testing-library/react";
import { FlyingHearts } from "../index";

describe("FlyingHearts Component", () => {
Expand Down Expand Up @@ -29,7 +30,9 @@ describe("FlyingHearts Component", () => {

expect(onComplete).not.toHaveBeenCalled();

vi.advanceTimersByTime(7000);
act(() => {
vi.advanceTimersByTime(7000);
});

expect(onComplete).toHaveBeenCalledTimes(1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ describe("InfoBox Component", () => {
localStorage.clear();
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

it("renders the message text", () => {
renderWithProviders(<InfoBox message="This is an info message" storageKey="test-info" />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ describe("NotificationBell", () => {
vi.clearAllMocks();
});

afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});

const openPopover = async () => {
renderWithProviders(<NotificationBell />);
await userEvent.click(screen.getByRole("button"));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from "@testing-library/react";
import { screen, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../../test/renderWithProviders";
import SetupModal from "../SetupModal";
Expand Down Expand Up @@ -46,7 +46,9 @@ describe("SetupModal", () => {
const onSkip = vi.fn();
renderWithProviders(<SetupModal onComplete={vi.fn()} onSkip={onSkip} />);
await user.click(screen.getByText("Skip for now"));
vi.advanceTimersByTime(400);
act(() => {
vi.advanceTimersByTime(400);
});
expect(onSkip).toHaveBeenCalledTimes(1);
});

Expand All @@ -60,7 +62,9 @@ describe("SetupModal", () => {
const onComplete = vi.fn();
renderWithProviders(<SetupModal onComplete={onComplete} onSkip={vi.fn()} />);
await user.click(screen.getByText("Add demo data"));
vi.advanceTimersByTime(400);
act(() => {
vi.advanceTimersByTime(400);
});
expect(onComplete).toHaveBeenCalledTimes(1);
expect(reloadSpy).toHaveBeenCalledTimes(1);
});
Expand All @@ -70,7 +74,9 @@ describe("SetupModal", () => {
const onComplete = vi.fn();
renderWithProviders(<SetupModal onComplete={onComplete} onSkip={vi.fn()} />);
await user.click(screen.getByText("Start blank"));
vi.advanceTimersByTime(400);
act(() => {
vi.advanceTimersByTime(400);
});
expect(onComplete).toHaveBeenCalledTimes(1);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("formatDate", () => {
});

it("formats another date using the default preference", () => {
expect(formatDate("2023-03-15T00:00:00Z")).toBe("15-03-2023");
expect(formatDate("2023-03-15")).toBe("15-03-2023");
});

it("throws for empty string", () => {
Expand Down
7 changes: 6 additions & 1 deletion Clients/src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ expect.extend(matchers as unknown as MatchersObject);

// ---- MSW lifecycle ----
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterEach(() => {
server.resetHandlers();
vi.restoreAllMocks();
vi.useRealTimers();
localStorage.clear();
});
afterAll(() => server.close());

// ---- Environment stubs ----
Expand Down
33 changes: 18 additions & 15 deletions Servers/advisor/__tests__/routingRealCatalogue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,36 +55,39 @@ describe("routing / real catalogue smoke", () => {
expect(FULL).toBeGreaterThan(60);
});

it("OBSERVABILITY: log routing metrics for sample queries", async () => {
it("covers every core domain agent across the sample queries", async () => {
const samples = [
"List all my vendors and their SLA status",
"Show me the risk register entries for our top projects",
"What's our gap analysis for EU AI Act Article 10?",
"Any open incidents from this week's data breach?",
"List all foundation models in our model inventory",
"Hello, what can you help me with?",
"Vendor risk assessment with related compliance gap evidence",
];
// eslint-disable-next-line no-console
console.log(`\n Full catalogue size: ${FULL} tools\n ${"─".repeat(95)}`);
const agentsSeen = new Set<string>();
for (const q of samples) {
const r = await selectActiveTools({
message: q,
availableTools,
toolsDefinition,
});
const reduction =
r.metrics.fullCount > 0
? Math.round(((r.metrics.fullCount - r.metrics.activeCount) / r.metrics.fullCount) * 100)
: 0;
// eslint-disable-next-line no-console
console.log(
` Q="${q.slice(0, 55).padEnd(55)}" → ${r.reason.padEnd(22)} agents=[${(r.selectedAgents.join(", ") || "—").padEnd(35)}] ${String(r.metrics.activeCount).padStart(3)}/${r.metrics.fullCount} (-${reduction}%)`,
);
for (const agent of r.selectedAgents) agentsSeen.add(agent);
// Every routed query must yield a well-formed subset
expect(r.metrics.fullCount).toBe(FULL);
expect(r.metrics.activeCount).toBeGreaterThan(0);
expect(r.metrics.activeCount).toBeLessThanOrEqual(FULL);
expect(r.selectedAgents.length).toBeGreaterThan(0);
}
// The core domain agents must all be reachable by the router
for (const agent of [
"vendor-agent",
"risk-agent",
"compliance-agent",
"incident-agent",
"model-agent",
]) {
expect(agentsSeen).toContain(agent);
}
// eslint-disable-next-line no-console
console.log(` ${"─".repeat(95)}\n`);
expect(true).toBe(true); // observability-only test
});

it("vendor query selects vendor-agent and reduces the catalogue", async () => {
Expand Down
3 changes: 2 additions & 1 deletion Servers/middleware/rateLimit.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import logger from "../utils/logger/fileLogger";
// production must NOT silently relax brute-force protection, so anything we
// don't recognise as dev/test is treated as production.
const nodeEnv = (process.env.NODE_ENV ?? "").trim().toLowerCase();
const isNonProduction = nodeEnv === "development" || nodeEnv === "test" || nodeEnv === "local";
export const isNonProduction =
nodeEnv === "development" || nodeEnv === "test" || nodeEnv === "local";

/**
* Rate limit configuration with time window and request limits
Expand Down
2 changes: 1 addition & 1 deletion Servers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"test:smoke": "jest --config jest.config.js --globalSetup=\"<rootDir>/tests/integration/globalSetup.js\" --testPathPatterns=deadline-summary --runInBand",
"test": "npm run test:unit",
"test:watch": "jest --watch",
"test:integration": "jest --config jest.config.js --globalSetup=\"<rootDir>/tests/integration/globalSetup.js\" --testMatch=\"**/tests/integration/**/*.test.ts\" --runInBand",
"test:integration": "node --max-old-space-size=4096 node_modules/jest/bin/jest.js --config jest.config.js --globalSetup=\"<rootDir>/tests/integration/globalSetup.js\" --testMatch=\"**/tests/integration/**/*.test.ts\" --runInBand",
"build": "tsc",
"start": "npm run migrate-db && node dist/index.js",
"postbuild": "node scripts/copyFiles.js",
Expand Down
Loading
Loading