Skip to content
This repository was archived by the owner on Jan 19, 2026. It is now read-only.

Commit 4d4da8c

Browse files
authored
feat: integration tests for all tools (#59)
1 parent ac727e7 commit 4d4da8c

10 files changed

Lines changed: 1605 additions & 2 deletions

src/tools/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { z } from "zod";
22
import type { ApiClient } from "../api/client";
3-
import type { DurableObjectCache } from "../lib/utils/cache/DurableObjectCache";
3+
import type { ScopedCache } from "../lib/utils/cache/ScopedCache";
44

55
export type State = {
66
projectId: string | undefined;
@@ -10,7 +10,7 @@ export type State = {
1010

1111
export type Context = {
1212
api: ApiClient;
13-
cache: DurableObjectCache<State>;
13+
cache: ScopedCache<State>;
1414
env: Env;
1515
getProjectId: () => Promise<string>;
1616
getOrgID: () => Promise<string>;

tests/shared/test-utils.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { expect } from "vitest";
2+
import { ApiClient } from "../../src/api/client";
3+
import type { Context } from "../../src/tools/types";
4+
import { MemoryCache } from "../../src/lib/utils/cache/MemoryCache";
5+
6+
export const API_BASE_URL = process.env.TEST_API_BASE_URL || "http://localhost:8010";
7+
export const API_TOKEN = process.env.TEST_API_TOKEN;
8+
export const TEST_ORG_ID = process.env.TEST_ORG_ID;
9+
export const TEST_PROJECT_ID = process.env.TEST_PROJECT_ID;
10+
11+
export interface CreatedResources {
12+
featureFlags: number[];
13+
insights: number[];
14+
dashboards: number[];
15+
}
16+
17+
export function validateEnvironmentVariables() {
18+
if (!API_TOKEN) {
19+
throw new Error("TEST_API_TOKEN environment variable is required");
20+
}
21+
22+
if (!TEST_ORG_ID) {
23+
throw new Error("TEST_ORG_ID environment variable is required");
24+
}
25+
26+
if (!TEST_PROJECT_ID) {
27+
throw new Error("TEST_PROJECT_ID environment variable is required");
28+
}
29+
}
30+
31+
export function createTestClient(): ApiClient {
32+
return new ApiClient({
33+
apiToken: API_TOKEN!,
34+
baseUrl: API_BASE_URL,
35+
});
36+
}
37+
38+
export function createTestContext(client: ApiClient): Context {
39+
const cache = new MemoryCache<any>("test-user");
40+
41+
const context: Context = {
42+
api: client,
43+
cache,
44+
env: {} as any,
45+
getProjectId: async () => {
46+
const projectId = await cache.get("projectId");
47+
if (!projectId) {
48+
throw new Error("No active project set");
49+
}
50+
return projectId;
51+
},
52+
getOrgID: async () => {
53+
const orgId = await cache.get("orgId");
54+
if (!orgId) {
55+
throw new Error("No active organization set");
56+
}
57+
return orgId;
58+
},
59+
getDistinctId: async () => {
60+
const distinctId = await cache.get("distinctId");
61+
return distinctId || "";
62+
},
63+
};
64+
65+
return context;
66+
}
67+
68+
export async function setActiveProjectAndOrg(context: Context, projectId: string, orgId: string) {
69+
const cache = context.cache;
70+
await cache.set("projectId", projectId);
71+
await cache.set("orgId", orgId);
72+
}
73+
74+
export async function cleanupResources(
75+
client: ApiClient,
76+
projectId: string,
77+
resources: CreatedResources,
78+
) {
79+
for (const flagId of resources.featureFlags) {
80+
try {
81+
await client.featureFlags({ projectId }).delete({ flagId });
82+
} catch (error) {
83+
console.warn(`Failed to cleanup feature flag ${flagId}:`, error);
84+
}
85+
}
86+
resources.featureFlags = [];
87+
88+
for (const insightId of resources.insights) {
89+
try {
90+
await client.insights({ projectId }).delete({ insightId });
91+
} catch (error) {
92+
console.warn(`Failed to cleanup insight ${insightId}:`, error);
93+
}
94+
}
95+
resources.insights = [];
96+
97+
for (const dashboardId of resources.dashboards) {
98+
try {
99+
await client.dashboards({ projectId }).delete({ dashboardId });
100+
} catch (error) {
101+
console.warn(`Failed to cleanup dashboard ${dashboardId}:`, error);
102+
}
103+
}
104+
resources.dashboards = [];
105+
}
106+
107+
export function parseToolResponse(result: any) {
108+
expect(result.content).toBeDefined();
109+
expect(result.content[0].type).toBe("text");
110+
return JSON.parse(result.content[0].text);
111+
}
112+
113+
export function generateUniqueKey(prefix: string): string {
114+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
115+
}
116+
117+
export const SAMPLE_HOGQL_QUERIES = {
118+
pageviews: {
119+
kind: "DataVisualizationNode" as const,
120+
source: {
121+
kind: "HogQLQuery" as const,
122+
query: "SELECT event, count() AS event_count FROM events WHERE timestamp >= now() - INTERVAL 7 DAY AND event = '$pageview' GROUP BY event ORDER BY event_count DESC LIMIT 10",
123+
filters: {
124+
dateRange: {
125+
date_from: "-7d",
126+
date_to: "-1d",
127+
},
128+
},
129+
},
130+
},
131+
topEvents: {
132+
kind: "DataVisualizationNode" as const,
133+
source: {
134+
kind: "HogQLQuery" as const,
135+
query: "SELECT event, count() AS event_count FROM events WHERE timestamp >= now() - INTERVAL 7 DAY GROUP BY event ORDER BY event_count DESC LIMIT 10",
136+
filters: {
137+
dateRange: {
138+
date_from: "-7d",
139+
date_to: "-1d",
140+
},
141+
},
142+
},
143+
},
144+
};
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { describe, it, expect, beforeAll, afterEach } from "vitest";
2+
import {
3+
validateEnvironmentVariables,
4+
createTestClient,
5+
createTestContext,
6+
setActiveProjectAndOrg,
7+
cleanupResources,
8+
parseToolResponse,
9+
generateUniqueKey,
10+
TEST_PROJECT_ID,
11+
TEST_ORG_ID,
12+
type CreatedResources,
13+
} from "../shared/test-utils";
14+
import createDashboardTool from "../../src/tools/dashboards/create";
15+
import updateDashboardTool from "../../src/tools/dashboards/update";
16+
import deleteDashboardTool from "../../src/tools/dashboards/delete";
17+
import getAllDashboardsTool from "../../src/tools/dashboards/getAll";
18+
import getDashboardTool from "../../src/tools/dashboards/get";
19+
import type { Context } from "../../src/tools/types";
20+
21+
describe("Dashboards", { concurrent: false }, () => {
22+
let context: Context;
23+
const createdResources: CreatedResources = {
24+
featureFlags: [],
25+
insights: [],
26+
dashboards: [],
27+
};
28+
29+
beforeAll(async () => {
30+
validateEnvironmentVariables();
31+
const client = createTestClient();
32+
context = createTestContext(client);
33+
await setActiveProjectAndOrg(context, TEST_PROJECT_ID!, TEST_ORG_ID!);
34+
});
35+
36+
afterEach(async () => {
37+
await cleanupResources(context.api, TEST_PROJECT_ID!, createdResources);
38+
});
39+
40+
describe("create-dashboard tool", () => {
41+
const createTool = createDashboardTool();
42+
43+
it("should create a dashboard with minimal fields", async () => {
44+
const params = {
45+
data: {
46+
name: generateUniqueKey("Test Dashboard"),
47+
description: "Integration test dashboard",
48+
},
49+
};
50+
51+
const result = await createTool.handler(context, params);
52+
const dashboardData = parseToolResponse(result);
53+
54+
expect(dashboardData.id).toBeDefined();
55+
expect(dashboardData.name).toBe(params.data.name);
56+
expect(dashboardData.url).toContain("/dashboard/");
57+
58+
createdResources.dashboards.push(dashboardData.id);
59+
});
60+
61+
it("should create a dashboard with tags", async () => {
62+
const params = {
63+
data: {
64+
name: generateUniqueKey("Tagged Dashboard"),
65+
description: "Dashboard with tags",
66+
tags: ["test", "integration"],
67+
},
68+
};
69+
70+
const result = await createTool.handler(context, params);
71+
const dashboardData = parseToolResponse(result);
72+
73+
expect(dashboardData.id).toBeDefined();
74+
expect(dashboardData.name).toBe(params.data.name);
75+
76+
createdResources.dashboards.push(dashboardData.id);
77+
});
78+
});
79+
80+
describe("update-dashboard tool", () => {
81+
const createTool = createDashboardTool();
82+
const updateTool = updateDashboardTool();
83+
84+
it("should update dashboard name and description", async () => {
85+
const createParams = {
86+
data: {
87+
name: generateUniqueKey("Original Dashboard"),
88+
description: "Original description",
89+
},
90+
};
91+
92+
const createResult = await createTool.handler(context, createParams);
93+
const createdDashboard = parseToolResponse(createResult);
94+
createdResources.dashboards.push(createdDashboard.id);
95+
96+
const updateParams = {
97+
dashboardId: createdDashboard.id,
98+
data: {
99+
name: "Updated Dashboard Name",
100+
description: "Updated description",
101+
},
102+
};
103+
104+
const updateResult = await updateTool.handler(context, updateParams);
105+
const updatedDashboard = parseToolResponse(updateResult);
106+
107+
expect(updatedDashboard.id).toBe(createdDashboard.id);
108+
expect(updatedDashboard.name).toBe(updateParams.data.name);
109+
});
110+
});
111+
112+
describe("get-all-dashboards tool", () => {
113+
const getAllTool = getAllDashboardsTool();
114+
115+
it("should return dashboards with proper structure", async () => {
116+
const result = await getAllTool.handler(context, {});
117+
const dashboards = parseToolResponse(result);
118+
119+
expect(Array.isArray(dashboards)).toBe(true);
120+
if (dashboards.length > 0) {
121+
const dashboard = dashboards[0];
122+
expect(dashboard).toHaveProperty("id");
123+
expect(dashboard).toHaveProperty("name");
124+
}
125+
});
126+
});
127+
128+
describe("get-dashboard tool", () => {
129+
const createTool = createDashboardTool();
130+
const getTool = getDashboardTool();
131+
132+
it("should get a specific dashboard by ID", async () => {
133+
const createParams = {
134+
data: {
135+
name: generateUniqueKey("Get Test Dashboard"),
136+
description: "Test dashboard for get operation",
137+
},
138+
};
139+
140+
const createResult = await createTool.handler(context, createParams);
141+
const createdDashboard = parseToolResponse(createResult);
142+
createdResources.dashboards.push(createdDashboard.id);
143+
144+
const result = await getTool.handler(context, { dashboardId: createdDashboard.id });
145+
const retrievedDashboard = parseToolResponse(result);
146+
147+
expect(retrievedDashboard.id).toBe(createdDashboard.id);
148+
expect(retrievedDashboard.name).toBe(createParams.data.name);
149+
});
150+
});
151+
152+
describe("delete-dashboard tool", () => {
153+
const createTool = createDashboardTool();
154+
const deleteTool = deleteDashboardTool();
155+
156+
it("should delete a dashboard", async () => {
157+
const createParams = {
158+
data: {
159+
name: generateUniqueKey("Delete Test Dashboard"),
160+
description: "Test dashboard for deletion",
161+
},
162+
};
163+
164+
const createResult = await createTool.handler(context, createParams);
165+
const createdDashboard = parseToolResponse(createResult);
166+
167+
const deleteResult = await deleteTool.handler(context, {
168+
dashboardId: createdDashboard.id,
169+
});
170+
const deleteResponse = parseToolResponse(deleteResult);
171+
172+
expect(deleteResponse.success).toBe(true);
173+
expect(deleteResponse.message).toContain("deleted successfully");
174+
});
175+
});
176+
177+
describe("Dashboard workflow", () => {
178+
it("should support full CRUD workflow", async () => {
179+
const createTool = createDashboardTool();
180+
const updateTool = updateDashboardTool();
181+
const getTool = getDashboardTool();
182+
const deleteTool = deleteDashboardTool();
183+
184+
const createParams = {
185+
data: {
186+
name: generateUniqueKey("Workflow Test Dashboard"),
187+
description: "Testing full workflow",
188+
},
189+
};
190+
191+
const createResult = await createTool.handler(context, createParams);
192+
const createdDashboard = parseToolResponse(createResult);
193+
194+
const getResult = await getTool.handler(context, { dashboardId: createdDashboard.id });
195+
const retrievedDashboard = parseToolResponse(getResult);
196+
expect(retrievedDashboard.id).toBe(createdDashboard.id);
197+
198+
const updateParams = {
199+
dashboardId: createdDashboard.id,
200+
data: {
201+
name: "Updated Workflow Dashboard",
202+
description: "Updated workflow description",
203+
},
204+
};
205+
206+
const updateResult = await updateTool.handler(context, updateParams);
207+
const updatedDashboard = parseToolResponse(updateResult);
208+
expect(updatedDashboard.name).toBe(updateParams.data.name);
209+
210+
const deleteResult = await deleteTool.handler(context, {
211+
dashboardId: createdDashboard.id,
212+
});
213+
const deleteResponse = parseToolResponse(deleteResult);
214+
expect(deleteResponse.success).toBe(true);
215+
});
216+
});
217+
});

0 commit comments

Comments
 (0)