Skip to content

Commit 54a9534

Browse files
Copilothotlong
andcommitted
Add Phase 11 AI hook test files for useAISession, useRAG, useMCPTools, useAgent, useAICost
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 392d399 commit 54a9534

5 files changed

Lines changed: 652 additions & 0 deletions

File tree

__tests__/hooks/useAICost.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Tests for useAICost – validates AI cost analytics
3+
* and budget management operations.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
7+
/* ---- Mock useClient from SDK ---- */
8+
const mockSummary = jest.fn();
9+
const mockHistory = jest.fn();
10+
const mockSetBudget = jest.fn();
11+
12+
const mockClient = {
13+
ai: { cost: { summary: mockSummary, history: mockHistory, setBudget: mockSetBudget } },
14+
};
15+
16+
jest.mock("@objectstack/client-react", () => ({
17+
useClient: () => mockClient,
18+
}));
19+
20+
import { useAICost } from "~/hooks/useAICost";
21+
22+
beforeEach(() => {
23+
mockSummary.mockReset();
24+
mockHistory.mockReset();
25+
mockSetBudget.mockReset();
26+
});
27+
28+
describe("useAICost", () => {
29+
it("fetches cost summary and stores it", async () => {
30+
const summaryData = {
31+
totalCost: 42.5,
32+
totalTokens: 100000,
33+
byModel: { "gpt-4": 30.0, "gpt-3.5": 12.5 },
34+
period: "monthly",
35+
};
36+
mockSummary.mockResolvedValue(summaryData);
37+
38+
const { result } = renderHook(() => useAICost());
39+
40+
let fetched: unknown;
41+
await act(async () => {
42+
fetched = await result.current.getCostSummary();
43+
});
44+
45+
expect(mockSummary).toHaveBeenCalledWith({});
46+
expect(fetched).toEqual(summaryData);
47+
expect(result.current.summary).toEqual(summaryData);
48+
expect(result.current.isLoading).toBe(false);
49+
expect(result.current.error).toBeNull();
50+
});
51+
52+
it("fetches cost summary with period", async () => {
53+
const summaryData = {
54+
totalCost: 10.0,
55+
totalTokens: 25000,
56+
byModel: { "gpt-4": 10.0 },
57+
period: "weekly",
58+
};
59+
mockSummary.mockResolvedValue(summaryData);
60+
61+
const { result } = renderHook(() => useAICost());
62+
63+
await act(async () => {
64+
await result.current.getCostSummary("weekly");
65+
});
66+
67+
expect(mockSummary).toHaveBeenCalledWith({ period: "weekly" });
68+
expect(result.current.summary).toEqual(summaryData);
69+
expect(result.current.isLoading).toBe(false);
70+
expect(result.current.error).toBeNull();
71+
});
72+
73+
it("fetches cost history", async () => {
74+
const historyData = [
75+
{ model: "gpt-4", operation: "chat", inputTokens: 500, outputTokens: 200, cost: 0.05, timestamp: "2024-01-01T00:00:00Z" },
76+
{ model: "gpt-3.5", operation: "embed", inputTokens: 1000, outputTokens: 0, cost: 0.01, timestamp: "2024-01-01T01:00:00Z" },
77+
];
78+
mockHistory.mockResolvedValue(historyData);
79+
80+
const { result } = renderHook(() => useAICost());
81+
82+
let fetched: unknown;
83+
await act(async () => {
84+
fetched = await result.current.getCostHistory({ limit: 10, model: "gpt-4" });
85+
});
86+
87+
expect(mockHistory).toHaveBeenCalledWith({ limit: 10, model: "gpt-4" });
88+
expect(fetched).toEqual(historyData);
89+
expect(result.current.isLoading).toBe(false);
90+
expect(result.current.error).toBeNull();
91+
});
92+
93+
it("sets budget limit and updates summary", async () => {
94+
const summaryData = {
95+
totalCost: 42.5,
96+
totalTokens: 100000,
97+
byModel: { "gpt-4": 30.0 },
98+
period: "monthly",
99+
};
100+
mockSummary.mockResolvedValue(summaryData);
101+
mockSetBudget.mockResolvedValue(undefined);
102+
103+
const { result } = renderHook(() => useAICost());
104+
105+
// First fetch summary so there's state to update
106+
await act(async () => {
107+
await result.current.getCostSummary();
108+
});
109+
110+
await act(async () => {
111+
await result.current.setBudgetLimit(100, "monthly");
112+
});
113+
114+
expect(mockSetBudget).toHaveBeenCalledWith({ limit: 100, period: "monthly" });
115+
expect(result.current.summary?.budgetLimit).toBe(100);
116+
expect(result.current.isLoading).toBe(false);
117+
expect(result.current.error).toBeNull();
118+
});
119+
120+
it("handles summary error", async () => {
121+
mockSummary.mockRejectedValue(new Error("Failed to fetch cost summary"));
122+
123+
const { result } = renderHook(() => useAICost());
124+
125+
await act(async () => {
126+
await expect(
127+
result.current.getCostSummary(),
128+
).rejects.toThrow("Failed to fetch cost summary");
129+
});
130+
131+
expect(result.current.error?.message).toBe("Failed to fetch cost summary");
132+
});
133+
134+
it("handles setBudget error", async () => {
135+
mockSetBudget.mockRejectedValue(new Error("Failed to set budget limit"));
136+
137+
const { result } = renderHook(() => useAICost());
138+
139+
await act(async () => {
140+
await expect(
141+
result.current.setBudgetLimit(100),
142+
).rejects.toThrow("Failed to set budget limit");
143+
});
144+
145+
expect(result.current.error?.message).toBe("Failed to set budget limit");
146+
});
147+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Tests for useAISession – validates AI conversation session
3+
* create, resume, delete, and list operations.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
7+
/* ---- Mock useClient from SDK ---- */
8+
const mockCreate = jest.fn();
9+
const mockGet = jest.fn();
10+
const mockDelete = jest.fn();
11+
const mockList = jest.fn();
12+
13+
const mockClient = {
14+
ai: { sessions: { create: mockCreate, get: mockGet, delete: mockDelete, list: mockList } },
15+
};
16+
17+
jest.mock("@objectstack/client-react", () => ({
18+
useClient: () => mockClient,
19+
}));
20+
21+
import { useAISession } from "~/hooks/useAISession";
22+
23+
beforeEach(() => {
24+
mockCreate.mockReset();
25+
mockGet.mockReset();
26+
mockDelete.mockReset();
27+
mockList.mockReset();
28+
});
29+
30+
describe("useAISession", () => {
31+
it("creates session and adds to list", async () => {
32+
const session = {
33+
id: "sess-1",
34+
title: "My Chat",
35+
createdAt: "2024-01-01T00:00:00Z",
36+
updatedAt: "2024-01-01T00:00:00Z",
37+
messageCount: 0,
38+
tokenUsage: 0,
39+
};
40+
mockCreate.mockResolvedValue(session);
41+
42+
const { result } = renderHook(() => useAISession());
43+
44+
let created: unknown;
45+
await act(async () => {
46+
created = await result.current.createSession("My Chat");
47+
});
48+
49+
expect(mockCreate).toHaveBeenCalledWith({ title: "My Chat" });
50+
expect(created).toEqual(session);
51+
expect(result.current.activeSession).toEqual(session);
52+
expect(result.current.sessions).toContainEqual(session);
53+
expect(result.current.isLoading).toBe(false);
54+
expect(result.current.error).toBeNull();
55+
});
56+
57+
it("resumes session by ID", async () => {
58+
const session = {
59+
id: "sess-2",
60+
title: "Resumed Chat",
61+
createdAt: "2024-01-01T00:00:00Z",
62+
updatedAt: "2024-01-02T00:00:00Z",
63+
messageCount: 5,
64+
tokenUsage: 120,
65+
};
66+
mockGet.mockResolvedValue(session);
67+
68+
const { result } = renderHook(() => useAISession());
69+
70+
let resumed: unknown;
71+
await act(async () => {
72+
resumed = await result.current.resumeSession("sess-2");
73+
});
74+
75+
expect(mockGet).toHaveBeenCalledWith("sess-2");
76+
expect(resumed).toEqual(session);
77+
expect(result.current.activeSession).toEqual(session);
78+
expect(result.current.isLoading).toBe(false);
79+
expect(result.current.error).toBeNull();
80+
});
81+
82+
it("deletes session and removes from list", async () => {
83+
const session = {
84+
id: "sess-3",
85+
title: "To Delete",
86+
createdAt: "2024-01-01T00:00:00Z",
87+
updatedAt: "2024-01-01T00:00:00Z",
88+
messageCount: 0,
89+
tokenUsage: 0,
90+
};
91+
mockCreate.mockResolvedValue(session);
92+
mockDelete.mockResolvedValue(undefined);
93+
94+
const { result } = renderHook(() => useAISession());
95+
96+
await act(async () => {
97+
await result.current.createSession("To Delete");
98+
});
99+
100+
expect(result.current.sessions).toHaveLength(1);
101+
102+
await act(async () => {
103+
await result.current.deleteSession("sess-3");
104+
});
105+
106+
expect(mockDelete).toHaveBeenCalledWith("sess-3");
107+
expect(result.current.sessions).toHaveLength(0);
108+
expect(result.current.activeSession).toBeNull();
109+
expect(result.current.isLoading).toBe(false);
110+
expect(result.current.error).toBeNull();
111+
});
112+
113+
it("lists all sessions", async () => {
114+
const sessions = [
115+
{ id: "sess-1", title: "Chat 1", createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", messageCount: 0, tokenUsage: 0 },
116+
{ id: "sess-2", title: "Chat 2", createdAt: "2024-01-02T00:00:00Z", updatedAt: "2024-01-02T00:00:00Z", messageCount: 3, tokenUsage: 50 },
117+
];
118+
mockList.mockResolvedValue(sessions);
119+
120+
const { result } = renderHook(() => useAISession());
121+
122+
let listed: unknown;
123+
await act(async () => {
124+
listed = await result.current.listSessions();
125+
});
126+
127+
expect(mockList).toHaveBeenCalled();
128+
expect(listed).toEqual(sessions);
129+
expect(result.current.sessions).toEqual(sessions);
130+
expect(result.current.isLoading).toBe(false);
131+
expect(result.current.error).toBeNull();
132+
});
133+
134+
it("handles create error", async () => {
135+
mockCreate.mockRejectedValue(new Error("Failed to create session"));
136+
137+
const { result } = renderHook(() => useAISession());
138+
139+
await act(async () => {
140+
await expect(
141+
result.current.createSession("Bad"),
142+
).rejects.toThrow("Failed to create session");
143+
});
144+
145+
expect(result.current.error?.message).toBe("Failed to create session");
146+
});
147+
148+
it("handles resume error", async () => {
149+
mockGet.mockRejectedValue(new Error("Failed to resume session"));
150+
151+
const { result } = renderHook(() => useAISession());
152+
153+
await act(async () => {
154+
await expect(
155+
result.current.resumeSession("nonexistent"),
156+
).rejects.toThrow("Failed to resume session");
157+
});
158+
159+
expect(result.current.error?.message).toBe("Failed to resume session");
160+
});
161+
});

0 commit comments

Comments
 (0)