test(api): cover assistant country tools
This commit is contained in:
@@ -0,0 +1,141 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { PermissionKey } from "@capakraken/shared";
|
||||||
|
|
||||||
|
import { executeTool } from "../router/assistant-tools.js";
|
||||||
|
import { createToolContext } from "./assistant-tools-country-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant country get tools", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets a country by identifier and falls back from id to code and name lookup", async () => {
|
||||||
|
const db = {
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce(null)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
id: "country_de",
|
||||||
|
code: "DE",
|
||||||
|
name: "Germany",
|
||||||
|
dailyWorkingHours: 8,
|
||||||
|
scheduleRules: null,
|
||||||
|
isActive: true,
|
||||||
|
metroCities: [{ id: "city_muc", name: "Munich" }],
|
||||||
|
_count: { resources: 12 },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ctx = createToolContext(db, [PermissionKey.VIEW_ALL_RESOURCES]);
|
||||||
|
|
||||||
|
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" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbackResult = await executeTool(
|
||||||
|
"get_country",
|
||||||
|
JSON.stringify({ identifier: "Germany" }),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(db.country.findUnique).toHaveBeenNthCalledWith(1, {
|
||||||
|
where: { id: "ES" },
|
||||||
|
include: {
|
||||||
|
metroCities: { orderBy: { name: "asc" } },
|
||||||
|
_count: { select: { resources: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(db.country.findUnique).toHaveBeenNthCalledWith(2, {
|
||||||
|
where: { id: "Germany" },
|
||||||
|
include: {
|
||||||
|
metroCities: { orderBy: { name: "asc" } },
|
||||||
|
_count: { select: { resources: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(db.country.findFirst).toHaveBeenNthCalledWith(1, {
|
||||||
|
where: { code: { equals: "ES", mode: "insensitive" } },
|
||||||
|
include: {
|
||||||
|
metroCities: { orderBy: { name: "asc" } },
|
||||||
|
_count: { select: { resources: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(db.country.findFirst).toHaveBeenNthCalledWith(2, {
|
||||||
|
where: { code: { equals: "GERMANY", mode: "insensitive" } },
|
||||||
|
include: {
|
||||||
|
metroCities: { orderBy: { name: "asc" } },
|
||||||
|
_count: { select: { resources: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(db.country.findFirst).toHaveBeenNthCalledWith(3, {
|
||||||
|
where: { name: { equals: "Germany", mode: "insensitive" } },
|
||||||
|
include: {
|
||||||
|
metroCities: { orderBy: { name: "asc" } },
|
||||||
|
_count: { select: { resources: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(JSON.parse(fallbackResult.content)).toEqual({
|
||||||
|
id: "country_de",
|
||||||
|
code: "DE",
|
||||||
|
name: "Germany",
|
||||||
|
dailyWorkingHours: 8,
|
||||||
|
scheduleRules: null,
|
||||||
|
isActive: true,
|
||||||
|
resourceCount: 12,
|
||||||
|
metroCities: [{ id: "city_muc", name: "Munich" }],
|
||||||
|
cities: ["Munich"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}, [PermissionKey.VIEW_ALL_RESOURCES]);
|
||||||
|
|
||||||
|
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.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { executeTool } from "../router/assistant-tools.js";
|
||||||
|
import { createToolContext } from "./assistant-tools-country-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant country list tools", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
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" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
import { executeTool } from "../router/assistant-tools.js";
|
||||||
|
import { createToolContext } from "./assistant-tools-country-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant country mutation tools - errors", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
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: "You do not have permission to perform this action.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { executeTool } from "../router/assistant-tools.js";
|
||||||
|
import { createToolContext } from "./assistant-tools-country-test-helpers.js";
|
||||||
|
|
||||||
|
describe("assistant country mutation tools - success", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
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("updates a country for admin users and keeps invalidation semantics stable", async () => {
|
||||||
|
const update = vi.fn().mockResolvedValue({
|
||||||
|
id: "country_es",
|
||||||
|
code: "ES",
|
||||||
|
name: "Spain Updated",
|
||||||
|
dailyWorkingHours: 7.5,
|
||||||
|
scheduleRules: { shortFriday: true },
|
||||||
|
isActive: false,
|
||||||
|
metroCities: [],
|
||||||
|
_count: { resources: 0 },
|
||||||
|
});
|
||||||
|
const ctx = createToolContext({
|
||||||
|
country: {
|
||||||
|
findUnique: vi.fn().mockResolvedValue({ id: "country_es", code: "ES", name: "Spain" }),
|
||||||
|
update,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await executeTool(
|
||||||
|
"update_country",
|
||||||
|
JSON.stringify({
|
||||||
|
id: "country_es",
|
||||||
|
data: {
|
||||||
|
name: "Spain Updated",
|
||||||
|
dailyWorkingHours: 7.5,
|
||||||
|
scheduleRules: null,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(update).toHaveBeenCalledWith({
|
||||||
|
where: { id: "country_es" },
|
||||||
|
data: {
|
||||||
|
name: "Spain Updated",
|
||||||
|
dailyWorkingHours: 7.5,
|
||||||
|
scheduleRules: expect.anything(),
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
include: { metroCities: true },
|
||||||
|
});
|
||||||
|
expect(result.action).toEqual({
|
||||||
|
type: "invalidate",
|
||||||
|
scope: ["country", "resource", "holidayCalendar", "vacation"],
|
||||||
|
});
|
||||||
|
expect(result.data).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
country: { id: "country_es", isActive: false, name: "Spain Updated" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
|
||||||
|
import type { ToolContext } from "../router/assistant-tools.js";
|
||||||
|
|
||||||
|
export 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user