@stigmer/react 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/agent/__tests__/useAgent.test.d.ts +2 -0
  2. package/agent/__tests__/useAgent.test.d.ts.map +1 -0
  3. package/agent/__tests__/useAgent.test.js +86 -0
  4. package/agent/__tests__/useAgent.test.js.map +1 -0
  5. package/agent/__tests__/useDefaultAgent.test.js +106 -223
  6. package/agent/__tests__/useDefaultAgent.test.js.map +1 -1
  7. package/agent/useDefaultAgent.d.ts +9 -0
  8. package/agent/useDefaultAgent.d.ts.map +1 -1
  9. package/agent/useDefaultAgent.js +35 -5
  10. package/agent/useDefaultAgent.js.map +1 -1
  11. package/composer/ComposerToolbar.d.ts +18 -16
  12. package/composer/ComposerToolbar.d.ts.map +1 -1
  13. package/composer/ComposerToolbar.js +10 -11
  14. package/composer/ComposerToolbar.js.map +1 -1
  15. package/composer/ConfigureMenu.d.ts.map +1 -1
  16. package/composer/ConfigureMenu.js +1 -1
  17. package/composer/ConfigureMenu.js.map +1 -1
  18. package/composer/ContextPopover.d.ts +1 -3
  19. package/composer/ContextPopover.d.ts.map +1 -1
  20. package/composer/ContextPopover.js +2 -2
  21. package/composer/ContextPopover.js.map +1 -1
  22. package/composer/SessionComposer.js +5 -5
  23. package/composer/SessionComposer.js.map +1 -1
  24. package/composer/icons.js +3 -3
  25. package/internal/withTimeout.d.ts +8 -0
  26. package/internal/withTimeout.d.ts.map +1 -0
  27. package/internal/withTimeout.js +19 -0
  28. package/internal/withTimeout.js.map +1 -0
  29. package/package.json +4 -4
  30. package/session/__tests__/useCreateSession.test.js +99 -191
  31. package/session/__tests__/useCreateSession.test.js.map +1 -1
  32. package/session/__tests__/useNewSessionFlow.test.js +71 -0
  33. package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
  34. package/session/__tests__/useSession.test.js +71 -108
  35. package/session/__tests__/useSession.test.js.map +1 -1
  36. package/session/__tests__/useSessionList.test.d.ts +2 -0
  37. package/session/__tests__/useSessionList.test.d.ts.map +1 -0
  38. package/session/__tests__/useSessionList.test.js +63 -0
  39. package/session/__tests__/useSessionList.test.js.map +1 -0
  40. package/session/useNewSessionFlow.d.ts.map +1 -1
  41. package/session/useNewSessionFlow.js +13 -7
  42. package/session/useNewSessionFlow.js.map +1 -1
  43. package/src/agent/__tests__/useAgent.test.tsx +116 -0
  44. package/src/agent/__tests__/useDefaultAgent.test.tsx +115 -240
  45. package/src/agent/useDefaultAgent.ts +53 -2
  46. package/src/composer/ComposerToolbar.tsx +76 -96
  47. package/src/composer/ConfigureMenu.tsx +16 -14
  48. package/src/composer/ContextPopover.tsx +11 -11
  49. package/src/composer/SessionComposer.tsx +6 -6
  50. package/src/composer/icons.tsx +6 -6
  51. package/src/internal/withTimeout.ts +25 -0
  52. package/src/session/__tests__/useCreateSession.test.tsx +114 -235
  53. package/src/session/__tests__/useNewSessionFlow.test.tsx +96 -1
  54. package/src/session/__tests__/useSession.test.tsx +82 -141
  55. package/src/session/__tests__/useSessionList.test.tsx +86 -0
  56. package/src/session/useNewSessionFlow.ts +18 -9
  57. package/styles.css +1 -1
@@ -1,53 +1,25 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { renderHook, act } from "@testing-library/react";
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, waitFor, act } from "@testing-library/react";
3
3
  import type { ReactNode } from "react";
4
- import { create } from "@bufbuild/protobuf";
5
- import {
6
- SessionSchema,
7
- type Session,
8
- } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
9
- import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
10
- import type { Stigmer } from "@stigmer/sdk";
11
4
  import { StigmerContext } from "../../context";
