@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,540 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
import type { DB } from "@intx/db";
|
|
7
|
-
import {
|
|
8
|
-
agentAsset as agentAssetTable,
|
|
9
|
-
asset as assetTable,
|
|
10
|
-
} from "@intx/db/schema";
|
|
11
|
-
import { generateKeyPair } from "@intx/crypto-node";
|
|
12
|
-
import type { KeyPair } from "@intx/types/runtime";
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
AssetServiceError,
|
|
16
|
-
createAssetService,
|
|
17
|
-
type AssetService,
|
|
18
|
-
} from "./asset-service";
|
|
19
|
-
import { createRepoStore } from "./repo-store";
|
|
20
|
-
import type { Principal, RepoStore } from "./repo-store";
|
|
21
|
-
import { skillKindHandler, skillAuthorize } from "./skill-kind";
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// DB stub
|
|
25
|
-
//
|
|
26
|
-
// Mirrors the mocking style used by hub-session-orchestrator.test.ts: a
|
|
27
|
-
// minimal in-memory store routed by drizzle table identity. Captures the
|
|
28
|
-
// rows passed to insert/values so the test can assert column-level shape,
|
|
29
|
-
// and raises a typed Postgres unique-violation error (code "23505") to
|
|
30
|
-
// exercise the AssetService's translation of duplicates into typed domain
|
|
31
|
-
// errors.
|
|
32
|
-
//
|
|
33
|
-
// The where-clauses passed by drizzle's `eq` are opaque values; rather
|
|
34
|
-
// than inspect them, the stub exposes `nextFindFirstAssetId` and
|
|
35
|
-
// `nextSelectAgentId` setters that the tests use to tell the stub which
|
|
36
|
-
// id the next query targets.
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
type AssetRow = {
|
|
40
|
-
id: string;
|
|
41
|
-
tenantId: string;
|
|
42
|
-
kind: string;
|
|
43
|
-
name: string;
|
|
44
|
-
displayName: string | null;
|
|
45
|
-
creatorPrincipalId: string | null;
|
|
46
|
-
createdAt: Date;
|
|
47
|
-
updatedAt: Date;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
type AgentAssetRow = {
|
|
51
|
-
id: string;
|
|
52
|
-
agentId: string;
|
|
53
|
-
assetId: string;
|
|
54
|
-
ref: string;
|
|
55
|
-
accessMode: string;
|
|
56
|
-
createdAt: Date;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
class UniqueViolation extends Error {
|
|
60
|
-
readonly code = "23505";
|
|
61
|
-
constructor(constraint: string) {
|
|
62
|
-
super(`duplicate key value violates unique constraint "${constraint}"`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
class ForeignKeyViolation extends Error {
|
|
67
|
-
readonly code = "23503";
|
|
68
|
-
constructor(constraint: string) {
|
|
69
|
-
super(`insert violates foreign key constraint "${constraint}"`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type DBStub = {
|
|
74
|
-
db: DB["db"];
|
|
75
|
-
assets: AssetRow[];
|
|
76
|
-
agentAssets: AgentAssetRow[];
|
|
77
|
-
nextFindFirstAssetId: (id: string) => void;
|
|
78
|
-
nextSelectAgentId: (id: string) => void;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
function makeDB(): DBStub {
|
|
82
|
-
const assets: AssetRow[] = [];
|
|
83
|
-
const agentAssets: AgentAssetRow[] = [];
|
|
84
|
-
|
|
85
|
-
let lastFindFirstId: string | null = null;
|
|
86
|
-
let lastSelectAgentId: string | null = null;
|
|
87
|
-
|
|
88
|
-
function isAssetRow(row: AssetRow | AgentAssetRow): row is AssetRow {
|
|
89
|
-
return "tenantId" in row && "kind" in row;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function insertReturning(
|
|
93
|
-
table: unknown,
|
|
94
|
-
row: AssetRow | AgentAssetRow,
|
|
95
|
-
): Promise<unknown[]> {
|
|
96
|
-
if (table === assetTable) {
|
|
97
|
-
if (!isAssetRow(row)) {
|
|
98
|
-
throw new Error("insert into asset received non-asset row shape");
|
|
99
|
-
}
|
|
100
|
-
if (
|
|
101
|
-
assets.some(
|
|
102
|
-
(a) =>
|
|
103
|
-
a.tenantId === row.tenantId &&
|
|
104
|
-
a.kind === row.kind &&
|
|
105
|
-
a.name === row.name,
|
|
106
|
-
)
|
|
107
|
-
) {
|
|
108
|
-
return Promise.reject(new UniqueViolation("asset_tenant_kind_name"));
|
|
109
|
-
}
|
|
110
|
-
assets.push(row);
|
|
111
|
-
return Promise.resolve([row]);
|
|
112
|
-
}
|
|
113
|
-
if (table === agentAssetTable) {
|
|
114
|
-
if (isAssetRow(row)) {
|
|
115
|
-
throw new Error("insert into agent_asset received asset row shape");
|
|
116
|
-
}
|
|
117
|
-
if (!assets.some((a) => a.id === row.assetId)) {
|
|
118
|
-
return Promise.reject(
|
|
119
|
-
new ForeignKeyViolation("agent_asset_asset_id_asset_id_fk"),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
if (
|
|
123
|
-
agentAssets.some(
|
|
124
|
-
(a) => a.agentId === row.agentId && a.assetId === row.assetId,
|
|
125
|
-
)
|
|
126
|
-
) {
|
|
127
|
-
return Promise.reject(new UniqueViolation("agent_asset_agent_asset"));
|
|
128
|
-
}
|
|
129
|
-
agentAssets.push(row);
|
|
130
|
-
return Promise.resolve([row]);
|
|
131
|
-
}
|
|
132
|
-
throw new Error(`unexpected insert table`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function findFirstAsset(): Promise<AssetRow | undefined> {
|
|
136
|
-
if (lastFindFirstId === null) {
|
|
137
|
-
return Promise.resolve(undefined);
|
|
138
|
-
}
|
|
139
|
-
const match = assets.find((a) => a.id === lastFindFirstId);
|
|
140
|
-
lastFindFirstId = null;
|
|
141
|
-
return Promise.resolve(match);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function joinAgentAssets(): Promise<unknown[]> {
|
|
145
|
-
if (lastSelectAgentId === null) {
|
|
146
|
-
return Promise.resolve([]);
|
|
147
|
-
}
|
|
148
|
-
const id = lastSelectAgentId;
|
|
149
|
-
lastSelectAgentId = null;
|
|
150
|
-
const joined = agentAssets
|
|
151
|
-
.filter((aa) => aa.agentId === id)
|
|
152
|
-
.map((aa) => {
|
|
153
|
-
const a = assets.find((x) => x.id === aa.assetId);
|
|
154
|
-
if (a === undefined) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
`inconsistent stub: agent_asset ${aa.id} references missing asset ${aa.assetId}`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
return { agentAsset: aa, asset: a };
|
|
160
|
-
});
|
|
161
|
-
return Promise.resolve(joined);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion --
|
|
165
|
-
* drizzle PgDatabase type cannot be structurally satisfied in tests */
|
|
166
|
-
const db = {
|
|
167
|
-
insert(t: unknown) {
|
|
168
|
-
return {
|
|
169
|
-
values(row: AssetRow | AgentAssetRow) {
|
|
170
|
-
return {
|
|
171
|
-
returning: () => insertReturning(t, row),
|
|
172
|
-
};
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
},
|
|
176
|
-
query: {
|
|
177
|
-
asset: {
|
|
178
|
-
findFirst: (_args: { where: unknown }) => findFirstAsset(),
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
select(_cols: unknown) {
|
|
182
|
-
return {
|
|
183
|
-
from: (_t: unknown) => ({
|
|
184
|
-
innerJoin: (_t2: unknown, _on: unknown) => ({
|
|
185
|
-
where: (_w: unknown) => ({
|
|
186
|
-
orderBy: (..._order: unknown[]) => joinAgentAssets(),
|
|
187
|
-
}),
|
|
188
|
-
}),
|
|
189
|
-
}),
|
|
190
|
-
};
|
|
191
|
-
},
|
|
192
|
-
} as unknown as DB["db"];
|
|
193
|
-
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
db,
|
|
197
|
-
assets,
|
|
198
|
-
agentAssets,
|
|
199
|
-
nextFindFirstAssetId: (id) => {
|
|
200
|
-
lastFindFirstId = id;
|
|
201
|
-
},
|
|
202
|
-
nextSelectAgentId: (id) => {
|
|
203
|
-
lastSelectAgentId = id;
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// RepoStore fixture
|
|
210
|
-
//
|
|
211
|
-
// The skill kind handler runs against a real on-disk RepoStore so the
|
|
212
|
-
// validatePush -> path_violation translation is exercised end-to-end.
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
const tempDirs: string[] = [];
|
|
216
|
-
|
|
217
|
-
async function makeTempDir(prefix: string): Promise<string> {
|
|
218
|
-
const d = await fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
219
|
-
tempDirs.push(d);
|
|
220
|
-
return d;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
let signingKey: KeyPair;
|
|
224
|
-
|
|
225
|
-
async function createSkillRepoStore(): Promise<RepoStore> {
|
|
226
|
-
if (signingKey === undefined) {
|
|
227
|
-
signingKey = await generateKeyPair();
|
|
228
|
-
}
|
|
229
|
-
const dataDir = await makeTempDir("asset-svc-");
|
|
230
|
-
return createRepoStore({
|
|
231
|
-
dataDir,
|
|
232
|
-
signingKey,
|
|
233
|
-
handlers: { skill: skillKindHandler },
|
|
234
|
-
authorize: skillAuthorize,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const HUB: Principal = { kind: "hub" };
|
|
239
|
-
|
|
240
|
-
const VALID_SKILL_TREE = {
|
|
241
|
-
files: {
|
|
242
|
-
"greet/SKILL.md":
|
|
243
|
-
"---\nname: greet\ndescription: Say hello in a chosen language\n---\n\nbody\n",
|
|
244
|
-
},
|
|
245
|
-
message: "initial",
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const INVALID_SKILL_TREE = {
|
|
249
|
-
files: {
|
|
250
|
-
// Reserved name "anthropic" is rejected by the kind handler's
|
|
251
|
-
// arktype frontmatter schema; the substrate translates the
|
|
252
|
-
// rejection into a "path_violation:"-prefixed Error message.
|
|
253
|
-
"anthropic/SKILL.md":
|
|
254
|
-
"---\nname: anthropic\ndescription: reserved-name skill\n---\n\nbody\n",
|
|
255
|
-
},
|
|
256
|
-
message: "initial",
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// Tests
|
|
261
|
-
// ---------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
describe("AssetService", () => {
|
|
264
|
-
let dbFixture: DBStub;
|
|
265
|
-
let repoStore: RepoStore;
|
|
266
|
-
let service: AssetService;
|
|
267
|
-
|
|
268
|
-
beforeEach(async () => {
|
|
269
|
-
dbFixture = makeDB();
|
|
270
|
-
repoStore = await createSkillRepoStore();
|
|
271
|
-
service = createAssetService({ db: dbFixture.db, repoStore });
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
describe("createAsset", () => {
|
|
275
|
-
test("inserts the asset row and initializes the skill repo", async () => {
|
|
276
|
-
const asset = await service.createAsset({
|
|
277
|
-
tenantId: "tnt_1",
|
|
278
|
-
kind: "skill",
|
|
279
|
-
name: "greet",
|
|
280
|
-
displayName: "Greeting skill",
|
|
281
|
-
creatorPrincipalId: "prn_1",
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
expect(asset.kind).toBe("skill");
|
|
285
|
-
expect(asset.tenantId).toBe("tnt_1");
|
|
286
|
-
expect(asset.name).toBe("greet");
|
|
287
|
-
expect(asset.displayName).toBe("Greeting skill");
|
|
288
|
-
expect(asset.creatorPrincipalId).toBe("prn_1");
|
|
289
|
-
expect(asset.id).toMatch(/^ast_/);
|
|
290
|
-
|
|
291
|
-
expect(dbFixture.assets).toHaveLength(1);
|
|
292
|
-
const inserted = dbFixture.assets[0];
|
|
293
|
-
if (inserted === undefined) throw new Error("unreachable");
|
|
294
|
-
expect(inserted.id).toBe(asset.id);
|
|
295
|
-
|
|
296
|
-
// initRepo creates the on-disk repo so subsequent writeTree calls
|
|
297
|
-
// succeed without ENOENT. Writing a tree against the new repo is
|
|
298
|
-
// the cheapest way to assert the repo exists; the substrate would
|
|
299
|
-
// throw at the first git operation otherwise.
|
|
300
|
-
const out = await repoStore.writeTree(
|
|
301
|
-
HUB,
|
|
302
|
-
{ kind: "skill", id: asset.id },
|
|
303
|
-
"refs/heads/main",
|
|
304
|
-
VALID_SKILL_TREE,
|
|
305
|
-
);
|
|
306
|
-
expect(out.commitSha).toMatch(/^[0-9a-f]{40}$/);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("rejects kind agent-state", async () => {
|
|
310
|
-
const err = await service
|
|
311
|
-
.createAsset({
|
|
312
|
-
tenantId: "tnt_1",
|
|
313
|
-
kind: "agent-state",
|
|
314
|
-
name: "x",
|
|
315
|
-
})
|
|
316
|
-
.catch((e: unknown) => e);
|
|
317
|
-
|
|
318
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
319
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
320
|
-
expect(err.reason).toBe("unsupported_kind");
|
|
321
|
-
expect(dbFixture.assets).toHaveLength(0);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
test("accepts a valid lowercase-kebab name", async () => {
|
|
325
|
-
const asset = await service.createAsset({
|
|
326
|
-
tenantId: "tnt_1",
|
|
327
|
-
kind: "skill",
|
|
328
|
-
name: "my-skills",
|
|
329
|
-
});
|
|
330
|
-
expect(asset.name).toBe("my-skills");
|
|
331
|
-
expect(dbFixture.assets).toHaveLength(1);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test.each([
|
|
335
|
-
[".", "single dot"],
|
|
336
|
-
["..", "double dot"],
|
|
337
|
-
["my skill", "embedded space"],
|
|
338
|
-
["skills/greet", "embedded slash"],
|
|
339
|
-
])("rejects name %p (%s) as invalid_name", async (badName) => {
|
|
340
|
-
const err = await service
|
|
341
|
-
.createAsset({
|
|
342
|
-
tenantId: "tnt_1",
|
|
343
|
-
kind: "skill",
|
|
344
|
-
name: badName,
|
|
345
|
-
})
|
|
346
|
-
.catch((e: unknown) => e);
|
|
347
|
-
|
|
348
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
349
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
350
|
-
expect(err.reason).toBe("invalid_name");
|
|
351
|
-
expect(dbFixture.assets).toHaveLength(0);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test("surfaces duplicate (tenantId, kind, name) as duplicate_asset", async () => {
|
|
355
|
-
await service.createAsset({
|
|
356
|
-
tenantId: "tnt_1",
|
|
357
|
-
kind: "skill",
|
|
358
|
-
name: "greet",
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
const err = await service
|
|
362
|
-
.createAsset({
|
|
363
|
-
tenantId: "tnt_1",
|
|
364
|
-
kind: "skill",
|
|
365
|
-
name: "greet",
|
|
366
|
-
})
|
|
367
|
-
.catch((e: unknown) => e);
|
|
368
|
-
|
|
369
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
370
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
371
|
-
expect(err.reason).toBe("duplicate_asset");
|
|
372
|
-
expect(dbFixture.assets).toHaveLength(1);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe("populateAsset", () => {
|
|
377
|
-
test("happy path writes the tree and returns commitSha", async () => {
|
|
378
|
-
const asset = await service.createAsset({
|
|
379
|
-
tenantId: "tnt_1",
|
|
380
|
-
kind: "skill",
|
|
381
|
-
name: "greet",
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
dbFixture.nextFindFirstAssetId(asset.id);
|
|
385
|
-
const out = await service.populateAsset({
|
|
386
|
-
assetId: asset.id,
|
|
387
|
-
ref: "refs/heads/main",
|
|
388
|
-
tree: VALID_SKILL_TREE,
|
|
389
|
-
principal: HUB,
|
|
390
|
-
});
|
|
391
|
-
expect(out.commitSha).toMatch(/^[0-9a-f]{40}$/);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
test("surfaces an unknown assetId as not_found", async () => {
|
|
395
|
-
dbFixture.nextFindFirstAssetId("ast_missing");
|
|
396
|
-
const err = await service
|
|
397
|
-
.populateAsset({
|
|
398
|
-
assetId: "ast_missing",
|
|
399
|
-
ref: "refs/heads/main",
|
|
400
|
-
tree: VALID_SKILL_TREE,
|
|
401
|
-
principal: HUB,
|
|
402
|
-
})
|
|
403
|
-
.catch((e: unknown) => e);
|
|
404
|
-
|
|
405
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
406
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
407
|
-
expect(err.reason).toBe("not_found");
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test("translates path_violation: error from validatePush", async () => {
|
|
411
|
-
const asset = await service.createAsset({
|
|
412
|
-
tenantId: "tnt_1",
|
|
413
|
-
kind: "skill",
|
|
414
|
-
name: "greet",
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
dbFixture.nextFindFirstAssetId(asset.id);
|
|
418
|
-
const err = await service
|
|
419
|
-
.populateAsset({
|
|
420
|
-
assetId: asset.id,
|
|
421
|
-
ref: "refs/heads/main",
|
|
422
|
-
tree: INVALID_SKILL_TREE,
|
|
423
|
-
principal: HUB,
|
|
424
|
-
})
|
|
425
|
-
.catch((e: unknown) => e);
|
|
426
|
-
|
|
427
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
428
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
429
|
-
expect(err.reason).toBe("path_violation");
|
|
430
|
-
expect(err.message).toContain("path_violation:");
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
describe("attachAsset", () => {
|
|
435
|
-
test("inserts an agent_asset row", async () => {
|
|
436
|
-
const asset = await service.createAsset({
|
|
437
|
-
tenantId: "tnt_1",
|
|
438
|
-
kind: "skill",
|
|
439
|
-
name: "greet",
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
const aa = await service.attachAsset({
|
|
443
|
-
agentId: "agt_1",
|
|
444
|
-
assetId: asset.id,
|
|
445
|
-
ref: "refs/heads/main",
|
|
446
|
-
accessMode: "read-only",
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
expect(aa.id).toMatch(/^aas_/);
|
|
450
|
-
expect(aa.agentId).toBe("agt_1");
|
|
451
|
-
expect(aa.assetId).toBe(asset.id);
|
|
452
|
-
expect(aa.accessMode).toBe("read-only");
|
|
453
|
-
expect(dbFixture.agentAssets).toHaveLength(1);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test("defaults accessMode to read-only when omitted", async () => {
|
|
457
|
-
const asset = await service.createAsset({
|
|
458
|
-
tenantId: "tnt_1",
|
|
459
|
-
kind: "skill",
|
|
460
|
-
name: "greet",
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
const aa = await service.attachAsset({
|
|
464
|
-
agentId: "agt_1",
|
|
465
|
-
assetId: asset.id,
|
|
466
|
-
ref: "refs/heads/main",
|
|
467
|
-
});
|
|
468
|
-
expect(aa.accessMode).toBe("read-only");
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
test("surfaces a missing assetId reference as invalid_reference", async () => {
|
|
472
|
-
const err = await service
|
|
473
|
-
.attachAsset({
|
|
474
|
-
agentId: "agt_1",
|
|
475
|
-
assetId: "ast_missing",
|
|
476
|
-
ref: "refs/heads/main",
|
|
477
|
-
})
|
|
478
|
-
.catch((e: unknown) => e);
|
|
479
|
-
|
|
480
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
481
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
482
|
-
expect(err.reason).toBe("invalid_reference");
|
|
483
|
-
expect(dbFixture.agentAssets).toHaveLength(0);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test("surfaces duplicate (agentId, assetId) as duplicate_attachment", async () => {
|
|
487
|
-
const asset = await service.createAsset({
|
|
488
|
-
tenantId: "tnt_1",
|
|
489
|
-
kind: "skill",
|
|
490
|
-
name: "greet",
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
await service.attachAsset({
|
|
494
|
-
agentId: "agt_1",
|
|
495
|
-
assetId: asset.id,
|
|
496
|
-
ref: "refs/heads/main",
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
const err = await service
|
|
500
|
-
.attachAsset({
|
|
501
|
-
agentId: "agt_1",
|
|
502
|
-
assetId: asset.id,
|
|
503
|
-
ref: "refs/heads/main",
|
|
504
|
-
})
|
|
505
|
-
.catch((e: unknown) => e);
|
|
506
|
-
|
|
507
|
-
expect(err).toBeInstanceOf(AssetServiceError);
|
|
508
|
-
if (!(err instanceof AssetServiceError)) throw new Error("unreachable");
|
|
509
|
-
expect(err.reason).toBe("duplicate_attachment");
|
|
510
|
-
expect(dbFixture.agentAssets).toHaveLength(1);
|
|
511
|
-
});
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
describe("listAgentAssets", () => {
|
|
515
|
-
test("returns joined rows including asset.name and asset.kind", async () => {
|
|
516
|
-
const asset = await service.createAsset({
|
|
517
|
-
tenantId: "tnt_1",
|
|
518
|
-
kind: "skill",
|
|
519
|
-
name: "greet",
|
|
520
|
-
displayName: "Greeting",
|
|
521
|
-
});
|
|
522
|
-
await service.attachAsset({
|
|
523
|
-
agentId: "agt_1",
|
|
524
|
-
assetId: asset.id,
|
|
525
|
-
ref: "refs/heads/main",
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
dbFixture.nextSelectAgentId("agt_1");
|
|
529
|
-
const rows = await service.listAgentAssets("agt_1");
|
|
530
|
-
expect(rows).toHaveLength(1);
|
|
531
|
-
const row = rows[0];
|
|
532
|
-
if (row === undefined) throw new Error("unreachable");
|
|
533
|
-
expect(row.agentId).toBe("agt_1");
|
|
534
|
-
expect(row.asset.id).toBe(asset.id);
|
|
535
|
-
expect(row.asset.kind).toBe("skill");
|
|
536
|
-
expect(row.asset.name).toBe("greet");
|
|
537
|
-
expect(row.asset.displayName).toBe("Greeting");
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
});
|