@intx/hub-sessions 0.1.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/LICENSE +176 -0
- package/README.md +82 -1
- package/dist/agent-repo.d.ts +93 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +189 -0
- package/dist/asset-service.d.ts +104 -0
- package/dist/asset-service.js +267 -0
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +33 -0
- package/dist/credential-push.js +109 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +365 -0
- package/dist/hub-session-lookups.d.ts +135 -0
- package/dist/hub-session-lookups.js +663 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1829 -0
- package/dist/repo-store/subscribe-kind.d.ts +56 -0
- package/dist/repo-store/subscribe-kind.js +144 -0
- package/dist/repo-store/types.d.ts +571 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +416 -0
- package/dist/session-service.js +1191 -0
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +292 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +37 -0
- package/dist/workflow-kind.js +310 -0
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +440 -0
- package/dist/workflow-run-kind.js +2916 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +153 -0
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +222 -0
- package/dist/ws/sidecar-events.js +72 -0
- package/dist/ws/sidecar-handler.d.ts +305 -0
- package/dist/ws/sidecar-handler.js +2537 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
- package/dist/ws/sidecar-token-authenticator.js +81 -0
- package/package.json +35 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,895 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import type { CryptoProvider, HarnessConfig } from "@intx/types/runtime";
|
|
4
|
-
import type { AgentRepoStore, DeployContent } from "./agent-repo";
|
|
5
|
-
import type { AgentAssetWithAsset, AssetService } from "./asset-service";
|
|
6
|
-
import type { Principal, RepoId, RepoStore } from "./repo-store";
|
|
7
|
-
import {
|
|
8
|
-
createSessionService,
|
|
9
|
-
SessionLaunchError,
|
|
10
|
-
type UserMessageParams,
|
|
11
|
-
} from "./session-service";
|
|
12
|
-
import { skillKindHandler } from "./skill-kind";
|
|
13
|
-
import type { SendPackOptions, SidecarRouter } from "./ws/sidecar-handler";
|
|
14
|
-
import { createSidecarEmitter } from "./ws/sidecar-events";
|
|
15
|
-
|
|
16
|
-
type Call = { method: string; args: unknown[] };
|
|
17
|
-
|
|
18
|
-
function createMockRouter(): SidecarRouter & {
|
|
19
|
-
calls: Call[];
|
|
20
|
-
routeMailResult: boolean;
|
|
21
|
-
} {
|
|
22
|
-
const calls: Call[] = [];
|
|
23
|
-
const track =
|
|
24
|
-
(method: string) =>
|
|
25
|
-
(...args: unknown[]) => {
|
|
26
|
-
calls.push({ method, args });
|
|
27
|
-
return Promise.resolve();
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// track() returns a generic variadic function; each SidecarRouter method has
|
|
31
|
-
// a specific typed signature. The casts below are unavoidable given the
|
|
32
|
-
// generic tracker design — each method's parameter types cannot be inferred.
|
|
33
|
-
const mock: SidecarRouter & {
|
|
34
|
-
calls: Call[];
|
|
35
|
-
routeMailResult: boolean;
|
|
36
|
-
} = {
|
|
37
|
-
calls,
|
|
38
|
-
routeMailResult: true,
|
|
39
|
-
handleOpen: track("handleOpen") as SidecarRouter["handleOpen"],
|
|
40
|
-
handleMessage: track("handleMessage") as SidecarRouter["handleMessage"],
|
|
41
|
-
handleClose: track("handleClose") as SidecarRouter["handleClose"],
|
|
42
|
-
routeMail(agentAddress: string, rawMessage: string): boolean {
|
|
43
|
-
calls.push({ method: "routeMail", args: [agentAddress, rawMessage] });
|
|
44
|
-
return mock.routeMailResult;
|
|
45
|
-
},
|
|
46
|
-
sendAgentDeploy: track(
|
|
47
|
-
"sendAgentDeploy",
|
|
48
|
-
) as SidecarRouter["sendAgentDeploy"],
|
|
49
|
-
sendAgentUndeploy: track(
|
|
50
|
-
"sendAgentUndeploy",
|
|
51
|
-
) as SidecarRouter["sendAgentUndeploy"],
|
|
52
|
-
sendSessionStart: track(
|
|
53
|
-
"sendSessionStart",
|
|
54
|
-
) as SidecarRouter["sendSessionStart"],
|
|
55
|
-
sendSessionAbort: track(
|
|
56
|
-
"sendSessionAbort",
|
|
57
|
-
) as SidecarRouter["sendSessionAbort"],
|
|
58
|
-
sendGrantsUpdate: track(
|
|
59
|
-
"sendGrantsUpdate",
|
|
60
|
-
) as SidecarRouter["sendGrantsUpdate"],
|
|
61
|
-
sendSourcesUpdate: track(
|
|
62
|
-
"sendSourcesUpdate",
|
|
63
|
-
) as SidecarRouter["sendSourcesUpdate"],
|
|
64
|
-
sendPack: ((
|
|
65
|
-
agentAddress: string,
|
|
66
|
-
pack: Uint8Array,
|
|
67
|
-
ref: string,
|
|
68
|
-
commitSha: string,
|
|
69
|
-
options?: SendPackOptions,
|
|
70
|
-
) => {
|
|
71
|
-
calls.push({
|
|
72
|
-
method: "sendPack",
|
|
73
|
-
args: [agentAddress, pack, ref, commitSha, options],
|
|
74
|
-
});
|
|
75
|
-
return Promise.resolve();
|
|
76
|
-
}) as SidecarRouter["sendPack"],
|
|
77
|
-
sendSyncRequest: track(
|
|
78
|
-
"sendSyncRequest",
|
|
79
|
-
) as SidecarRouter["sendSyncRequest"],
|
|
80
|
-
subscribeAgent: (() => () => undefined) as SidecarRouter["subscribeAgent"],
|
|
81
|
-
dispatchAgentEvent: () => undefined,
|
|
82
|
-
getConnectedSidecars: () => [],
|
|
83
|
-
getRoutableAddresses: () => [],
|
|
84
|
-
getConnectorState: () => null,
|
|
85
|
-
events: createSidecarEmitter(),
|
|
86
|
-
};
|
|
87
|
-
return mock;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function createMockRepoStore(): AgentRepoStore & { calls: Call[] } {
|
|
91
|
-
const calls: Call[] = [];
|
|
92
|
-
return {
|
|
93
|
-
calls,
|
|
94
|
-
async writeDeployTree(agentId: string, content: DeployContent) {
|
|
95
|
-
calls.push({ method: "writeDeployTree", args: [agentId, content] });
|
|
96
|
-
return { commitSha: "abc123" + "0".repeat(34) };
|
|
97
|
-
},
|
|
98
|
-
async createDeployPack(agentId: string) {
|
|
99
|
-
calls.push({ method: "createDeployPack", args: [agentId] });
|
|
100
|
-
return {
|
|
101
|
-
pack: new Uint8Array([1, 2, 3]),
|
|
102
|
-
commitSha: "abc123" + "0".repeat(34),
|
|
103
|
-
ref: "refs/heads/deploy",
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
async receiveStatePack(
|
|
107
|
-
repoId: { kind: "agent-state"; id: string },
|
|
108
|
-
_pack: Uint8Array,
|
|
109
|
-
_ref: string,
|
|
110
|
-
_commitSha: string,
|
|
111
|
-
) {
|
|
112
|
-
calls.push({ method: "receiveStatePack", args: [repoId.id] });
|
|
113
|
-
},
|
|
114
|
-
getSigningPublicKey() {
|
|
115
|
-
return new Uint8Array(32);
|
|
116
|
-
},
|
|
117
|
-
getDeployRef: (_agentId: string) => Promise.resolve(null),
|
|
118
|
-
repoStore: unusedRepoStore(),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function unusedRepoStore(): RepoStore {
|
|
123
|
-
// SessionService tests never exercise the substrate; the inner
|
|
124
|
-
// store is only present because AgentRepoStore exposes it. A typed
|
|
125
|
-
// throwing stub keeps the surface honest without dragging in a
|
|
126
|
-
// tmpdir-backed real store.
|
|
127
|
-
const unused = () =>
|
|
128
|
-
Promise.reject(new Error("mock AgentRepoStore.repoStore is not wired"));
|
|
129
|
-
return {
|
|
130
|
-
initRepo: unused,
|
|
131
|
-
writeTree: unused,
|
|
132
|
-
receivePack: unused,
|
|
133
|
-
createPack: unused,
|
|
134
|
-
resolveRef: unused,
|
|
135
|
-
listRefs: unused,
|
|
136
|
-
resolveHead: unused,
|
|
137
|
-
getRepoDir: () => {
|
|
138
|
-
throw new Error("mock AgentRepoStore.repoStore is not wired");
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
type FakeAssetPackEntry = {
|
|
144
|
-
pack: Uint8Array;
|
|
145
|
-
commitSha: string;
|
|
146
|
-
ref: string;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
function createFakeRepoStore(
|
|
150
|
-
packsByAssetId: Map<string, FakeAssetPackEntry>,
|
|
151
|
-
): RepoStore & {
|
|
152
|
-
resolveRefCalls: { repoId: RepoId; ref: string }[];
|
|
153
|
-
createPackCalls: { repoId: RepoId; ref: string }[];
|
|
154
|
-
} {
|
|
155
|
-
const resolveRefCalls: { repoId: RepoId; ref: string }[] = [];
|
|
156
|
-
const createPackCalls: { repoId: RepoId; ref: string }[] = [];
|
|
157
|
-
const unused = () =>
|
|
158
|
-
Promise.reject(new Error("repoStore method not wired in fake"));
|
|
159
|
-
return {
|
|
160
|
-
initRepo: unused,
|
|
161
|
-
writeTree: unused,
|
|
162
|
-
receivePack: unused,
|
|
163
|
-
listRefs: unused,
|
|
164
|
-
resolveHead: unused,
|
|
165
|
-
getRepoDir: () => {
|
|
166
|
-
throw new Error("repoStore method not wired in fake");
|
|
167
|
-
},
|
|
168
|
-
async resolveRef(_principal: Principal, repoId: RepoId, ref: string) {
|
|
169
|
-
resolveRefCalls.push({ repoId, ref });
|
|
170
|
-
const entry = packsByAssetId.get(repoId.id);
|
|
171
|
-
if (entry === undefined) return null;
|
|
172
|
-
return entry.commitSha;
|
|
173
|
-
},
|
|
174
|
-
async createPack(_principal: Principal, repoId: RepoId, ref: string) {
|
|
175
|
-
createPackCalls.push({ repoId, ref });
|
|
176
|
-
const entry = packsByAssetId.get(repoId.id);
|
|
177
|
-
if (entry === undefined) {
|
|
178
|
-
throw new Error(`no fake pack registered for ${repoId.id}`);
|
|
179
|
-
}
|
|
180
|
-
return {
|
|
181
|
-
pack: entry.pack,
|
|
182
|
-
commitSha: entry.commitSha,
|
|
183
|
-
ref: entry.ref,
|
|
184
|
-
};
|
|
185
|
-
},
|
|
186
|
-
resolveRefCalls,
|
|
187
|
-
createPackCalls,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function createFakeAssetService(
|
|
192
|
-
attachments: AgentAssetWithAsset[],
|
|
193
|
-
): AssetService {
|
|
194
|
-
return {
|
|
195
|
-
createAsset: () => {
|
|
196
|
-
throw new Error("not used");
|
|
197
|
-
},
|
|
198
|
-
populateAsset: () => {
|
|
199
|
-
throw new Error("not used");
|
|
200
|
-
},
|
|
201
|
-
attachAsset: () => {
|
|
202
|
-
throw new Error("not used");
|
|
203
|
-
},
|
|
204
|
-
listAgentAssets: async (_agentId: string) => attachments,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
type CapturedSessionAssetRow = {
|
|
209
|
-
instanceId: string;
|
|
210
|
-
agentAssetId: string;
|
|
211
|
-
mountPath: string;
|
|
212
|
-
assetPackSha: string;
|
|
213
|
-
sourceCommitSha: string;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
function createFakeDb(captured: CapturedSessionAssetRow[]) {
|
|
217
|
-
// The session-service calls `db.insert(sessionAssetTable).values(row)` on
|
|
218
|
-
// the happy path and `db.delete(sessionAssetTable).where(...)` on the
|
|
219
|
-
// rollback path when sendPack fails. Both are no-ops here aside from
|
|
220
|
-
// recording the inserts; the delete just resolves so the catch handler
|
|
221
|
-
// can rethrow without secondary errors.
|
|
222
|
-
const builder = {
|
|
223
|
-
values(row: CapturedSessionAssetRow) {
|
|
224
|
-
captured.push(row);
|
|
225
|
-
return Promise.resolve();
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
return {
|
|
229
|
-
insert(_table: unknown) {
|
|
230
|
-
return builder;
|
|
231
|
-
},
|
|
232
|
-
delete(_table: unknown) {
|
|
233
|
-
return {
|
|
234
|
-
where(_predicate: unknown) {
|
|
235
|
-
return Promise.resolve();
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const AGENT_ADDRESS = "agent-1@test.local";
|
|
243
|
-
const AGENT_ID = "agent-1";
|
|
244
|
-
const INSTANCE_ID = "instance-1";
|
|
245
|
-
|
|
246
|
-
const MOCK_CONFIG: HarnessConfig = {
|
|
247
|
-
sessionId: "ses-1",
|
|
248
|
-
agentId: AGENT_ID,
|
|
249
|
-
tenantId: "tenant-1",
|
|
250
|
-
principalId: "prin-1",
|
|
251
|
-
agentAddress: AGENT_ADDRESS,
|
|
252
|
-
systemPrompt: "Test",
|
|
253
|
-
tools: [],
|
|
254
|
-
grants: [],
|
|
255
|
-
sources: [],
|
|
256
|
-
defaultSource: "",
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const MOCK_CONTENT: DeployContent = {
|
|
260
|
-
systemPrompt: "Test",
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
describe("SessionService", () => {
|
|
264
|
-
let router: ReturnType<typeof createMockRouter>;
|
|
265
|
-
let repoStore: ReturnType<typeof createMockRepoStore>;
|
|
266
|
-
|
|
267
|
-
beforeEach(() => {
|
|
268
|
-
router = createMockRouter();
|
|
269
|
-
repoStore = createMockRepoStore();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("launchSession calls steps in order", async () => {
|
|
273
|
-
const service = createSessionService({
|
|
274
|
-
sidecarRouter: router,
|
|
275
|
-
agentRepoStore: repoStore,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
await service.launchSession({
|
|
279
|
-
agentAddress: AGENT_ADDRESS,
|
|
280
|
-
agentId: AGENT_ID,
|
|
281
|
-
instanceId: INSTANCE_ID,
|
|
282
|
-
config: MOCK_CONFIG,
|
|
283
|
-
deployContent: MOCK_CONTENT,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const methods = [
|
|
287
|
-
...repoStore.calls.map((c) => c.method),
|
|
288
|
-
...router.calls.map((c) => c.method),
|
|
289
|
-
];
|
|
290
|
-
|
|
291
|
-
expect(methods).toEqual([
|
|
292
|
-
"writeDeployTree",
|
|
293
|
-
"createDeployPack",
|
|
294
|
-
"sendAgentDeploy",
|
|
295
|
-
"sendPack",
|
|
296
|
-
"sendSessionStart",
|
|
297
|
-
]);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("launchSession cleans up on pack failure", async () => {
|
|
301
|
-
router.sendPack = () => Promise.reject(new Error("pack failed"));
|
|
302
|
-
|
|
303
|
-
const service = createSessionService({
|
|
304
|
-
sidecarRouter: router,
|
|
305
|
-
agentRepoStore: repoStore,
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const err = await service
|
|
309
|
-
.launchSession({
|
|
310
|
-
agentAddress: AGENT_ADDRESS,
|
|
311
|
-
agentId: AGENT_ID,
|
|
312
|
-
instanceId: INSTANCE_ID,
|
|
313
|
-
config: MOCK_CONFIG,
|
|
314
|
-
deployContent: MOCK_CONTENT,
|
|
315
|
-
})
|
|
316
|
-
.catch((e: unknown) => e);
|
|
317
|
-
|
|
318
|
-
expect(err).toBeInstanceOf(SessionLaunchError);
|
|
319
|
-
if (!(err instanceof SessionLaunchError)) throw new Error("unreachable");
|
|
320
|
-
expect(err.phase).toBe("pack");
|
|
321
|
-
expect(err.leakedAgent).toBe(false);
|
|
322
|
-
|
|
323
|
-
const routerMethods = router.calls.map((c) => c.method);
|
|
324
|
-
expect(routerMethods).toContain("sendAgentUndeploy");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test("launchSession cleans up on session start failure", async () => {
|
|
328
|
-
router.sendSessionStart = () => Promise.reject(new Error("start failed"));
|
|
329
|
-
|
|
330
|
-
const service = createSessionService({
|
|
331
|
-
sidecarRouter: router,
|
|
332
|
-
agentRepoStore: repoStore,
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const err = await service
|
|
336
|
-
.launchSession({
|
|
337
|
-
agentAddress: AGENT_ADDRESS,
|
|
338
|
-
agentId: AGENT_ID,
|
|
339
|
-
instanceId: INSTANCE_ID,
|
|
340
|
-
config: MOCK_CONFIG,
|
|
341
|
-
deployContent: MOCK_CONTENT,
|
|
342
|
-
})
|
|
343
|
-
.catch((e: unknown) => e);
|
|
344
|
-
|
|
345
|
-
expect(err).toBeInstanceOf(SessionLaunchError);
|
|
346
|
-
if (!(err instanceof SessionLaunchError)) throw new Error("unreachable");
|
|
347
|
-
expect(err.phase).toBe("start");
|
|
348
|
-
expect(err.leakedAgent).toBe(false);
|
|
349
|
-
|
|
350
|
-
const routerMethods = router.calls.map((c) => c.method);
|
|
351
|
-
expect(routerMethods).toContain("sendAgentUndeploy");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test("launchSession reports leaked agent when cleanup fails", async () => {
|
|
355
|
-
router.sendPack = () => Promise.reject(new Error("pack failed"));
|
|
356
|
-
router.sendAgentUndeploy = () =>
|
|
357
|
-
Promise.reject(new Error("cleanup failed"));
|
|
358
|
-
|
|
359
|
-
const service = createSessionService({
|
|
360
|
-
sidecarRouter: router,
|
|
361
|
-
agentRepoStore: repoStore,
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const err = await service
|
|
365
|
-
.launchSession({
|
|
366
|
-
agentAddress: AGENT_ADDRESS,
|
|
367
|
-
agentId: AGENT_ID,
|
|
368
|
-
instanceId: INSTANCE_ID,
|
|
369
|
-
config: MOCK_CONFIG,
|
|
370
|
-
deployContent: MOCK_CONTENT,
|
|
371
|
-
})
|
|
372
|
-
.catch((e: unknown) => e);
|
|
373
|
-
|
|
374
|
-
expect(err).toBeInstanceOf(SessionLaunchError);
|
|
375
|
-
if (!(err instanceof SessionLaunchError)) throw new Error("unreachable");
|
|
376
|
-
expect(err.leakedAgent).toBe(true);
|
|
377
|
-
expect(err.phase).toBe("pack");
|
|
378
|
-
|
|
379
|
-
// The original error (pack failure) must be preserved as the cause,
|
|
380
|
-
// not the cleanup failure.
|
|
381
|
-
expect(err.cause).toBeInstanceOf(Error);
|
|
382
|
-
if (!(err.cause instanceof Error)) throw new Error("unreachable");
|
|
383
|
-
expect(err.cause.message).toBe("pack failed");
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
test("launchSession does not provision on write failure", async () => {
|
|
387
|
-
repoStore.writeDeployTree = () => Promise.reject(new Error("write failed"));
|
|
388
|
-
|
|
389
|
-
const service = createSessionService({
|
|
390
|
-
sidecarRouter: router,
|
|
391
|
-
agentRepoStore: repoStore,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const err = await service
|
|
395
|
-
.launchSession({
|
|
396
|
-
agentAddress: AGENT_ADDRESS,
|
|
397
|
-
agentId: AGENT_ID,
|
|
398
|
-
instanceId: INSTANCE_ID,
|
|
399
|
-
config: MOCK_CONFIG,
|
|
400
|
-
deployContent: MOCK_CONTENT,
|
|
401
|
-
})
|
|
402
|
-
.catch((e: unknown) => e);
|
|
403
|
-
|
|
404
|
-
expect(err).toBeInstanceOf(SessionLaunchError);
|
|
405
|
-
if (!(err instanceof SessionLaunchError)) throw new Error("unreachable");
|
|
406
|
-
expect(err.phase).toBe("write");
|
|
407
|
-
expect(err.leakedAgent).toBe(false);
|
|
408
|
-
expect(router.calls.length).toBe(0);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test("launchSession does not send pack on provision failure", async () => {
|
|
412
|
-
router.sendAgentDeploy = () =>
|
|
413
|
-
Promise.reject(new Error("provision failed"));
|
|
414
|
-
|
|
415
|
-
const service = createSessionService({
|
|
416
|
-
sidecarRouter: router,
|
|
417
|
-
agentRepoStore: repoStore,
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const err = await service
|
|
421
|
-
.launchSession({
|
|
422
|
-
agentAddress: AGENT_ADDRESS,
|
|
423
|
-
agentId: AGENT_ID,
|
|
424
|
-
instanceId: INSTANCE_ID,
|
|
425
|
-
config: MOCK_CONFIG,
|
|
426
|
-
deployContent: MOCK_CONTENT,
|
|
427
|
-
})
|
|
428
|
-
.catch((e: unknown) => e);
|
|
429
|
-
|
|
430
|
-
expect(err).toBeInstanceOf(SessionLaunchError);
|
|
431
|
-
if (!(err instanceof SessionLaunchError)) throw new Error("unreachable");
|
|
432
|
-
expect(err.phase).toBe("provision");
|
|
433
|
-
expect(err.leakedAgent).toBe(false);
|
|
434
|
-
|
|
435
|
-
const routerMethods = router.calls.map((c) => c.method);
|
|
436
|
-
expect(routerMethods).not.toContain("sendPack");
|
|
437
|
-
expect(routerMethods).not.toContain("sendAgentUndeploy");
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("endSession awaits undeploy ack", async () => {
|
|
441
|
-
const service = createSessionService({
|
|
442
|
-
sidecarRouter: router,
|
|
443
|
-
agentRepoStore: repoStore,
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
await service.endSession(AGENT_ADDRESS, "test_end");
|
|
447
|
-
|
|
448
|
-
expect(router.calls.length).toBe(1);
|
|
449
|
-
const call = router.calls[0];
|
|
450
|
-
if (call === undefined) throw new Error("unreachable");
|
|
451
|
-
expect(call.method).toBe("sendAgentUndeploy");
|
|
452
|
-
expect(call.args).toEqual([AGENT_ADDRESS, "test_end"]);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
// --- sendUserMessage tests ---
|
|
456
|
-
|
|
457
|
-
function mockCryptoProvider(): CryptoProvider {
|
|
458
|
-
const fakeSig = new Uint8Array(64);
|
|
459
|
-
fakeSig.fill(0xab);
|
|
460
|
-
return {
|
|
461
|
-
sign: async (_data: Uint8Array) => fakeSig,
|
|
462
|
-
signSSH: async () => "unused-in-this-test",
|
|
463
|
-
verify: async () => true,
|
|
464
|
-
getPublicKey: () => new Uint8Array(32),
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function userMessageParams(
|
|
469
|
-
overrides?: Partial<UserMessageParams>,
|
|
470
|
-
): UserMessageParams {
|
|
471
|
-
return {
|
|
472
|
-
agentAddress: AGENT_ADDRESS,
|
|
473
|
-
from: "user@test.local",
|
|
474
|
-
messageId: "<msg-1@test.local>",
|
|
475
|
-
date: new Date("2026-01-15T12:00:00Z"),
|
|
476
|
-
content: "Hello agent",
|
|
477
|
-
sessionId: "ses-1",
|
|
478
|
-
tenantId: "tenant-1",
|
|
479
|
-
cryptoProvider: mockCryptoProvider(),
|
|
480
|
-
...overrides,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
test("sendUserMessage calls routeMail with base64 MIME", async () => {
|
|
485
|
-
const service = createSessionService({
|
|
486
|
-
sidecarRouter: router,
|
|
487
|
-
agentRepoStore: repoStore,
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
await service.sendUserMessage(userMessageParams());
|
|
491
|
-
|
|
492
|
-
const mailCalls = router.calls.filter((c) => c.method === "routeMail");
|
|
493
|
-
expect(mailCalls.length).toBe(1);
|
|
494
|
-
const call = mailCalls[0];
|
|
495
|
-
if (call === undefined) throw new Error("unreachable");
|
|
496
|
-
expect(call.args[0]).toBe(AGENT_ADDRESS);
|
|
497
|
-
|
|
498
|
-
const rawArg = call.args[1];
|
|
499
|
-
if (typeof rawArg !== "string") throw new Error("expected string arg");
|
|
500
|
-
const decoded = Buffer.from(rawArg, "base64").toString("utf-8");
|
|
501
|
-
expect(decoded).toContain("From: user@test.local");
|
|
502
|
-
expect(decoded).toContain(`To: ${AGENT_ADDRESS}`);
|
|
503
|
-
expect(decoded).toContain("Message-ID: <msg-1@test.local>");
|
|
504
|
-
expect(decoded).toContain("Interchange-Session-ID: ses-1");
|
|
505
|
-
expect(decoded).toContain("Interchange-Tenant-ID: tenant-1");
|
|
506
|
-
expect(decoded).toContain("Hello agent");
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("sendUserMessage includes threading headers", async () => {
|
|
510
|
-
const service = createSessionService({
|
|
511
|
-
sidecarRouter: router,
|
|
512
|
-
agentRepoStore: repoStore,
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
await service.sendUserMessage(
|
|
516
|
-
userMessageParams({
|
|
517
|
-
inReplyTo: "<prev@test.local>",
|
|
518
|
-
references: ["<root@test.local>", "<prev@test.local>"],
|
|
519
|
-
}),
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
const call = router.calls.find((c) => c.method === "routeMail");
|
|
523
|
-
if (call === undefined) throw new Error("unreachable");
|
|
524
|
-
|
|
525
|
-
const rawArg1 = call.args[1];
|
|
526
|
-
if (typeof rawArg1 !== "string") throw new Error("expected string arg");
|
|
527
|
-
const decoded = Buffer.from(rawArg1, "base64").toString("utf-8");
|
|
528
|
-
expect(decoded).toContain("In-Reply-To: <prev@test.local>");
|
|
529
|
-
expect(decoded).toContain(
|
|
530
|
-
"References: <root@test.local> <prev@test.local>",
|
|
531
|
-
);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test("sendUserMessage throws when agent is unreachable", async () => {
|
|
535
|
-
router.routeMailResult = false;
|
|
536
|
-
|
|
537
|
-
const service = createSessionService({
|
|
538
|
-
sidecarRouter: router,
|
|
539
|
-
agentRepoStore: repoStore,
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
const err = await service
|
|
543
|
-
.sendUserMessage(userMessageParams())
|
|
544
|
-
.catch((e: unknown) => e);
|
|
545
|
-
|
|
546
|
-
expect(err).toBeInstanceOf(Error);
|
|
547
|
-
if (!(err instanceof Error)) throw new Error("unreachable");
|
|
548
|
-
expect(err.message).toContain("unreachable");
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("sendUserMessage propagates signing failure", async () => {
|
|
552
|
-
const badProvider: CryptoProvider = {
|
|
553
|
-
sign: async () => {
|
|
554
|
-
throw new Error("signing failed");
|
|
555
|
-
},
|
|
556
|
-
signSSH: async () => {
|
|
557
|
-
throw new Error("unreachable in this test");
|
|
558
|
-
},
|
|
559
|
-
verify: async () => true,
|
|
560
|
-
getPublicKey: () => new Uint8Array(32),
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
const service = createSessionService({
|
|
564
|
-
sidecarRouter: router,
|
|
565
|
-
agentRepoStore: repoStore,
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const err = await service
|
|
569
|
-
.sendUserMessage(userMessageParams({ cryptoProvider: badProvider }))
|
|
570
|
-
.catch((e: unknown) => e);
|
|
571
|
-
|
|
572
|
-
expect(err).toBeInstanceOf(Error);
|
|
573
|
-
if (!(err instanceof Error)) throw new Error("unreachable");
|
|
574
|
-
expect(err.message).toBe("signing failed");
|
|
575
|
-
expect(router.calls.filter((c) => c.method === "routeMail").length).toBe(0);
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
// ---------------------------------------------------------------------
|
|
579
|
-
// Attachment fan-out
|
|
580
|
-
// ---------------------------------------------------------------------
|
|
581
|
-
|
|
582
|
-
function makeAttachment(overrides: {
|
|
583
|
-
id: string;
|
|
584
|
-
assetId: string;
|
|
585
|
-
name: string;
|
|
586
|
-
ref?: string;
|
|
587
|
-
}): AgentAssetWithAsset {
|
|
588
|
-
return {
|
|
589
|
-
id: overrides.id,
|
|
590
|
-
agentId: AGENT_ID,
|
|
591
|
-
assetId: overrides.assetId,
|
|
592
|
-
ref: overrides.ref ?? "refs/heads/main",
|
|
593
|
-
accessMode: "read-only",
|
|
594
|
-
createdAt: new Date(),
|
|
595
|
-
asset: {
|
|
596
|
-
id: overrides.assetId,
|
|
597
|
-
tenantId: "tenant-1",
|
|
598
|
-
kind: "skill",
|
|
599
|
-
name: overrides.name,
|
|
600
|
-
displayName: null,
|
|
601
|
-
},
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
test("launchSession fans out attachment packs and inserts manifest rows", async () => {
|
|
606
|
-
const packsByAssetId = new Map<string, FakeAssetPackEntry>([
|
|
607
|
-
[
|
|
608
|
-
"ast_greet",
|
|
609
|
-
{
|
|
610
|
-
pack: new Uint8Array([10, 11, 12]),
|
|
611
|
-
commitSha: "c".repeat(40),
|
|
612
|
-
ref: "refs/heads/main",
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
[
|
|
616
|
-
"ast_search",
|
|
617
|
-
{
|
|
618
|
-
pack: new Uint8Array([20, 21, 22, 23]),
|
|
619
|
-
commitSha: "d".repeat(40),
|
|
620
|
-
ref: "refs/heads/main",
|
|
621
|
-
},
|
|
622
|
-
],
|
|
623
|
-
]);
|
|
624
|
-
const fakeRepoStore = createFakeRepoStore(packsByAssetId);
|
|
625
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- replace the empty unusedRepoStore with the resolving fake for this test
|
|
626
|
-
(repoStore as unknown as { repoStore: RepoStore }).repoStore =
|
|
627
|
-
fakeRepoStore;
|
|
628
|
-
|
|
629
|
-
const attachments = [
|
|
630
|
-
makeAttachment({ id: "aas_greet", assetId: "ast_greet", name: "greet" }),
|
|
631
|
-
makeAttachment({
|
|
632
|
-
id: "aas_search",
|
|
633
|
-
assetId: "ast_search",
|
|
634
|
-
name: "search",
|
|
635
|
-
}),
|
|
636
|
-
];
|
|
637
|
-
|
|
638
|
-
const captured: CapturedSessionAssetRow[] = [];
|
|
639
|
-
const service = createSessionService({
|
|
640
|
-
sidecarRouter: router,
|
|
641
|
-
agentRepoStore: repoStore,
|
|
642
|
-
assetService: createFakeAssetService(attachments),
|
|
643
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DB stub satisfies the narrow surface session-service actually calls (insert().values())
|
|
644
|
-
db: createFakeDb(captured) as unknown as NonNullable<
|
|
645
|
-
Parameters<typeof createSessionService>[0]["db"]
|
|
646
|
-
>,
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
await service.launchSession({
|
|
650
|
-
agentAddress: AGENT_ADDRESS,
|
|
651
|
-
agentId: AGENT_ID,
|
|
652
|
-
instanceId: INSTANCE_ID,
|
|
653
|
-
config: MOCK_CONFIG,
|
|
654
|
-
deployContent: MOCK_CONTENT,
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
expect(captured).toHaveLength(2);
|
|
658
|
-
|
|
659
|
-
const greetRow = captured.find((r) => r.agentAssetId === "aas_greet");
|
|
660
|
-
if (greetRow === undefined) throw new Error("greet row missing");
|
|
661
|
-
expect(greetRow.mountPath).toBe("skills/greet/");
|
|
662
|
-
expect(greetRow.sourceCommitSha).toBe("c".repeat(40));
|
|
663
|
-
expect(greetRow.instanceId).toBe(INSTANCE_ID);
|
|
664
|
-
expect(greetRow.assetPackSha).toBe(
|
|
665
|
-
createHash("sha256")
|
|
666
|
-
.update(new Uint8Array([10, 11, 12]))
|
|
667
|
-
.digest("hex"),
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
const searchRow = captured.find((r) => r.agentAssetId === "aas_search");
|
|
671
|
-
if (searchRow === undefined) throw new Error("search row missing");
|
|
672
|
-
expect(searchRow.mountPath).toBe("skills/search/");
|
|
673
|
-
expect(searchRow.sourceCommitSha).toBe("d".repeat(40));
|
|
674
|
-
|
|
675
|
-
const packCalls = router.calls.filter((c) => c.method === "sendPack");
|
|
676
|
-
// 1 deploy pack + 2 attachment packs
|
|
677
|
-
expect(packCalls).toHaveLength(3);
|
|
678
|
-
const attachmentPackCalls = packCalls.slice(1);
|
|
679
|
-
const opts0 = attachmentPackCalls[0]?.args[4];
|
|
680
|
-
const opts1 = attachmentPackCalls[1]?.args[4];
|
|
681
|
-
expect(opts0).toEqual({
|
|
682
|
-
mountPath: "skills/greet/",
|
|
683
|
-
repoId: { kind: "skill", id: "ast_greet" },
|
|
684
|
-
});
|
|
685
|
-
expect(opts1).toEqual({
|
|
686
|
-
mountPath: "skills/search/",
|
|
687
|
-
repoId: { kind: "skill", id: "ast_search" },
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
test("launchSession appends the available_skills stanza to deploy prompt before writeDeployTree", async () => {
|
|
692
|
-
const assetGreet = "ast_skill_greet_" + Math.random().toString(36).slice(2);
|
|
693
|
-
const assetSearch =
|
|
694
|
-
"ast_skill_search_" + Math.random().toString(36).slice(2);
|
|
695
|
-
|
|
696
|
-
// Seed the skill index for both assets by driving the kind
|
|
697
|
-
// handler's push lifecycle directly. The substrate runs
|
|
698
|
-
// validatePush then onRefUpdated in the same write; we mirror
|
|
699
|
-
// that ordering here.
|
|
700
|
-
async function seedSkillIndex(
|
|
701
|
-
assetId: string,
|
|
702
|
-
skills: { name: string; description: string }[],
|
|
703
|
-
): Promise<void> {
|
|
704
|
-
const ref = "refs/heads/main";
|
|
705
|
-
const repoId: RepoId = { kind: "skill", id: assetId };
|
|
706
|
-
const files: Record<string, string> = {};
|
|
707
|
-
for (const s of skills) {
|
|
708
|
-
files[`${s.name}/SKILL.md`] =
|
|
709
|
-
`---\nname: ${s.name}\ndescription: ${s.description}\n---\nbody\n`;
|
|
710
|
-
}
|
|
711
|
-
const readBlob = async (p: string): Promise<Uint8Array> => {
|
|
712
|
-
const body = files[p];
|
|
713
|
-
if (body === undefined) throw new Error(`missing ${p}`);
|
|
714
|
-
return new TextEncoder().encode(body);
|
|
715
|
-
};
|
|
716
|
-
const result = await skillKindHandler.validatePush({
|
|
717
|
-
repoId,
|
|
718
|
-
ref,
|
|
719
|
-
topLevelTreePaths: skills.map((s) => s.name),
|
|
720
|
-
readBlob,
|
|
721
|
-
});
|
|
722
|
-
if (!result.ok) {
|
|
723
|
-
throw new Error(`validatePush failed: ${result.reason}`);
|
|
724
|
-
}
|
|
725
|
-
await skillKindHandler.onRefUpdated({
|
|
726
|
-
repoId,
|
|
727
|
-
ref,
|
|
728
|
-
oldSha: null,
|
|
729
|
-
newSha: "a".repeat(40),
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
await seedSkillIndex(assetGreet, [
|
|
734
|
-
{ name: "wave", description: "Waves at the user." },
|
|
735
|
-
{ name: "bow", description: "Bows formally with A & B." },
|
|
736
|
-
]);
|
|
737
|
-
await seedSkillIndex(assetSearch, [
|
|
738
|
-
{ name: "wave", description: "Searches for waves." },
|
|
739
|
-
]);
|
|
740
|
-
|
|
741
|
-
const packsByAssetId = new Map<string, FakeAssetPackEntry>([
|
|
742
|
-
[
|
|
743
|
-
assetGreet,
|
|
744
|
-
{
|
|
745
|
-
pack: new Uint8Array([10, 11, 12]),
|
|
746
|
-
commitSha: "c".repeat(40),
|
|
747
|
-
ref: "refs/heads/main",
|
|
748
|
-
},
|
|
749
|
-
],
|
|
750
|
-
[
|
|
751
|
-
assetSearch,
|
|
752
|
-
{
|
|
753
|
-
pack: new Uint8Array([20, 21, 22]),
|
|
754
|
-
commitSha: "d".repeat(40),
|
|
755
|
-
ref: "refs/heads/main",
|
|
756
|
-
},
|
|
757
|
-
],
|
|
758
|
-
]);
|
|
759
|
-
const fakeRepoStore = createFakeRepoStore(packsByAssetId);
|
|
760
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- replace the empty unusedRepoStore with the resolving fake for this test
|
|
761
|
-
(repoStore as unknown as { repoStore: RepoStore }).repoStore =
|
|
762
|
-
fakeRepoStore;
|
|
763
|
-
|
|
764
|
-
const attachments = [
|
|
765
|
-
makeAttachment({
|
|
766
|
-
id: "aas_greet",
|
|
767
|
-
assetId: assetGreet,
|
|
768
|
-
name: "greeter",
|
|
769
|
-
}),
|
|
770
|
-
makeAttachment({
|
|
771
|
-
id: "aas_search",
|
|
772
|
-
assetId: assetSearch,
|
|
773
|
-
name: "searcher",
|
|
774
|
-
}),
|
|
775
|
-
];
|
|
776
|
-
|
|
777
|
-
const captured: CapturedSessionAssetRow[] = [];
|
|
778
|
-
const service = createSessionService({
|
|
779
|
-
sidecarRouter: router,
|
|
780
|
-
agentRepoStore: repoStore,
|
|
781
|
-
assetService: createFakeAssetService(attachments),
|
|
782
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DB stub satisfies the narrow surface session-service actually calls (insert().values())
|
|
783
|
-
db: createFakeDb(captured) as unknown as NonNullable<
|
|
784
|
-
Parameters<typeof createSessionService>[0]["db"]
|
|
785
|
-
>,
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
await service.launchSession({
|
|
789
|
-
agentAddress: AGENT_ADDRESS,
|
|
790
|
-
agentId: AGENT_ID,
|
|
791
|
-
instanceId: INSTANCE_ID,
|
|
792
|
-
config: MOCK_CONFIG,
|
|
793
|
-
deployContent: { systemPrompt: "Base prompt" },
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
const writeCall = repoStore.calls.find(
|
|
797
|
-
(c) => c.method === "writeDeployTree",
|
|
798
|
-
);
|
|
799
|
-
if (writeCall === undefined) throw new Error("writeDeployTree not called");
|
|
800
|
-
const content = writeCall.args[1];
|
|
801
|
-
if (
|
|
802
|
-
content === null ||
|
|
803
|
-
typeof content !== "object" ||
|
|
804
|
-
!("systemPrompt" in content) ||
|
|
805
|
-
typeof content.systemPrompt !== "string"
|
|
806
|
-
) {
|
|
807
|
-
throw new Error("writeDeployTree content shape unexpected");
|
|
808
|
-
}
|
|
809
|
-
const prompt = content.systemPrompt;
|
|
810
|
-
|
|
811
|
-
expect(prompt.startsWith("Base prompt")).toBe(true);
|
|
812
|
-
expect(prompt).toContain("<available_skills>");
|
|
813
|
-
expect(prompt).toContain("</available_skills>");
|
|
814
|
-
|
|
815
|
-
// Skill order: assets in listAgentAssets order; within an asset,
|
|
816
|
-
// skills in index order (which the handler sorts).
|
|
817
|
-
const greetWaveIdx = prompt.indexOf("<name>greeter/wave</name>");
|
|
818
|
-
const greetBowIdx = prompt.indexOf("<name>greeter/bow</name>");
|
|
819
|
-
const searchWaveIdx = prompt.indexOf("<name>searcher/wave</name>");
|
|
820
|
-
expect(greetWaveIdx).toBeGreaterThan(-1);
|
|
821
|
-
expect(greetBowIdx).toBeGreaterThan(-1);
|
|
822
|
-
expect(searchWaveIdx).toBeGreaterThan(-1);
|
|
823
|
-
expect(greetBowIdx).toBeLessThan(greetWaveIdx);
|
|
824
|
-
expect(greetWaveIdx).toBeLessThan(searchWaveIdx);
|
|
825
|
-
|
|
826
|
-
expect(prompt).toContain(
|
|
827
|
-
"<description>Bows formally with A & B.</description>",
|
|
828
|
-
);
|
|
829
|
-
expect(prompt).toContain("<path>workspace/skills/greeter/wave/</path>");
|
|
830
|
-
expect(prompt).toContain("<path>workspace/skills/searcher/wave/</path>");
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
test("launchSession omits the available_skills stanza when no skill assets are attached", async () => {
|
|
834
|
-
const service = createSessionService({
|
|
835
|
-
sidecarRouter: router,
|
|
836
|
-
agentRepoStore: repoStore,
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
await service.launchSession({
|
|
840
|
-
agentAddress: AGENT_ADDRESS,
|
|
841
|
-
agentId: AGENT_ID,
|
|
842
|
-
instanceId: INSTANCE_ID,
|
|
843
|
-
config: MOCK_CONFIG,
|
|
844
|
-
deployContent: { systemPrompt: "Only the base prompt" },
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
const writeCall = repoStore.calls.find(
|
|
848
|
-
(c) => c.method === "writeDeployTree",
|
|
849
|
-
);
|
|
850
|
-
if (writeCall === undefined) throw new Error("writeDeployTree not called");
|
|
851
|
-
const content = writeCall.args[1];
|
|
852
|
-
if (
|
|
853
|
-
content === null ||
|
|
854
|
-
typeof content !== "object" ||
|
|
855
|
-
!("systemPrompt" in content) ||
|
|
856
|
-
typeof content.systemPrompt !== "string"
|
|
857
|
-
) {
|
|
858
|
-
throw new Error("writeDeployTree content shape unexpected");
|
|
859
|
-
}
|
|
860
|
-
expect(content.systemPrompt).toBe("Only the base prompt");
|
|
861
|
-
expect(content.systemPrompt).not.toContain("<available_skills>");
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
test("launchSession without assetService leaves the deploy-only flow unchanged", async () => {
|
|
865
|
-
const service = createSessionService({
|
|
866
|
-
sidecarRouter: router,
|
|
867
|
-
agentRepoStore: repoStore,
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
await service.launchSession({
|
|
871
|
-
agentAddress: AGENT_ADDRESS,
|
|
872
|
-
agentId: AGENT_ID,
|
|
873
|
-
instanceId: INSTANCE_ID,
|
|
874
|
-
config: MOCK_CONFIG,
|
|
875
|
-
deployContent: MOCK_CONTENT,
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
const methods = [
|
|
879
|
-
...repoStore.calls.map((c) => c.method),
|
|
880
|
-
...router.calls.map((c) => c.method),
|
|
881
|
-
];
|
|
882
|
-
expect(methods).toEqual([
|
|
883
|
-
"writeDeployTree",
|
|
884
|
-
"createDeployPack",
|
|
885
|
-
"sendAgentDeploy",
|
|
886
|
-
"sendPack",
|
|
887
|
-
"sendSessionStart",
|
|
888
|
-
]);
|
|
889
|
-
|
|
890
|
-
// No mountPath options on the single sendPack call.
|
|
891
|
-
const packCall = router.calls.find((c) => c.method === "sendPack");
|
|
892
|
-
if (packCall === undefined) throw new Error("sendPack not called");
|
|
893
|
-
expect(packCall.args[4]).toBeUndefined();
|
|
894
|
-
});
|
|
895
|
-
});
|