12
5
  import { FetchCacheContext } from "../../internal/FetchCacheProvider";
13
- import { FetchCache } from "../../internal/fetch-cache";
14
6
  import { useSession } from "../useSession";
15
7
 
16
- // ---------------------------------------------------------------------------
17
- // Helpers
18
- // ---------------------------------------------------------------------------
19
-
20
- function makeSession(id: string): Session {
21
- const session = create(SessionSchema);
22
- const metadata = create(ApiResourceMetadataSchema);
23
- metadata.id = id;
24
- session.metadata = metadata;
25
- return session;
26
- }
27
-
28
- function createMockStigmer(sessionGet: ReturnType<typeof vi.fn>): Stigmer {
8
+ function createMockStigmer(overrides: {
9
+ get?: (...args: unknown[]) => Promise<unknown>;
10
+ } = {}) {
29
11
  return {
30
- session: { get: sessionGet },
31
- } as unknown as Stigmer;
32
- }
33
-
34
- async function flush(): Promise<void> {
35
- await act(async () => {
36
- await Promise.resolve();
37
- });
12
+ session: {
13
+ get: overrides.get ?? vi.fn().mockResolvedValue(null),
14
+ },
15
+ } as never;
38
16
  }
39
17
 
40
- /**
41
- * Wrapper that provides both the Stigmer client and a shared FetchCache
42
- * instance via context. Sharing the cache instance across renderHook
43
- * calls mirrors the production layout where FetchCacheProvider sits
44
- * above the key-based remount boundary.
45
- */
46
- function createWrapper(client: Stigmer, cache: FetchCache) {
18
+ function wrapper(client: unknown) {
47
19
  return function Wrapper({ children }: { children: ReactNode }) {
48
20
  return (
49
- <FetchCacheContext.Provider value={cache}>
50
- <StigmerContext.Provider value={client}>
21
+ <FetchCacheContext.Provider value={null}>
22
+ <StigmerContext.Provider value={client as never}>
51
23
  {children}
52
24
  </StigmerContext.Provider>
53
25
  </FetchCacheContext.Provider>
@@ -55,133 +27,102 @@ function createWrapper(client: Stigmer, cache: FetchCache) {
55
27
  };
56
28
  }
57
29
 
58
- // ---------------------------------------------------------------------------
59
- // Tests
60
- // ---------------------------------------------------------------------------
30
+ describe("useSession", () => {
31
+ beforeEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
61
34
 
62
- describe("useSession cache behavior", () => {
63
- it("first visit shows loading then resolves data", async () => {
64
- const session = makeSession("ses_1");
65
- const sessionGet = vi.fn().mockResolvedValue(session);
66
- const cache = new FetchCache();
67
- const wrapper = createWrapper(createMockStigmer(sessionGet), cache);
35
+ it("fetches session by ID", async () => {
36
+ const session = {
37
+ metadata: { id: "ses-1", name: "Test Session" },
38
+ status: { subject: "Hello world" },
39
+ };
40
+ const get = vi.fn().mockResolvedValue(session);
41
+ const client = createMockStigmer({ get });
68
42
 
69
- const { result } = renderHook(() => useSession("ses_1"), { wrapper });
43
+ const { result } = renderHook(() => useSession("ses-1"), {
44
+ wrapper: wrapper(client),
45
+ });
70
46
 
71
47
  expect(result.current.isLoading).toBe(true);
72
48
  expect(result.current.session).toBeNull();
73
49
 
74
- await flush();
50
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
75
51
 
76
- expect(result.current.isLoading).toBe(false);
77
52
  expect(result.current.session).toBe(session);
78
- expect(sessionGet).toHaveBeenCalledOnce();
53
+ expect(result.current.error).toBeNull();
54
+ expect(get).toHaveBeenCalledWith("ses-1");
79
55
  });
80
56
 
81
- it("remount serves cached data instantly (no isLoading)", async () => {
82
- const session1 = makeSession("ses_1");
83
- const sessionGet = vi.fn().mockResolvedValue(session1);
84
- const client = createMockStigmer(sessionGet);
85
- const cache = new FetchCache();
86
- const wrapper = createWrapper(client, cache);
87
-
88
- // First mount — populates the cache.
89
- const { result: r1, unmount } = renderHook(
90
- () => useSession("ses_1"),
91
- { wrapper },
92
- );
93
- await flush();
94
- expect(r1.current.session).toBe(session1);
95
- unmount();
96
-
97
- // Second mount (simulates remount after key={activeSessionId} change).
98
- const freshSession = makeSession("ses_1");
99
- sessionGet.mockResolvedValue(freshSession);
100
-
101
- const { result: r2 } = renderHook(
102
- () => useSession("ses_1"),
103
- { wrapper },
104
- );
57
+ it("skips fetching when id is null", () => {
58
+ const get = vi.fn();
59
+ const client = createMockStigmer({ get });
105
60
 
106
- // Cached data is served synchronously no loading skeleton.
107
- expect(r2.current.isLoading).toBe(false);
108
- expect(r2.current.session).toBe(session1);
109
- expect(r2.current.isRefetching).toBe(true);
61
+ const { result } = renderHook(() => useSession(null), {
62
+ wrapper: wrapper(client),
63
+ });
110
64
 
111
- // Background fetch completes with fresh data.
112
- await flush();
113
- expect(r2.current.session).toBe(freshSession);
114
- expect(r2.current.isRefetching).toBe(false);
65
+ expect(result.current.isLoading).toBe(false);
66
+ expect(result.current.session).toBeNull();
67
+ expect(get).not.toHaveBeenCalled();
115
68
  });
116
69
 
117
- it("different session IDs have independent cache entries", async () => {
118
- const sessionA = makeSession("ses_A");
119
- const sessionB = makeSession("ses_B");
120
- const sessionGet = vi.fn()
121
- .mockResolvedValueOnce(sessionA)
122
- .mockResolvedValueOnce(sessionB)
123
- .mockResolvedValue(sessionA);
124
- const cache = new FetchCache();
125
- const wrapper = createWrapper(createMockStigmer(sessionGet), cache);
126
-
127
- // Mount session A.
128
- const { unmount: unmountA } = renderHook(
129
- () => useSession("ses_A"),
130
- { wrapper },
131
- );
132
- await flush();
133
- unmountA();
70
+ it("exposes error on fetch failure", async () => {
71
+ const apiError = new Error("Connection refused");
72
+ const get = vi.fn().mockRejectedValue(apiError);
73
+ const client = createMockStigmer({ get });
134
74
 
135
- // Mount session B.
136
- const { unmount: unmountB } = renderHook(
137
- () => useSession("ses_B"),
138
- { wrapper },
139
- );
140
- await flush();
141
- unmountB();
75
+ const { result } = renderHook(() => useSession("ses-bad"), {
76
+ wrapper: wrapper(client),
77
+ });
142
78
 
143
- // Remount session A — should get A's cached data, not B's.
144
- const { result } = renderHook(
145
- () => useSession("ses_A"),
146
- { wrapper },
147
- );
79
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
148
80
 
149
- expect(result.current.isLoading).toBe(false);
150
- expect(result.current.session?.metadata?.id).toBe("ses_A");
81
+ expect(result.current.error).toBeTruthy();
82
+ expect(result.current.error!.message).toBe("Connection refused");
83
+ expect(result.current.session).toBeNull();
151
84
  });
152
85
 
153
- it("works without FetchCacheProvider (standard loading flow)", async () => {
154
- const session = makeSession("ses_1");
155
- const sessionGet = vi.fn().mockResolvedValue(session);
156
- const client = createMockStigmer(sessionGet);
157
-
158
- function NoCacheWrapper({ children }: { children: ReactNode }) {
159
- return (
160
- <StigmerContext.Provider value={client}>
161
- {children}
162
- </StigmerContext.Provider>
163
- );
164
- }
86
+ it("refetch triggers a new fetch", async () => {
87
+ const session = { metadata: { id: "ses-1" } };
88
+ const get = vi.fn().mockResolvedValue(session);
89
+ const client = createMockStigmer({ get });
165
90
 
166
- const { result } = renderHook(() => useSession("ses_1"), {
167
- wrapper: NoCacheWrapper,
91
+ const { result } = renderHook(() => useSession("ses-1"), {
92
+ wrapper: wrapper(client),
168
93
  });
169
94
 
170
- expect(result.current.isLoading).toBe(true);
171
- await flush();
95
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
96
+ expect(get).toHaveBeenCalledTimes(1);
97
+
98
+ act(() => result.current.refetch());
99
+
100
+ await waitFor(() => expect(get).toHaveBeenCalledTimes(2));
172
101
  expect(result.current.session).toBe(session);
173
102
  });
174
103
 
175
- it("null id skips fetching and caching", async () => {
176
- const sessionGet = vi.fn();
177
- const cache = new FetchCache();
178
- const wrapper = createWrapper(createMockStigmer(sessionGet), cache);
104
+ it("re-fetches when id changes", async () => {
105
+ const session1 = { metadata: { id: "ses-1" } };
106
+ const session2 = { metadata: { id: "ses-2" } };
107
+ const get = vi.fn()
108
+ .mockResolvedValueOnce(session1)
109
+ .mockResolvedValueOnce(session2);
110
+ const client = createMockStigmer({ get });
111
+
112
+ const { result, rerender } = renderHook(
113
+ ({ id }: { id: string }) => useSession(id),
114
+ {
115
+ wrapper: wrapper(client),
116
+ initialProps: { id: "ses-1" },
117
+ },
118
+ );
179
119
 
180
- const { result } = renderHook(() => useSession(null), { wrapper });
120
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
121
+ expect(result.current.session).toBe(session1);
181
122
 
182
- expect(result.current.isLoading).toBe(false);
183
- expect(result.current.session).toBeNull();
184
- expect(sessionGet).not.toHaveBeenCalled();
185
- expect(cache.size).toBe(0);
123
+ rerender({ id: "ses-2" });
124
+
125
+ await waitFor(() => expect(get).toHaveBeenCalledTimes(2));
126
+ expect(get).toHaveBeenLastCalledWith("ses-2");
186
127
  });
187
128
  });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { StigmerContext } from "../../context";
5
+ import { FetchCacheContext } from "../../internal/FetchCacheProvider";
6
+ import { useSessionList } from "../useSessionList";
7
+
8
+ function createMockStigmer(overrides: {
9
+ list?: (...args: unknown[]) => Promise<unknown>;
10
+ } = {}) {
11
+ return {
12
+ session: {
13
+ list: overrides.list ?? vi.fn().mockResolvedValue({ entries: [], totalPages: 0 }),
14
+ },
15
+ } as never;
16
+ }
17
+
18
+ function wrapper(client: unknown) {
19
+ return function Wrapper({ children }: { children: ReactNode }) {
20
+ return (
21
+ <FetchCacheContext.Provider value={null}>
22
+ <StigmerContext.Provider value={client as never}>
23
+ {children}
24
+ </StigmerContext.Provider>
25
+ </FetchCacheContext.Provider>
26
+ );
27
+ };
28
+ }
29
+
30
+ describe("useSessionList", () => {
31
+ beforeEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ it("fetches session list with default page size", async () => {
36
+ const sessions = [
37
+ { metadata: { id: "ses-1", name: "Session 1" } },
38
+ { metadata: { id: "ses-2", name: "Session 2" } },
39
+ ];
40
+ const list = vi.fn().mockResolvedValue({ entries: sessions, totalPages: 1 });
41
+ const client = createMockStigmer({ list });
42
+
43
+ const { result } = renderHook(() => useSessionList(), {
44
+ wrapper: wrapper(client),
45
+ });
46
+
47
+ expect(result.current.isLoading).toBe(true);
48
+ expect(result.current.sessions).toEqual([]);
49
+
50
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
51
+
52
+ expect(result.current.sessions).toHaveLength(2);
53
+ expect(result.current.error).toBeNull();
54
+ expect(list).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ it("passes custom page size to the API", async () => {
58
+ const list = vi.fn().mockResolvedValue({ entries: [], totalPages: 0 });
59
+ const client = createMockStigmer({ list });
60
+
61
+ renderHook(() => useSessionList({ pageSize: 10 }), {
62
+ wrapper: wrapper(client),
63
+ });
64
+
65
+ await waitFor(() => expect(list).toHaveBeenCalled());
66
+
67
+ const callArg = list.mock.calls[0][0];
68
+ expect(callArg.pageSize).toBe(10);
69
+ });
70
+
71
+ it("exposes error on fetch failure", async () => {
72
+ const apiError = new Error("Network failure");
73
+ const list = vi.fn().mockRejectedValue(apiError);
74
+ const client = createMockStigmer({ list });
75
+
76
+ const { result } = renderHook(() => useSessionList(), {
77
+ wrapper: wrapper(client),
78
+ });
79
+
80
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
81
+
82
+ expect(result.current.error).toBeTruthy();
83
+ expect(result.current.error!.message).toBe("Network failure");
84
+ expect(result.current.sessions).toEqual([]);
85
+ });
86
+ });
@@ -11,9 +11,12 @@ import { useWorkspaceEntries, type UseWorkspaceEntriesReturn } from "../workspac
11
11
  import { useSessionVariables, type UseSessionVariablesReturn } from "../execution/useSessionVariables";
12
12
  import type { SessionComposerSubmitContext } from "../composer";
13
13
  import { useRunnerList } from "../runner/useRunnerList";
14
+ import { withTimeout } from "../internal/withTimeout";
14
15
  import { useCreateSession } from "./useCreateSession";
15
16
  import { useCreateAgentExecution } from "../execution/useCreateAgentExecution";
16
17
 
18
+ const DEFAULT_AGENT_TIMEOUT_MS = 10_000;
19
+
17
20
  const STORAGE_KEY_HARNESS = "stigmer:session:harness";
18
21
  const STORAGE_KEY_RUNNER = "stigmer:session:runner";
19
22
 
@@ -164,6 +167,7 @@ export function useNewSessionFlow(
164
167
  agent: defaultAgent,
165
168
  isLoading: isDefaultAgentLoading,
166
169
  error: defaultAgentError,
170
+ waitForResolution: waitForDefaultAgent,
167
171
  } = useDefaultAgent(org);
168
172
  const workspace = useWorkspaceEntries();
169
173
  const sessionVariables = useSessionVariables();
@@ -293,25 +297,29 @@ export function useNewSessionFlow(
293
297
  }));
294
298
  }
295
299
  } else {
296
- const defaultInstanceId = defaultAgent?.status?.defaultInstanceId;
297
- if (!defaultInstanceId) {
300
+ let resolvedInstanceId = defaultAgent?.status?.defaultInstanceId;
301
+ if (!resolvedInstanceId) {
298
302
  if (isDefaultAgentLoading) {
303
+ const resolved = await withTimeout(
304
+ waitForDefaultAgent(),
305
+ DEFAULT_AGENT_TIMEOUT_MS,
306
+ "Default agent did not load in time. Please try again.",
307
+ );
308
+ resolvedInstanceId = resolved?.status?.defaultInstanceId;
309
+ } else if (defaultAgentError) {
299
310
  throw new Error(
300
- "Loading default agent. Please try again in a moment.",
311
+ "Failed to load default agent. Please try again.",
301
312
  );
302
313
  }
303
- if (defaultAgentError) {
314
+ if (!resolvedInstanceId) {
304
315
  throw new Error(
305
- "Failed to load default agent. Please try again.",
316
+ "No default agent available. Select an agent to start a session.",
306
317
  );
307
318
  }
308
- throw new Error(
309
- "No default agent available. Select an agent to start a session.",
310
- );
311
319
  }
312
320
  ({ sessionId } = await createSession({
313
321
  ...sessionFields,
314
- agentInstanceId: defaultInstanceId,
322
+ agentInstanceId: resolvedInstanceId,
315
323
  }));
316
324
  }
317
325
 
@@ -340,6 +348,7 @@ export function useNewSessionFlow(
340
348
  defaultAgent,
341
349
  isDefaultAgentLoading,
342
350
  defaultAgentError,
351
+ waitForDefaultAgent,
343
352
  createSession,
344
353
  createExecution,
345
354
  sessionVariables,