@stigmer/react 0.2.2 → 0.3.0
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.
- package/composer/ComposerToolbar.d.ts +5 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +6 -3
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +17 -1
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +32 -35
- package/composer/SessionComposer.js.map +1 -1
- package/execution/MessageEntry.d.ts +3 -1
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +30 -1
- package/execution/MessageEntry.js.map +1 -1
- package/github/index.d.ts +1 -1
- package/github/index.d.ts.map +1 -1
- package/github/index.js.map +1 -1
- package/github/useGitHubConnection.d.ts +70 -1
- package/github/useGitHubConnection.d.ts.map +1 -1
- package/github/useGitHubConnection.js +99 -20
- package/github/useGitHubConnection.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +19 -3
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/models/HarnessSelector.d.ts +41 -0
- package/models/HarnessSelector.d.ts.map +1 -0
- package/models/HarnessSelector.js +74 -0
- package/models/HarnessSelector.js.map +1 -0
- package/models/ModelSelector.d.ts +26 -16
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +128 -48
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/HarnessSelector.test.d.ts +2 -0
- package/models/__tests__/HarnessSelector.test.d.ts.map +1 -0
- package/models/__tests__/HarnessSelector.test.js +160 -0
- package/models/__tests__/HarnessSelector.test.js.map +1 -0
- package/models/__tests__/harness.test.d.ts +2 -0
- package/models/__tests__/harness.test.d.ts.map +1 -0
- package/models/__tests__/harness.test.js +50 -0
- package/models/__tests__/harness.test.js.map +1 -0
- package/models/__tests__/useModelRegistry.test.d.ts +2 -0
- package/models/__tests__/useModelRegistry.test.d.ts.map +1 -0
- package/models/__tests__/useModelRegistry.test.js +148 -0
- package/models/__tests__/useModelRegistry.test.js.map +1 -0
- package/models/harness.d.ts +21 -0
- package/models/harness.d.ts.map +1 -0
- package/models/harness.js +34 -0
- package/models/harness.js.map +1 -0
- package/models/index.d.ts +7 -2
- package/models/index.d.ts.map +1 -1
- package/models/index.js +3 -1
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +53 -13
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +51 -40
- package/models/registry.js.map +1 -1
- package/models/useModelRegistry.d.ts +39 -19
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +45 -23
- package/models/useModelRegistry.js.map +1 -1
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +23 -2
- package/organization/OrgProfilePanel.js.map +1 -1
- package/package.json +4 -4
- package/runner/RunnerFileBrowser.d.ts +11 -1
- package/runner/RunnerFileBrowser.d.ts.map +1 -1
- package/runner/RunnerFileBrowser.js +70 -7
- package/runner/RunnerFileBrowser.js.map +1 -1
- package/runner/RunnerListPanel.js +2 -1
- package/runner/RunnerListPanel.js.map +1 -1
- package/runner/WorkspaceRunnerSelector.d.ts +36 -0
- package/runner/WorkspaceRunnerSelector.d.ts.map +1 -0
- package/runner/WorkspaceRunnerSelector.js +63 -0
- package/runner/WorkspaceRunnerSelector.js.map +1 -0
- package/runner/__tests__/phase.test.js +6 -2
- package/runner/__tests__/phase.test.js.map +1 -1
- package/runner/index.d.ts +2 -0
- package/runner/index.d.ts.map +1 -1
- package/runner/index.js +1 -0
- package/runner/index.js.map +1 -1
- package/runner/phase.d.ts +9 -7
- package/runner/phase.d.ts.map +1 -1
- package/runner/phase.js +18 -12
- package/runner/phase.js.map +1 -1
- package/runner/useRunnerFileBrowser.d.ts.map +1 -1
- package/runner/useRunnerFileBrowser.js +26 -2
- package/runner/useRunnerFileBrowser.js.map +1 -1
- package/session/__tests__/useCreateSession.test.d.ts +2 -0
- package/session/__tests__/useCreateSession.test.d.ts.map +1 -0
- package/session/__tests__/useCreateSession.test.js +232 -0
- package/session/__tests__/useCreateSession.test.js.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.d.ts +2 -0
- package/session/__tests__/useNewSessionFlow.test.d.ts.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.js +199 -0
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -0
- package/session/__tests__/useSessionConversation.test.js +37 -0
- package/session/__tests__/useSessionConversation.test.js.map +1 -1
- package/session/index.d.ts +1 -1
- package/session/index.d.ts.map +1 -1
- package/session/useCreateSession.d.ts +8 -0
- package/session/useCreateSession.d.ts.map +1 -1
- package/session/useCreateSession.js +2 -0
- package/session/useCreateSession.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +6 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +34 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/usePersistedModel.d.ts +16 -1
- package/session/usePersistedModel.d.ts.map +1 -1
- package/session/usePersistedModel.js +15 -6
- package/session/usePersistedModel.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +6 -1
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +11 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +11 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/settings/MembersSection.d.ts.map +1 -1
- package/settings/MembersSection.js +7 -2
- package/settings/MembersSection.js.map +1 -1
- package/src/composer/ComposerToolbar.tsx +24 -1
- package/src/composer/SessionComposer.tsx +81 -44
- package/src/execution/MessageEntry.tsx +134 -1
- package/src/github/index.ts +1 -0
- package/src/github/useGitHubConnection.ts +162 -22
- package/src/identity-provider/IdentityProviderWizard.tsx +112 -3
- package/src/index.ts +16 -1
- package/src/models/HarnessSelector.tsx +130 -0
- package/src/models/ModelSelector.tsx +285 -81
- package/src/models/__tests__/HarnessSelector.test.tsx +190 -0
- package/src/models/__tests__/harness.test.ts +66 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +209 -0
- package/src/models/harness.ts +45 -0
- package/src/models/index.ts +7 -2
- package/src/models/registry.ts +122 -50
- package/src/models/useModelRegistry.ts +74 -24
- package/src/organization/OrgProfilePanel.tsx +98 -0
- package/src/runner/RunnerFileBrowser.tsx +227 -8
- package/src/runner/RunnerListPanel.tsx +13 -5
- package/src/runner/WorkspaceRunnerSelector.tsx +180 -0
- package/src/runner/__tests__/phase.test.ts +6 -2
- package/src/runner/index.ts +3 -0
- package/src/runner/phase.ts +18 -12
- package/src/runner/useRunnerFileBrowser.ts +39 -3
- package/src/session/__tests__/useCreateSession.test.tsx +296 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +258 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
- package/src/session/index.ts +1 -1
- package/src/session/useCreateSession.ts +9 -0
- package/src/session/useNewSessionFlow.ts +46 -9
- package/src/session/usePersistedModel.ts +30 -6
- package/src/session/useSessionConversation.ts +6 -1
- package/src/session/useSessionPageFlow.ts +26 -2
- package/src/settings/MembersSection.tsx +23 -1
- package/src/workspace/WorkspaceEditor.tsx +176 -126
- package/src/workspace/index.ts +5 -0
- package/src/workspace/useRecentWorkspaces.ts +162 -0
- package/src/workspace/useWorkspaceEntries.ts +13 -0
- package/styles.css +1 -1
- package/workspace/WorkspaceEditor.d.ts +25 -22
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +64 -43
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/index.d.ts +2 -0
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -0
- package/workspace/index.js.map +1 -1
- package/workspace/useRecentWorkspaces.d.ts +31 -0
- package/workspace/useRecentWorkspaces.d.ts.map +1 -0
- package/workspace/useRecentWorkspaces.js +117 -0
- package/workspace/useRecentWorkspaces.js.map +1 -0
- package/workspace/useWorkspaceEntries.d.ts +8 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js +4 -0
- package/workspace/useWorkspaceEntries.js.map +1 -1
package/src/runner/phase.ts
CHANGED
|
@@ -9,15 +9,17 @@ import { RunnerPhase } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/enum_p
|
|
|
9
9
|
export const PHASE_SORT_ORDER: Record<RunnerPhase, number> = {
|
|
10
10
|
[RunnerPhase.READY]: 0,
|
|
11
11
|
[RunnerPhase.BUSY]: 1,
|
|
12
|
-
[RunnerPhase.
|
|
13
|
-
[RunnerPhase.
|
|
14
|
-
[RunnerPhase.
|
|
15
|
-
[RunnerPhase.
|
|
12
|
+
[RunnerPhase.STARTING]: 2,
|
|
13
|
+
[RunnerPhase.PENDING]: 3,
|
|
14
|
+
[RunnerPhase.STOPPED]: 4,
|
|
15
|
+
[RunnerPhase.FAILED]: 5,
|
|
16
|
+
[RunnerPhase.UNSPECIFIED]: 6,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const LABELS: Record<RunnerPhase, string> = {
|
|
19
20
|
[RunnerPhase.READY]: "Ready",
|
|
20
21
|
[RunnerPhase.BUSY]: "Busy",
|
|
22
|
+
[RunnerPhase.STARTING]: "Starting",
|
|
21
23
|
[RunnerPhase.PENDING]: "Pending",
|
|
22
24
|
[RunnerPhase.STOPPED]: "Stopped",
|
|
23
25
|
[RunnerPhase.FAILED]: "Failed",
|
|
@@ -37,9 +39,10 @@ export function phaseLabel(phase: RunnerPhase): string {
|
|
|
37
39
|
/**
|
|
38
40
|
* Tailwind `bg-*` class for the small colored dot indicator.
|
|
39
41
|
*
|
|
40
|
-
* - Ready
|
|
41
|
-
* - Busy
|
|
42
|
-
* -
|
|
42
|
+
* - Ready → `bg-success` (green)
|
|
43
|
+
* - Busy → `bg-warning` (amber)
|
|
44
|
+
* - Starting → `bg-primary` (blue)
|
|
45
|
+
* - Others → `bg-muted-foreground` (neutral)
|
|
43
46
|
*/
|
|
44
47
|
export function phaseDotColor(phase: RunnerPhase): string {
|
|
45
48
|
switch (phase) {
|
|
@@ -47,6 +50,8 @@ export function phaseDotColor(phase: RunnerPhase): string {
|
|
|
47
50
|
return "bg-success";
|
|
48
51
|
case RunnerPhase.BUSY:
|
|
49
52
|
return "bg-warning";
|
|
53
|
+
case RunnerPhase.STARTING:
|
|
54
|
+
return "bg-primary";
|
|
50
55
|
default:
|
|
51
56
|
return "bg-muted-foreground";
|
|
52
57
|
}
|
|
@@ -65,14 +70,15 @@ export function isActivePhase(phase: RunnerPhase): boolean {
|
|
|
65
70
|
* Whether the runner is in a transitional phase whose status is
|
|
66
71
|
* expected to change soon without user action.
|
|
67
72
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
73
|
+
* `PENDING` — resource created, no process launched yet.
|
|
74
|
+
* `STARTING` — process launched, runtime bootstrapping in progress.
|
|
75
|
+
*
|
|
76
|
+
* This is distinct from {@link isActivePhase} (READY | BUSY) where
|
|
77
|
+
* the runner is stable and accepting work.
|
|
72
78
|
*
|
|
73
79
|
* Useful for driving conditional polling: poll while transitional
|
|
74
80
|
* runners exist, stop when all runners reach a stable state.
|
|
75
81
|
*/
|
|
76
82
|
export function isTransitionalPhase(phase: RunnerPhase): boolean {
|
|
77
|
-
return phase === RunnerPhase.PENDING;
|
|
83
|
+
return phase === RunnerPhase.PENDING || phase === RunnerPhase.STARTING;
|
|
78
84
|
}
|
|
@@ -58,6 +58,12 @@ export interface UseRunnerFileBrowserReturn {
|
|
|
58
58
|
// Reducer
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
|
|
61
|
+
/** Cached result of a single directory listing. */
|
|
62
|
+
interface CachedListing {
|
|
63
|
+
readonly entries: readonly DirectoryEntry[];
|
|
64
|
+
readonly fetchedAt: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
interface State {
|
|
62
68
|
currentPath: string;
|
|
63
69
|
entries: readonly DirectoryEntry[];
|
|
@@ -68,13 +74,18 @@ interface State {
|
|
|
68
74
|
error: Error | null;
|
|
69
75
|
/** The path we last requested — used for retry. */
|
|
70
76
|
requestedPath: string;
|
|
77
|
+
/** Directory listing cache keyed by resolved absolute path. */
|
|
78
|
+
cache: Map<string, CachedListing>;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
type Action =
|
|
74
82
|
| { type: "NAVIGATE"; path: string }
|
|
75
83
|
| { type: "SUCCESS"; resolvedPath: string; entries: DirectoryEntry[]; homeDirectory: string; currentDirectory: string }
|
|
76
84
|
| { type: "FAILURE"; error: Error }
|
|
77
|
-
| { type: "TOGGLE_HIDDEN" }
|
|
85
|
+
| { type: "TOGGLE_HIDDEN" }
|
|
86
|
+
| { type: "CACHE_HIT"; resolvedPath: string; entries: readonly DirectoryEntry[] };
|
|
87
|
+
|
|
88
|
+
const CACHE_MAX_AGE_MS = 30_000;
|
|
78
89
|
|
|
79
90
|
function reducer(state: State, action: Action): State {
|
|
80
91
|
switch (action.type) {
|
|
@@ -85,7 +96,12 @@ function reducer(state: State, action: Action): State {
|
|
|
85
96
|
isLoading: true,
|
|
86
97
|
error: null,
|
|
87
98
|
};
|
|
88
|
-
case "SUCCESS":
|
|
99
|
+
case "SUCCESS": {
|
|
100
|
+
const nextCache = new Map(state.cache);
|
|
101
|
+
nextCache.set(action.resolvedPath, {
|
|
102
|
+
entries: action.entries,
|
|
103
|
+
fetchedAt: Date.now(),
|
|
104
|
+
});
|
|
89
105
|
return {
|
|
90
106
|
...state,
|
|
91
107
|
currentPath: action.resolvedPath,
|
|
@@ -94,6 +110,17 @@ function reducer(state: State, action: Action): State {
|
|
|
94
110
|
currentDirectory: action.currentDirectory || state.currentDirectory,
|
|
95
111
|
isLoading: false,
|
|
96
112
|
error: null,
|
|
113
|
+
cache: nextCache,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case "CACHE_HIT":
|
|
117
|
+
return {
|
|
118
|
+
...state,
|
|
119
|
+
currentPath: action.resolvedPath,
|
|
120
|
+
entries: action.entries,
|
|
121
|
+
isLoading: false,
|
|
122
|
+
error: null,
|
|
123
|
+
requestedPath: action.resolvedPath,
|
|
97
124
|
};
|
|
98
125
|
case "FAILURE":
|
|
99
126
|
return { ...state, isLoading: false, error: action.error };
|
|
@@ -111,6 +138,7 @@ const INITIAL_STATE: State = {
|
|
|
111
138
|
isLoading: false,
|
|
112
139
|
error: null,
|
|
113
140
|
requestedPath: "",
|
|
141
|
+
cache: new Map(),
|
|
114
142
|
};
|
|
115
143
|
|
|
116
144
|
// ---------------------------------------------------------------------------
|
|
@@ -182,10 +210,19 @@ export function useRunnerFileBrowser(
|
|
|
182
210
|
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
|
|
183
211
|
const requestIdRef = useRef(0);
|
|
184
212
|
|
|
213
|
+
const cacheRef = useRef(state.cache);
|
|
214
|
+
cacheRef.current = state.cache;
|
|
215
|
+
|
|
185
216
|
const fetchDirectory = useCallback(
|
|
186
217
|
async (path: string) => {
|
|
187
218
|
if (!runnerId) return;
|
|
188
219
|
|
|
220
|
+
const cached = cacheRef.current.get(path);
|
|
221
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_MAX_AGE_MS) {
|
|
222
|
+
dispatch({ type: "CACHE_HIT", resolvedPath: path, entries: cached.entries });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
189
226
|
const id = ++requestIdRef.current;
|
|
190
227
|
dispatch({ type: "NAVIGATE", path });
|
|
191
228
|
|
|
@@ -200,7 +237,6 @@ export function useRunnerFileBrowser(
|
|
|
200
237
|
}),
|
|
201
238
|
);
|
|
202
239
|
|
|
203
|
-
// Stale response guard — a newer navigation has started.
|
|
204
240
|
if (id !== requestIdRef.current) return;
|
|
205
241
|
|
|
206
242
|
if (response.result.case === "error") {
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
5
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
6
|
+
import { StigmerContext } from "../../context";
|
|
7
|
+
import { useCreateSession } from "../useCreateSession";
|
|
8
|
+
|
|
9
|
+
function buildMockClient(overrides: {
|
|
10
|
+
sessionCreate?: ReturnType<typeof vi.fn>;
|
|
11
|
+
agentGetByReference?: ReturnType<typeof vi.fn>;
|
|
12
|
+
} = {}) {
|
|
13
|
+
return {
|
|
14
|
+
session: {
|
|
15
|
+
create: overrides.sessionCreate ?? vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
agent: {
|
|
18
|
+
getByReference: overrides.agentGetByReference ?? vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
} as unknown as Stigmer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeWrapper(client: Stigmer) {
|
|
24
|
+
return ({ children }: { children: ReactNode }) => (
|
|
25
|
+
<StigmerContext.Provider value={client}>{children}</StigmerContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fakeSessionResponse(sessionId: string) {
|
|
30
|
+
return { metadata: { id: sessionId } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("useCreateSession", () => {
|
|
34
|
+
let sessionCreateMock: ReturnType<typeof vi.fn>;
|
|
35
|
+
let agentGetByRefMock: ReturnType<typeof vi.fn>;
|
|
36
|
+
let client: Stigmer;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
sessionCreateMock = vi.fn();
|
|
40
|
+
agentGetByRefMock = vi.fn();
|
|
41
|
+
client = buildMockClient({
|
|
42
|
+
sessionCreate: sessionCreateMock,
|
|
43
|
+
agentGetByReference: agentGetByRefMock,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("starts in idle state", () => {
|
|
52
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
53
|
+
wrapper: makeWrapper(client),
|
|
54
|
+
});
|
|
55
|
+
expect(result.current.isCreating).toBe(false);
|
|
56
|
+
expect(result.current.error).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("agentInstanceId path", () => {
|
|
60
|
+
it("creates a session with the given instance ID", async () => {
|
|
61
|
+
sessionCreateMock.mockResolvedValueOnce(fakeSessionResponse("sess-1"));
|
|
62
|
+
|
|
63
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
64
|
+
wrapper: makeWrapper(client),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let outcome: { sessionId: string } | undefined;
|
|
68
|
+
await act(async () => {
|
|
69
|
+
outcome = await result.current.create({
|
|
70
|
+
org: "acme",
|
|
71
|
+
agentInstanceId: "inst-abc",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(outcome!.sessionId).toBe("sess-1");
|
|
76
|
+
expect(sessionCreateMock).toHaveBeenCalledOnce();
|
|
77
|
+
expect(sessionCreateMock.mock.calls[0][0]).toMatchObject({
|
|
78
|
+
org: "acme",
|
|
79
|
+
agentInstanceId: "inst-abc",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("agentRef path", () => {
|
|
85
|
+
it("resolves the agent reference to its default instance", async () => {
|
|
86
|
+
agentGetByRefMock.mockResolvedValueOnce({
|
|
87
|
+
status: { defaultInstanceId: "inst-resolved" },
|
|
88
|
+
});
|
|
89
|
+
sessionCreateMock.mockResolvedValueOnce(fakeSessionResponse("sess-2"));
|
|
90
|
+
|
|
91
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
92
|
+
wrapper: makeWrapper(client),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
await result.current.create({
|
|
97
|
+
org: "acme",
|
|
98
|
+
agentRef: { org: "acme", slug: "my-agent" },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(agentGetByRefMock).toHaveBeenCalledWith({
|
|
103
|
+
org: "acme",
|
|
104
|
+
slug: "my-agent",
|
|
105
|
+
});
|
|
106
|
+
expect(sessionCreateMock.mock.calls[0][0]).toMatchObject({
|
|
107
|
+
agentInstanceId: "inst-resolved",
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("throws when agent has no default instance", async () => {
|
|
112
|
+
agentGetByRefMock.mockResolvedValueOnce({ status: {} });
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
115
|
+
wrapper: makeWrapper(client),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await act(async () => {
|
|
119
|
+
await expect(
|
|
120
|
+
result.current.create({
|
|
121
|
+
org: "acme",
|
|
122
|
+
agentRef: { org: "acme", slug: "no-instance" },
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow("does not have a default instance");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(result.current.error).not.toBeNull();
|
|
128
|
+
expect(result.current.error!.message).toContain(
|
|
129
|
+
"does not have a default instance",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("harness proto conversion", () => {
|
|
135
|
+
it("passes Harness.CURSOR when harness is cursor", async () => {
|
|
136
|
+
sessionCreateMock.mockResolvedValueOnce(fakeSessionResponse("sess-h1"));
|
|
137
|
+
|
|
138
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
139
|
+
wrapper: makeWrapper(client),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await act(async () => {
|
|
143
|
+
await result.current.create({
|
|
144
|
+
org: "acme",
|
|
145
|
+
agentInstanceId: "inst-1",
|
|
146
|
+
harness: "cursor",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(sessionCreateMock.mock.calls[0][0].harness).toBe(Harness.CURSOR);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("passes Harness.NATIVE when harness is native", async () => {
|
|
154
|
+
sessionCreateMock.mockResolvedValueOnce(fakeSessionResponse("sess-h2"));
|
|
155
|
+
|
|
156
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
157
|
+
wrapper: makeWrapper(client),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
await result.current.create({
|
|
162
|
+
org: "acme",
|
|
163
|
+
agentInstanceId: "inst-1",
|
|
164
|
+
harness: "native",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(sessionCreateMock.mock.calls[0][0].harness).toBe(Harness.NATIVE);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("passes undefined when harness is omitted", async () => {
|
|
172
|
+
sessionCreateMock.mockResolvedValueOnce(fakeSessionResponse("sess-h3"));
|
|
173
|
+
|
|
174
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
175
|
+
wrapper: makeWrapper(client),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await act(async () => {
|
|
179
|
+
await result.current.create({
|
|
180
|
+
org: "acme",
|
|
181
|
+
agentInstanceId: "inst-1",
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(sessionCreateMock.mock.calls[0][0].harness).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("loading state lifecycle", () => {
|
|
190
|
+
it("isCreating is true during the RPC and false after", async () => {
|
|
191
|
+
let resolveCreate!: (v: unknown) => void;
|
|
192
|
+
sessionCreateMock.mockReturnValueOnce(
|
|
193
|
+
new Promise((r) => { resolveCreate = r; }),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
197
|
+
wrapper: makeWrapper(client),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let createPromise: Promise<unknown>;
|
|
201
|
+
act(() => {
|
|
202
|
+
createPromise = result.current.create({
|
|
203
|
+
org: "acme",
|
|
204
|
+
agentInstanceId: "inst-1",
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(result.current.isCreating).toBe(true);
|
|
209
|
+
|
|
210
|
+
await act(async () => {
|
|
211
|
+
resolveCreate(fakeSessionResponse("sess-lc"));
|
|
212
|
+
await createPromise;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.current.isCreating).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("error handling", () => {
|
|
220
|
+
it("sets error on RPC failure and resets isCreating", async () => {
|
|
221
|
+
sessionCreateMock.mockRejectedValueOnce(new Error("network timeout"));
|
|
222
|
+
|
|
223
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
224
|
+
wrapper: makeWrapper(client),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await act(async () => {
|
|
228
|
+
await expect(
|
|
229
|
+
result.current.create({
|
|
230
|
+
org: "acme",
|
|
231
|
+
agentInstanceId: "inst-1",
|
|
232
|
+
}),
|
|
233
|
+
).rejects.toThrow("network timeout");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result.current.isCreating).toBe(false);
|
|
237
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
238
|
+
expect(result.current.error!.message).toBe("network timeout");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("clearError resets error to null", async () => {
|
|
242
|
+
sessionCreateMock.mockRejectedValueOnce(new Error("fail"));
|
|
243
|
+
|
|
244
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
245
|
+
wrapper: makeWrapper(client),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await act(async () => {
|
|
249
|
+
try {
|
|
250
|
+
await result.current.create({
|
|
251
|
+
org: "acme",
|
|
252
|
+
agentInstanceId: "inst-1",
|
|
253
|
+
});
|
|
254
|
+
} catch { /* expected */ }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(result.current.error).not.toBeNull();
|
|
258
|
+
|
|
259
|
+
act(() => {
|
|
260
|
+
result.current.clearError();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(result.current.error).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("clears previous error on new create attempt", async () => {
|
|
267
|
+
sessionCreateMock
|
|
268
|
+
.mockRejectedValueOnce(new Error("first fail"))
|
|
269
|
+
.mockResolvedValueOnce(fakeSessionResponse("sess-retry"));
|
|
270
|
+
|
|
271
|
+
const { result } = renderHook(() => useCreateSession(), {
|
|
272
|
+
wrapper: makeWrapper(client),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await act(async () => {
|
|
276
|
+
try {
|
|
277
|
+
await result.current.create({
|
|
278
|
+
org: "acme",
|
|
279
|
+
agentInstanceId: "inst-1",
|
|
280
|
+
});
|
|
281
|
+
} catch { /* expected */ }
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(result.current.error).not.toBeNull();
|
|
285
|
+
|
|
286
|
+
await act(async () => {
|
|
287
|
+
await result.current.create({
|
|
288
|
+
org: "acme",
|
|
289
|
+
agentInstanceId: "inst-1",
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(result.current.error).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|