306 lines
8.1 KiB
TypeScript
306 lines
8.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { SystemRole } from "@capakraken/shared";
|
|
import { executeTool, type ToolContext } from "../router/assistant-tools.js";
|
|
|
|
function createToolContext(
|
|
db: Record<string, unknown>,
|
|
permissions: string[] = [],
|
|
userRole: SystemRole = SystemRole.ADMIN,
|
|
): ToolContext {
|
|
return {
|
|
db: db as ToolContext["db"],
|
|
userId: "user_1",
|
|
userRole,
|
|
permissions: new Set(permissions) as ToolContext["permissions"],
|
|
session: {
|
|
user: { email: "assistant@example.com", name: "Assistant User", image: null },
|
|
expires: "2026-03-29T00:00:00.000Z",
|
|
},
|
|
dbUser: {
|
|
id: "user_1",
|
|
systemRole: userRole,
|
|
permissionOverrides: null,
|
|
},
|
|
roleDefaults: null,
|
|
};
|
|
}
|
|
|
|
describe("assistant country tools", () => {
|
|
it("lists countries with schedule rules, active state, and metro cities", async () => {
|
|
const findMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "country_de",
|
|
code: "DE",
|
|
name: "Deutschland",
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: null,
|
|
isActive: true,
|
|
metroCities: [{ id: "city_muc", name: "Munich" }],
|
|
},
|
|
{
|
|
id: "country_es",
|
|
code: "ES",
|
|
name: "Spain",
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: null,
|
|
isActive: true,
|
|
metroCities: [{ id: "city_mad", name: "Madrid" }],
|
|
},
|
|
]);
|
|
const ctx = createToolContext({
|
|
country: {
|
|
findMany,
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"list_countries",
|
|
JSON.stringify({ search: "deu" }),
|
|
ctx,
|
|
);
|
|
|
|
const parsed = JSON.parse(result.content) as {
|
|
count: number;
|
|
countries: Array<{
|
|
code: string;
|
|
isActive: boolean;
|
|
metroCities: Array<{ id: string; name: string }>;
|
|
cities: string[];
|
|
}>;
|
|
};
|
|
|
|
expect(findMany).toHaveBeenCalledWith({
|
|
where: { isActive: true },
|
|
include: { metroCities: { orderBy: { name: "asc" } } },
|
|
orderBy: { name: "asc" },
|
|
});
|
|
expect(parsed.count).toBe(1);
|
|
expect(parsed.countries[0]).toMatchObject({
|
|
code: "DE",
|
|
isActive: true,
|
|
cities: ["Munich"],
|
|
metroCities: [{ id: "city_muc", name: "Munich" }],
|
|
});
|
|
});
|
|
|
|
it("gets a country by code and exposes schedule details and resource count", async () => {
|
|
const ctx = createToolContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
findFirst: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
id: "country_es",
|
|
code: "ES",
|
|
name: "Spain",
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: {
|
|
type: "spain",
|
|
fridayHours: 6.5,
|
|
summerPeriod: { from: "07-01", to: "09-15" },
|
|
summerHours: 6.5,
|
|
regularHours: 9,
|
|
},
|
|
isActive: true,
|
|
metroCities: [{ id: "city_mad", name: "Madrid" }],
|
|
_count: { resources: 4 },
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"get_country",
|
|
JSON.stringify({ identifier: "ES" }),
|
|
ctx,
|
|
);
|
|
|
|
const parsed = JSON.parse(result.content) as {
|
|
code: string;
|
|
resourceCount: number | null;
|
|
scheduleRules: { type: string };
|
|
metroCities: Array<{ name: string }>;
|
|
};
|
|
|
|
expect(parsed).toMatchObject({
|
|
code: "ES",
|
|
resourceCount: 4,
|
|
scheduleRules: { type: "spain" },
|
|
metroCities: [{ name: "Madrid" }],
|
|
});
|
|
});
|
|
|
|
it("returns a stable error when a country cannot be resolved", async () => {
|
|
const ctx = createToolContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
findFirst: vi.fn().mockResolvedValue(null),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"get_country",
|
|
JSON.stringify({ identifier: "Atlantis" }),
|
|
ctx,
|
|
);
|
|
|
|
expect(JSON.parse(result.content)).toEqual({
|
|
error: "Country not found with the given criteria.",
|
|
});
|
|
});
|
|
|
|
it("creates a country for admin users and returns an invalidation action", async () => {
|
|
const ctx = createToolContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
create: vi.fn().mockResolvedValue({
|
|
id: "country_es",
|
|
code: "ES",
|
|
name: "Spain",
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: null,
|
|
isActive: true,
|
|
metroCities: [],
|
|
_count: { resources: 0 },
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"create_country",
|
|
JSON.stringify({ code: "ES", name: "Spain", dailyWorkingHours: 8 }),
|
|
ctx,
|
|
);
|
|
|
|
expect(result.action).toEqual({
|
|
type: "invalidate",
|
|
scope: ["country", "resource", "holidayCalendar", "vacation"],
|
|
});
|
|
expect(result.data).toMatchObject({
|
|
success: true,
|
|
country: { code: "ES", name: "Spain" },
|
|
});
|
|
});
|
|
|
|
it("returns a stable error when creating a country with a duplicate code", async () => {
|
|
const ctx = createToolContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue({
|
|
id: "country_es_existing",
|
|
code: "ES",
|
|
name: "Existing Spain",
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"create_country",
|
|
JSON.stringify({ code: "ES", name: "Spain", dailyWorkingHours: 8 }),
|
|
ctx,
|
|
);
|
|
|
|
expect(JSON.parse(result.content)).toEqual({
|
|
error: "A country with this code already exists.",
|
|
});
|
|
});
|
|
|
|
it("returns a stable error when updating a missing country", async () => {
|
|
const ctx = createToolContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"update_country",
|
|
JSON.stringify({ id: "country_missing", data: { name: "Atlantis" } }),
|
|
ctx,
|
|
);
|
|
|
|
expect(JSON.parse(result.content)).toEqual({
|
|
error: "Country not found with the given criteria.",
|
|
});
|
|
});
|
|
|
|
it("refuses country mutations for non-admin users", async () => {
|
|
const ctx = createToolContext({ country: {} }, [], SystemRole.MANAGER);
|
|
|
|
const result = await executeTool(
|
|
"create_country",
|
|
JSON.stringify({ code: "ES", name: "Spain" }),
|
|
ctx,
|
|
);
|
|
|
|
expect(JSON.parse(result.content)).toEqual({
|
|
error: "Admin role required to perform this action.",
|
|
});
|
|
});
|
|
|
|
it("deletes metro cities only when no resources are assigned", async () => {
|
|
const ctx = createToolContext({
|
|
metroCity: {
|
|
findUnique: vi.fn().mockResolvedValue({
|
|
id: "city_ham",
|
|
name: "Hamburg",
|
|
_count: { resources: 0 },
|
|
}),
|
|
delete: vi.fn().mockResolvedValue(undefined),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"delete_metro_city",
|
|
JSON.stringify({ id: "city_ham" }),
|
|
ctx,
|
|
);
|
|
|
|
expect(result.action).toEqual({
|
|
type: "invalidate",
|
|
scope: ["country", "resource", "holidayCalendar", "vacation"],
|
|
});
|
|
expect(result.data).toMatchObject({
|
|
success: true,
|
|
message: "Deleted metro city: Hamburg",
|
|
});
|
|
});
|
|
|
|
it("returns a stable error when updating a missing metro city", async () => {
|
|
const ctx = createToolContext({
|
|
metroCity: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"update_metro_city",
|
|
JSON.stringify({ id: "city_missing", data: { name: "Hamburg-Mitte" } }),
|
|
ctx,
|
|
);
|
|
|
|
expect(JSON.parse(result.content)).toEqual({
|
|
error: "Metro city not found with the given criteria.",
|
|
});
|
|
});
|
|
|
|
it("returns a stable error when deleting a metro city that is still assigned", async () => {
|
|
const ctx = createToolContext({
|
|
metroCity: {
|
|
findUnique: vi.fn().mockResolvedValue({
|
|
id: "city_ham",
|
|
name: "Hamburg",
|
|
_count: { resources: 3 },
|
|
}),
|
|
},
|
|
});
|
|
|
|
const result = await executeTool(
|
|
"delete_metro_city",
|
|
JSON.stringify({ id: "city_ham" }),
|
|
ctx,
|
|
);
|
|
|
|
expect(JSON.parse(result.content)).toEqual({
|
|
error: "Metro city cannot be deleted while it is still assigned to resources.",
|
|
});
|
|
});
|
|
});
|