Skip to content

Commit f7bb870

Browse files
authored
Merge pull request #38 from objectstack-ai/copilot/complete-next-phase-development
2 parents b2b1e5d + 27014f6 commit f7bb870

24 files changed

+2785
-20
lines changed

__tests__/hooks/useAnalyticsQuery.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { renderHook, act, waitFor } from "@testing-library/react-native";
66

77
/* ---- Mock useClient from SDK ---- */
88
const mockAnalyticsQuery = jest.fn();
9+
const mockAnalyticsExplain = jest.fn();
910

1011
const mockClient = {
1112
analytics: {
1213
query: mockAnalyticsQuery,
14+
explain: mockAnalyticsExplain,
1315
},
1416
};
1517

@@ -21,6 +23,7 @@ import { useAnalyticsQuery } from "~/hooks/useAnalyticsQuery";
2123

2224
beforeEach(() => {
2325
mockAnalyticsQuery.mockReset();
26+
mockAnalyticsExplain.mockReset();
2427
});
2528

2629
describe("useAnalyticsQuery", () => {
@@ -152,4 +155,64 @@ describe("useAnalyticsQuery", () => {
152155
limit: 10,
153156
});
154157
});
158+
159+
it("calls explain with current query params", async () => {
160+
mockAnalyticsQuery.mockResolvedValue({ data: [], total: 0 });
161+
mockAnalyticsExplain.mockResolvedValue({
162+
sql: "SELECT status, COUNT(*) FROM tasks GROUP BY status",
163+
plan: "Seq Scan on tasks",
164+
description: "Count tasks grouped by status",
165+
});
166+
167+
const { result } = renderHook(() =>
168+
useAnalyticsQuery({ metric: "tasks", groupBy: "status", aggregate: "count" }),
169+
);
170+
171+
await waitFor(() => {
172+
expect(result.current.isLoading).toBe(false);
173+
});
174+
175+
let explainResult: unknown;
176+
await act(async () => {
177+
explainResult = await result.current.explain();
178+
});
179+
180+
expect(mockAnalyticsExplain).toHaveBeenCalledWith({
181+
metric: "tasks",
182+
groupBy: "status",
183+
aggregate: "count",
184+
field: undefined,
185+
filter: undefined,
186+
startDate: undefined,
187+
endDate: undefined,
188+
limit: undefined,
189+
});
190+
expect(explainResult).toEqual({
191+
sql: "SELECT status, COUNT(*) FROM tasks GROUP BY status",
192+
plan: "Seq Scan on tasks",
193+
description: "Count tasks grouped by status",
194+
});
195+
});
196+
197+
it("calls explain with custom payload", async () => {
198+
mockAnalyticsQuery.mockResolvedValue({ data: [], total: 0 });
199+
mockAnalyticsExplain.mockResolvedValue({ sql: "SELECT 1" });
200+
201+
const { result } = renderHook(() =>
202+
useAnalyticsQuery({ metric: "tasks" }),
203+
);
204+
205+
await waitFor(() => {
206+
expect(result.current.isLoading).toBe(false);
207+
});
208+
209+
await act(async () => {
210+
await result.current.explain({ metric: "custom", limit: 5 });
211+
});
212+
213+
expect(mockAnalyticsExplain).toHaveBeenCalledWith({
214+
metric: "custom",
215+
limit: 5,
216+
});
217+
});
155218
});
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Tests for useAutomation – validates automation trigger
3+
* and approval/rejection operations.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
7+
/* ---- Mock useClient from SDK ---- */
8+
const mockTrigger = jest.fn();
9+
const mockApprove = jest.fn();
10+
const mockReject = jest.fn();
11+
12+
const mockClient = {
13+
automation: { trigger: mockTrigger },
14+
workflow: { approve: mockApprove, reject: mockReject },
15+
};
16+
17+
jest.mock("@objectstack/client-react", () => ({
18+
useClient: () => mockClient,
19+
}));
20+
21+
import { useAutomation } from "~/hooks/useAutomation";
22+
23+
beforeEach(() => {
24+
mockTrigger.mockReset();
25+
mockApprove.mockReset();
26+
mockReject.mockReset();
27+
});
28+
29+
describe("useAutomation", () => {
30+
it("triggers an automation flow with payload", async () => {
31+
mockTrigger.mockResolvedValue({
32+
executionId: "exec-1",
33+
message: "Started",
34+
data: { status: "ok" },
35+
});
36+
37+
const { result } = renderHook(() => useAutomation());
38+
39+
let triggerResult: unknown;
40+
await act(async () => {
41+
triggerResult = await result.current.trigger("onboard-user", {
42+
userId: "123",
43+
});
44+
});
45+
46+
expect(mockTrigger).toHaveBeenCalledWith("onboard-user", {
47+
userId: "123",
48+
});
49+
expect(triggerResult).toEqual({
50+
success: true,
51+
executionId: "exec-1",
52+
message: "Started",
53+
data: { status: "ok" },
54+
});
55+
expect(result.current.isLoading).toBe(false);
56+
expect(result.current.error).toBeNull();
57+
});
58+
59+
it("triggers without payload", async () => {
60+
mockTrigger.mockResolvedValue({});
61+
62+
const { result } = renderHook(() => useAutomation());
63+
64+
await act(async () => {
65+
await result.current.trigger("daily-report");
66+
});
67+
68+
expect(mockTrigger).toHaveBeenCalledWith("daily-report", {});
69+
});
70+
71+
it("handles trigger error", async () => {
72+
mockTrigger.mockRejectedValue(new Error("Flow not found"));
73+
74+
const { result } = renderHook(() => useAutomation());
75+
76+
await act(async () => {
77+
await expect(
78+
result.current.trigger("nonexistent"),
79+
).rejects.toThrow("Flow not found");
80+
});
81+
82+
expect(result.current.error?.message).toBe("Flow not found");
83+
});
84+
85+
it("approves a workflow step", async () => {
86+
mockApprove.mockResolvedValue({ success: true });
87+
88+
const { result } = renderHook(() => useAutomation());
89+
90+
await act(async () => {
91+
await result.current.approve("tasks", "rec-1", "LGTM");
92+
});
93+
94+
expect(mockApprove).toHaveBeenCalledWith({
95+
object: "tasks",
96+
recordId: "rec-1",
97+
comment: "LGTM",
98+
});
99+
expect(result.current.error).toBeNull();
100+
});
101+
102+
it("handles approval error", async () => {
103+
mockApprove.mockRejectedValue(new Error("Not authorized"));
104+
105+
const { result } = renderHook(() => useAutomation());
106+
107+
await act(async () => {
108+
await expect(
109+
result.current.approve("tasks", "rec-1"),
110+
).rejects.toThrow("Not authorized");
111+
});
112+
113+
expect(result.current.error?.message).toBe("Not authorized");
114+
});
115+
116+
it("rejects a workflow step with reason", async () => {
117+
mockReject.mockResolvedValue({ success: true });
118+
119+
const { result } = renderHook(() => useAutomation());
120+
121+
await act(async () => {
122+
await result.current.reject("tasks", "rec-1", "Needs changes", "See comments");
123+
});
124+
125+
expect(mockReject).toHaveBeenCalledWith({
126+
object: "tasks",
127+
recordId: "rec-1",
128+
reason: "Needs changes",
129+
comment: "See comments",
130+
});
131+
expect(result.current.error).toBeNull();
132+
});
133+
134+
it("handles rejection error", async () => {
135+
mockReject.mockRejectedValue(new Error("Rejection failed"));
136+
137+
const { result } = renderHook(() => useAutomation());
138+
139+
await act(async () => {
140+
await expect(
141+
result.current.reject("tasks", "rec-1", "Bad"),
142+
).rejects.toThrow("Rejection failed");
143+
});
144+
145+
expect(result.current.error?.message).toBe("Rejection failed");
146+
});
147+
});

0 commit comments

Comments
 (0)