@frockbot/plugin-shell 0.1.4 → 0.2.1
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/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +19 -1
- package/src/backend-configuration.test.ts +166 -0
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-isolate.test.ts +198 -203
- package/src/backend-isolate.ts +96 -231
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +947 -206
- package/src/client/FrockBotApp.vue +38 -0
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +119 -0
- package/src/client/index.ts +78 -1
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/composition-views.ts +54 -0
- package/src/shared.ts +10 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type {
|
|
3
|
+
IsolateConnectionV1,
|
|
3
4
|
LlmStreamEvent,
|
|
4
5
|
NormalizedModelRequest,
|
|
5
6
|
} from "@frockbot/kernel-contracts";
|
|
@@ -9,28 +10,33 @@ import {
|
|
|
9
10
|
isolateModelEventStreamV1,
|
|
10
11
|
ISOLATE_MODEL_FAILURE_MESSAGE,
|
|
11
12
|
ISOLATE_MODEL_REQUEST_PREFIX,
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
matchesAdmittedConnectionV1,
|
|
14
|
+
matchingModelBindingV1,
|
|
14
15
|
type IsolateModelBindingV1,
|
|
15
16
|
type IsolateModelRequestRecordV1,
|
|
16
17
|
} from "./backend-isolate.ts";
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const CONNECTIONS: IsolateConnectionV1[] = [
|
|
20
|
+
{
|
|
21
|
+
connectionId: "connection-1",
|
|
22
|
+
packageId: "provider-ollama-cloud",
|
|
23
|
+
connectionTypeId: "ollama-cloud-account",
|
|
24
|
+
displayName: "Work",
|
|
25
|
+
generation: "generation-1",
|
|
26
|
+
safeMetadata: { region: "au" },
|
|
27
|
+
},
|
|
28
|
+
];
|
|
24
29
|
|
|
25
30
|
const BINDING: IsolateModelBindingV1 = {
|
|
26
|
-
packageId: "provider-ollama-cloud",
|
|
27
|
-
capabilityId: "ollama-cloud-models",
|
|
28
31
|
connectionId: "connection-1",
|
|
32
|
+
packageId: "provider-ollama-cloud",
|
|
29
33
|
provider: "ollama-cloud",
|
|
30
34
|
providerModelId: "glm-5.3-flash:cloud",
|
|
31
35
|
connectionGeneration: "generation-1",
|
|
32
36
|
};
|
|
33
37
|
|
|
38
|
+
const IDENTITY = { userId: "user-1", botId: "bot-1" } as const;
|
|
39
|
+
|
|
34
40
|
function request(
|
|
35
41
|
overrides: Partial<NormalizedModelRequest> = {},
|
|
36
42
|
): NormalizedModelRequest {
|
|
@@ -41,6 +47,10 @@ function request(
|
|
|
41
47
|
system: "",
|
|
42
48
|
messages: [{ role: "user", content: "hello" }],
|
|
43
49
|
tools: [],
|
|
50
|
+
modelBinding: {
|
|
51
|
+
connectionId: BINDING.connectionId,
|
|
52
|
+
connectionGeneration: BINDING.connectionGeneration,
|
|
53
|
+
},
|
|
44
54
|
...overrides,
|
|
45
55
|
};
|
|
46
56
|
}
|
|
@@ -53,7 +63,6 @@ function memoryStorage() {
|
|
|
53
63
|
values.set(key, structuredClone(value));
|
|
54
64
|
return Promise.resolve();
|
|
55
65
|
},
|
|
56
|
-
get: <T>(key: string) => Promise.resolve(values.get(key) as T | undefined),
|
|
57
66
|
list: <T>(options: { prefix: string }) =>
|
|
58
67
|
Promise.resolve(
|
|
59
68
|
new Map(
|
|
@@ -67,271 +76,259 @@ function memoryStorage() {
|
|
|
67
76
|
|
|
68
77
|
function host(
|
|
69
78
|
options: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
provider?: string;
|
|
74
|
-
providerModelId: string;
|
|
75
|
-
};
|
|
79
|
+
packageId?: string;
|
|
80
|
+
binding?: IsolateModelBindingV1;
|
|
81
|
+
withoutModel?: boolean;
|
|
76
82
|
stream?: (request: NormalizedModelRequest) => AsyncIterable<LlmStreamEvent>;
|
|
77
83
|
} = {},
|
|
78
84
|
) {
|
|
79
85
|
const storage = memoryStorage();
|
|
80
86
|
const forwarded: NormalizedModelRequest[] = [];
|
|
81
87
|
let minted = 0;
|
|
82
|
-
const binding = options.binding === undefined ? BINDING : options.binding;
|
|
83
88
|
return {
|
|
84
89
|
storage,
|
|
85
90
|
forwarded,
|
|
86
91
|
host: createIsolateCapabilityHost({
|
|
87
92
|
storage,
|
|
88
93
|
botId: "bot-1",
|
|
89
|
-
packageId: "bot-authored",
|
|
90
|
-
generationId: "
|
|
91
|
-
|
|
92
|
-
...(
|
|
94
|
+
packageId: options.packageId ?? "bot-authored",
|
|
95
|
+
generationId: "composition-1",
|
|
96
|
+
connections: CONNECTIONS,
|
|
97
|
+
...(!options.withoutModel
|
|
98
|
+
? { modelBinding: options.binding ?? BINDING }
|
|
99
|
+
: {}),
|
|
100
|
+
...(!options.withoutModel
|
|
93
101
|
? {
|
|
94
|
-
modelBinding: binding,
|
|
95
102
|
modelPath: {
|
|
96
103
|
stream: (value: NormalizedModelRequest) => {
|
|
97
104
|
forwarded.push(structuredClone(value));
|
|
98
105
|
return (
|
|
99
106
|
options.stream?.(value) ??
|
|
100
107
|
(async function* () {
|
|
101
|
-
yield {
|
|
102
|
-
type: "text-delta",
|
|
103
|
-
text: "hi",
|
|
104
|
-
} as LlmStreamEvent;
|
|
108
|
+
yield { type: "text-delta", text: "hi" } as LlmStreamEvent;
|
|
105
109
|
})()
|
|
106
110
|
);
|
|
107
111
|
},
|
|
108
112
|
},
|
|
109
113
|
}
|
|
110
114
|
: {}),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: {}),
|
|
115
|
+
memory: true,
|
|
116
|
+
workspace: true,
|
|
114
117
|
newId: () => `minted-${(minted += 1)}`,
|
|
115
118
|
now: () => new Date("2026-08-31T00:00:00.000Z"),
|
|
116
119
|
}),
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
describe("
|
|
121
|
-
test("
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
expect(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
describe("per-Bot isolate authority", () => {
|
|
124
|
+
test("two Packages of one Bot see the same Connections and capabilities", async () => {
|
|
125
|
+
const left = await host({ packageId: "package-left" }).host.list();
|
|
126
|
+
const right = await host({ packageId: "package-right" }).host.list();
|
|
127
|
+
expect(left).toEqual(right);
|
|
128
|
+
expect(left).toMatchObject({
|
|
129
|
+
status: "available",
|
|
130
|
+
connections: [
|
|
131
|
+
{ connectionId: "connection-1", generation: "generation-1" },
|
|
132
|
+
],
|
|
133
|
+
tools: true,
|
|
134
|
+
memory: true,
|
|
135
|
+
workspace: true,
|
|
136
|
+
notify: true,
|
|
137
|
+
schedule: true,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("matches only the Bot's configured provider and model", () => {
|
|
142
|
+
expect(matchingModelBindingV1(BINDING, request())).toEqual(BINDING);
|
|
132
143
|
expect(
|
|
133
|
-
|
|
134
|
-
[CAPABILITY],
|
|
135
|
-
BINDING,
|
|
136
|
-
request({ model: "some-other-model" }),
|
|
137
|
-
),
|
|
144
|
+
matchingModelBindingV1(BINDING, request({ provider: "foundation" })),
|
|
138
145
|
).toBeUndefined();
|
|
139
|
-
// No authoritative binding at all: an enabled Capability on its own authorizes
|
|
140
|
-
// nothing.
|
|
141
146
|
expect(
|
|
142
|
-
|
|
147
|
+
matchingModelBindingV1(BINDING, request({ model: "other" })),
|
|
143
148
|
).toBeUndefined();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("ignores a Bot-supplied model binding", () => {
|
|
147
149
|
expect(
|
|
148
|
-
|
|
149
|
-
[CAPABILITY],
|
|
150
|
+
matchingModelBindingV1(
|
|
150
151
|
BINDING,
|
|
151
152
|
request({
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
modelBinding: {
|
|
154
|
+
connectionId: BINDING.connectionId,
|
|
155
|
+
connectionGeneration: "generation-2",
|
|
156
|
+
},
|
|
155
157
|
}),
|
|
156
158
|
),
|
|
157
159
|
).toBeUndefined();
|
|
160
|
+
expect(matchingModelBindingV1(undefined, request())).toBeUndefined();
|
|
158
161
|
});
|
|
159
162
|
|
|
160
|
-
test("a
|
|
161
|
-
const subject = host();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
expect(outcome).toMatchObject({ status: "pending-user-decision" });
|
|
167
|
-
expect(await subject.host.recordedModelRequests()).toHaveLength(0);
|
|
168
|
-
expect(subject.forwarded).toHaveLength(0);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("a request with no durable model binding is a pending decision", async () => {
|
|
172
|
-
const subject = host({ binding: null, capabilities: [] });
|
|
173
|
-
|
|
174
|
-
const outcome = await subject.host.invokeModel(request());
|
|
175
|
-
|
|
176
|
-
expect(outcome).toMatchObject({ status: "pending-user-decision" });
|
|
177
|
-
expect(await subject.host.pendingDecisions()).toHaveLength(1);
|
|
163
|
+
test("a Bot with no configured model gets unavailable, never a pending decision", async () => {
|
|
164
|
+
const subject = host({ withoutModel: true });
|
|
165
|
+
await expect(subject.host.invokeModel(request())).resolves.toEqual({
|
|
166
|
+
status: "unavailable",
|
|
167
|
+
reason: "this Bot has no configured model",
|
|
168
|
+
});
|
|
178
169
|
expect(await subject.host.recordedModelRequests()).toHaveLength(0);
|
|
179
170
|
expect(subject.forwarded).toHaveLength(0);
|
|
180
171
|
});
|
|
181
172
|
|
|
182
|
-
test("a
|
|
183
|
-
const subject = host(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
test("a request that does not match the configured model is unavailable", async () => {
|
|
174
|
+
const subject = host();
|
|
175
|
+
await expect(
|
|
176
|
+
subject.host.invokeModel(
|
|
177
|
+
request({ provider: "foundation", model: "deterministic-v1" }),
|
|
178
|
+
),
|
|
179
|
+
).resolves.toEqual({
|
|
180
|
+
status: "unavailable",
|
|
181
|
+
reason: "the request does not match this Bot's configured model",
|
|
190
182
|
});
|
|
191
|
-
|
|
192
|
-
const outcome = await subject.host.invokeModel(request());
|
|
193
|
-
|
|
194
|
-
expect(outcome).toMatchObject({ status: "unavailable" });
|
|
195
|
-
expect(await subject.host.pendingDecisions()).toHaveLength(0);
|
|
196
183
|
expect(await subject.host.recordedModelRequests()).toHaveLength(0);
|
|
197
|
-
expect(subject.forwarded).toHaveLength(0);
|
|
198
184
|
});
|
|
199
185
|
|
|
200
|
-
test("
|
|
201
|
-
const subject = host(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
186
|
+
test("refuses a model binding outside the admitted snapshot", async () => {
|
|
187
|
+
const subject = host();
|
|
188
|
+
await expect(
|
|
189
|
+
subject.host.invokeModel(
|
|
190
|
+
request({
|
|
191
|
+
modelBinding: {
|
|
192
|
+
connectionId: "forged-connection",
|
|
193
|
+
connectionGeneration: "forged-generation",
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
197
|
+
).resolves.toEqual({
|
|
198
|
+
status: "unavailable",
|
|
199
|
+
reason: "the request does not match this Bot's configured model",
|
|
208
200
|
});
|
|
209
|
-
|
|
210
|
-
const outcome = await subject.host.invokeModel(
|
|
211
|
-
request({ provider: "foundation" }),
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
expect(outcome).toMatchObject({ status: "pending-user-decision" });
|
|
215
|
-
expect(await subject.host.pendingDecisions()).toHaveLength(1);
|
|
201
|
+
expect(subject.forwarded).toHaveLength(0);
|
|
216
202
|
});
|
|
217
203
|
|
|
218
|
-
test("
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
modelBinding: {
|
|
223
|
-
connectionId: "another-users-connection",
|
|
224
|
-
connectionGeneration: "forged",
|
|
225
|
-
},
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
expect(subject.forwarded[0]?.modelBinding).toEqual({
|
|
204
|
+
test("an old isolate cannot receive a newly added or regenerated Connection", () => {
|
|
205
|
+
const lease = {
|
|
206
|
+
status: "available" as const,
|
|
207
|
+
leaseId: "lease-1",
|
|
230
208
|
connectionId: "connection-1",
|
|
231
|
-
|
|
232
|
-
|
|
209
|
+
generation: "generation-1",
|
|
210
|
+
expiresAt: "2026-08-31T00:05:00.000Z",
|
|
211
|
+
};
|
|
212
|
+
expect(matchesAdmittedConnectionV1(CONNECTIONS[0], lease)).toBe(true);
|
|
213
|
+
expect(matchesAdmittedConnectionV1(undefined, lease)).toBe(false);
|
|
214
|
+
expect(
|
|
215
|
+
matchesAdmittedConnectionV1(CONNECTIONS[0], {
|
|
216
|
+
...lease,
|
|
217
|
+
generation: "generation-2",
|
|
218
|
+
}),
|
|
219
|
+
).toBe(false);
|
|
233
220
|
});
|
|
234
221
|
});
|
|
235
222
|
|
|
236
|
-
describe("
|
|
237
|
-
test("
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
223
|
+
describe("isolate binding digest", () => {
|
|
224
|
+
test("is order-independent for the same Bot authority", async () => {
|
|
225
|
+
const another = {
|
|
226
|
+
...CONNECTIONS[0]!,
|
|
227
|
+
connectionId: "connection-2",
|
|
228
|
+
generation: "generation-2",
|
|
229
|
+
};
|
|
230
|
+
const input = {
|
|
231
|
+
model: BINDING,
|
|
232
|
+
compositionGenerationId: "composition-1",
|
|
233
|
+
};
|
|
234
|
+
await expect(
|
|
235
|
+
isolateBindingDigestV1({
|
|
236
|
+
...IDENTITY,
|
|
237
|
+
...input,
|
|
238
|
+
connections: [CONNECTIONS[0]!, another],
|
|
239
|
+
}),
|
|
240
|
+
).resolves.toBe(
|
|
241
|
+
await isolateBindingDigestV1({
|
|
242
|
+
...IDENTITY,
|
|
243
|
+
...input,
|
|
244
|
+
connections: [another, CONNECTIONS[0]!],
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
243
247
|
});
|
|
244
248
|
|
|
245
|
-
test("
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
const same = await isolateBindingDigestV1({
|
|
253
|
-
userId: "user-1",
|
|
254
|
-
botId: "bot-1",
|
|
255
|
-
generationId: "generation-1",
|
|
256
|
-
capabilities: [structuredClone(CAPABILITY)],
|
|
257
|
-
});
|
|
249
|
+
test("changes with User and Bot identity", async () => {
|
|
250
|
+
const input = {
|
|
251
|
+
connections: CONNECTIONS,
|
|
252
|
+
model: BINDING,
|
|
253
|
+
compositionGenerationId: "composition-1",
|
|
254
|
+
};
|
|
255
|
+
const base = await isolateBindingDigestV1({ ...IDENTITY, ...input });
|
|
258
256
|
const otherUser = await isolateBindingDigestV1({
|
|
259
257
|
userId: "user-2",
|
|
260
|
-
botId:
|
|
261
|
-
|
|
262
|
-
capabilities: [CAPABILITY],
|
|
258
|
+
botId: IDENTITY.botId,
|
|
259
|
+
...input,
|
|
263
260
|
});
|
|
264
261
|
const otherBot = await isolateBindingDigestV1({
|
|
265
|
-
userId:
|
|
262
|
+
userId: IDENTITY.userId,
|
|
266
263
|
botId: "bot-2",
|
|
267
|
-
|
|
268
|
-
capabilities: [CAPABILITY],
|
|
264
|
+
...input,
|
|
269
265
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
266
|
+
|
|
267
|
+
expect(otherUser).not.toBe(base);
|
|
268
|
+
expect(otherBot).not.toBe(base);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("changes when a Connection is added, removed, or regenerated", async () => {
|
|
272
|
+
const base = await isolateBindingDigestV1({
|
|
273
|
+
...IDENTITY,
|
|
274
|
+
connections: CONNECTIONS,
|
|
275
|
+
model: BINDING,
|
|
276
|
+
compositionGenerationId: "composition-1",
|
|
275
277
|
});
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
CAPABILITY,
|
|
282
|
-
{
|
|
283
|
-
packageId: "clock",
|
|
284
|
-
capabilityId: "clock",
|
|
285
|
-
kind: "tool",
|
|
286
|
-
},
|
|
278
|
+
const added = await isolateBindingDigestV1({
|
|
279
|
+
...IDENTITY,
|
|
280
|
+
connections: [
|
|
281
|
+
...CONNECTIONS,
|
|
282
|
+
{ ...CONNECTIONS[0]!, connectionId: "connection-2" },
|
|
287
283
|
],
|
|
284
|
+
model: BINDING,
|
|
285
|
+
compositionGenerationId: "composition-1",
|
|
288
286
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
287
|
+
const removed = await isolateBindingDigestV1({
|
|
288
|
+
...IDENTITY,
|
|
289
|
+
connections: [],
|
|
290
|
+
model: BINDING,
|
|
291
|
+
compositionGenerationId: "composition-1",
|
|
292
|
+
});
|
|
293
|
+
const regenerated = await isolateBindingDigestV1({
|
|
294
|
+
...IDENTITY,
|
|
295
|
+
connections: [{ ...CONNECTIONS[0]!, generation: "generation-2" }],
|
|
296
|
+
model: BINDING,
|
|
297
|
+
compositionGenerationId: "composition-1",
|
|
298
|
+
});
|
|
299
|
+
expect(new Set([base, added, removed, regenerated]).size).toBe(4);
|
|
295
300
|
});
|
|
296
301
|
|
|
297
|
-
test("
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
await
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}),
|
|
318
|
-
);
|
|
302
|
+
test("changes with the model binding and Composition generation", async () => {
|
|
303
|
+
const base = await isolateBindingDigestV1({
|
|
304
|
+
...IDENTITY,
|
|
305
|
+
connections: CONNECTIONS,
|
|
306
|
+
model: BINDING,
|
|
307
|
+
compositionGenerationId: "composition-1",
|
|
308
|
+
});
|
|
309
|
+
const model = await isolateBindingDigestV1({
|
|
310
|
+
...IDENTITY,
|
|
311
|
+
connections: CONNECTIONS,
|
|
312
|
+
model: { ...BINDING, providerModelId: "other" },
|
|
313
|
+
compositionGenerationId: "composition-1",
|
|
314
|
+
});
|
|
315
|
+
const composition = await isolateBindingDigestV1({
|
|
316
|
+
...IDENTITY,
|
|
317
|
+
connections: CONNECTIONS,
|
|
318
|
+
model: BINDING,
|
|
319
|
+
compositionGenerationId: "composition-2",
|
|
320
|
+
});
|
|
321
|
+
expect(new Set([base, model, composition]).size).toBe(3);
|
|
319
322
|
});
|
|
320
323
|
});
|
|
321
324
|
|
|
322
325
|
describe("the isolate model request record", () => {
|
|
323
|
-
test("two invocations reusing one
|
|
326
|
+
test("two invocations reusing one request id produce two records", async () => {
|
|
324
327
|
const subject = host();
|
|
325
|
-
|
|
326
328
|
await subject.host.invokeModel(request());
|
|
327
329
|
await subject.host.invokeModel(request());
|
|
328
|
-
|
|
329
330
|
const recorded = await subject.host.recordedModelRequests();
|
|
330
331
|
expect(recorded).toHaveLength(2);
|
|
331
|
-
expect(recorded.map((entry) => entry.requestId)).toEqual([
|
|
332
|
-
"request-1",
|
|
333
|
-
"request-1",
|
|
334
|
-
]);
|
|
335
332
|
expect(new Set(recorded.map((entry) => entry.recordId)).size).toBe(2);
|
|
336
333
|
expect(
|
|
337
334
|
[...subject.storage.values.keys()].filter((key) =>
|
|
@@ -340,23 +337,22 @@ describe("the isolate model request record", () => {
|
|
|
340
337
|
).toHaveLength(2);
|
|
341
338
|
});
|
|
342
339
|
|
|
343
|
-
test("refuses an unbounded
|
|
344
|
-
const subject = host();
|
|
340
|
+
test("refuses an unbounded request id", async () => {
|
|
345
341
|
await expect(
|
|
346
|
-
|
|
342
|
+
host().host.invokeModel(request({ requestId: "r".repeat(257) })),
|
|
347
343
|
).rejects.toThrow("requestId is not bounded");
|
|
348
344
|
});
|
|
349
345
|
|
|
350
|
-
test("records the request that was forwarded", async () => {
|
|
351
|
-
const subject = host();
|
|
346
|
+
test("records the request that was forwarded and attributes the Package", async () => {
|
|
347
|
+
const subject = host({ packageId: "bot-authored" });
|
|
352
348
|
await subject.host.invokeModel(request());
|
|
353
349
|
const recorded = (await subject.host.recordedModelRequests())[0] as
|
|
354
350
|
IsolateModelRequestRecordV1 | undefined;
|
|
351
|
+
expect(recorded?.packageId).toBe("bot-authored");
|
|
355
352
|
expect(recorded?.request.modelBinding).toEqual({
|
|
356
353
|
connectionId: "connection-1",
|
|
357
354
|
connectionGeneration: "generation-1",
|
|
358
355
|
});
|
|
359
|
-
expect(recorded?.capabilityId).toBe("ollama-cloud-models");
|
|
360
356
|
});
|
|
361
357
|
});
|
|
362
358
|
|
|
@@ -372,7 +368,6 @@ describe("provider errors crossing into Bot code", () => {
|
|
|
372
368
|
);
|
|
373
369
|
const reader = stream.getReader();
|
|
374
370
|
await reader.read();
|
|
375
|
-
|
|
376
371
|
const failure = await reader.read().then(
|
|
377
372
|
() => undefined,
|
|
378
373
|
(error: unknown) => (error instanceof Error ? error.message : ""),
|