@peers-app/peers-sdk 0.25.20 → 0.26.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/dist/contracts/__tests__/builder.test.js +0 -2
- package/dist/contracts/__tests__/contract-provider-router.test.js +51 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +21 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +84 -0
- package/dist/contracts/__tests__/legacy-removal.test.d.ts +1 -0
- package/dist/contracts/__tests__/legacy-removal.test.js +90 -0
- package/dist/contracts/__tests__/system-contracts.test.js +88 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.js +44 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +446 -0
- package/dist/contracts/__tests__/tool-catalog.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog.test.js +649 -0
- package/dist/contracts/__tests__/workflow-context.test.d.ts +1 -0
- package/dist/contracts/__tests__/workflow-context.test.js +61 -0
- package/dist/contracts/builder.d.ts +9 -8
- package/dist/contracts/builder.js +34 -16
- package/dist/contracts/contract-provider-router.d.ts +12 -1
- package/dist/contracts/contract-provider-router.js +40 -2
- package/dist/contracts/contract-proxy.d.ts +20 -6
- package/dist/contracts/contract-proxy.js +23 -12
- package/dist/contracts/contract-resolvers.d.ts +22 -3
- package/dist/contracts/contract-resolvers.js +86 -7
- package/dist/contracts/extract.d.ts +7 -0
- package/dist/contracts/extract.js +3 -0
- package/dist/contracts/index.d.ts +11 -4
- package/dist/contracts/index.js +14 -2
- package/dist/contracts/registry.d.ts +3 -0
- package/dist/contracts/registry.js +2 -0
- package/dist/contracts/system/desktop-tools-contract.d.ts +5 -0
- package/dist/contracts/system/desktop-tools-contract.js +88 -0
- package/dist/contracts/system/host-tools-contract.d.ts +5 -0
- package/dist/contracts/system/host-tools-contract.js +76 -0
- package/dist/contracts/system/index.d.ts +3 -0
- package/dist/contracts/system/index.js +3 -0
- package/dist/contracts/system/tool-catalog-contract.d.ts +73 -0
- package/dist/contracts/system/tool-catalog-contract.js +70 -0
- package/dist/contracts/system-contracts.d.ts +3 -1
- package/dist/contracts/system-contracts.js +20 -6
- package/dist/contracts/tool-catalog-identity.d.ts +35 -0
- package/dist/contracts/tool-catalog-identity.js +88 -0
- package/dist/contracts/tool-catalog-migrations.d.ts +12 -0
- package/dist/contracts/tool-catalog-migrations.js +179 -0
- package/dist/contracts/tool-catalog-service.d.ts +30 -0
- package/dist/contracts/tool-catalog-service.js +287 -0
- package/dist/contracts/tool-catalog.d.ts +91 -0
- package/dist/contracts/tool-catalog.js +490 -0
- package/dist/contracts/types.d.ts +7 -5
- package/dist/data/messages.d.ts +3 -3
- package/dist/data/orm/sql.data-source.d.ts +5 -0
- package/dist/data/orm/sql.data-source.js +28 -10
- package/dist/data/orm/sql.data-source.test.d.ts +1 -1
- package/dist/data/orm/sql.data-source.test.js +160 -0
- package/dist/data/orm/table-container.d.ts +14 -0
- package/dist/data/orm/table-container.js +20 -0
- package/dist/data/tool-access-approvals.d.ts +6 -6
- package/dist/data/tool-catalog-fields.d.ts +31 -0
- package/dist/data/tool-catalog-fields.js +67 -0
- package/dist/data/tools.d.ts +38 -10
- package/dist/data/tools.js +31 -8
- package/dist/data/workflow-logs.d.ts +6 -2
- package/dist/data/workflow-logs.js +7 -5
- package/dist/data/workflow-runs.d.ts +6 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -3
- package/dist/package-installer/package-creator.d.ts +14 -5
- package/dist/package-installer/package-creator.js +92 -37
- package/dist/package-installer/package-creator.test.js +308 -23
- package/dist/package-installer/types.d.ts +8 -0
- package/dist/package-loader/contract-package-loader.d.ts +12 -3
- package/dist/package-loader/contract-package-loader.js +54 -50
- package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
- package/dist/package-loader/contract-package-loader.test.js +172 -13
- package/dist/package-loader/host-authority-loading.test.js +16 -3
- package/dist/package-loader/isolated-artifact.d.ts +14 -2
- package/dist/package-loader/isolated-artifact.js +6 -0
- package/dist/package-loader/isolated-artifact.test.js +34 -0
- package/dist/package-loader/isolated-runtime.d.ts +14 -2
- package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
- package/dist/package-loader/isolated-table-gateway.js +54 -5
- package/dist/package-loader/isolated-table-gateway.test.js +123 -0
- package/dist/package-loader/package-loader.d.ts +14 -2
- package/dist/package-loader/package-loader.js +184 -62
- package/dist/package-loader/package-loader.test.js +695 -2
- package/dist/package-loader/resolve-consumed-contracts.js +1 -1
- package/dist/system-ids.d.ts +6 -0
- package/dist/system-ids.js +7 -1
- package/dist/types/peers-package.d.ts +3 -2
- package/dist/types/workflow-run-context.d.ts +12 -2
- package/package.json +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -17
- package/dist/tools/tools-factory.d.ts +0 -5
- package/dist/tools/tools-factory.js +0 -34
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("../../index");
|
|
4
|
+
const nacl = require("tweetnacl");
|
|
5
|
+
const tools_1 = require("../../data/tools");
|
|
6
|
+
const field_type_1 = require("../../types/field-type");
|
|
7
|
+
const zod_types_1 = require("../../types/zod-types");
|
|
8
|
+
const utils_1 = require("../../utils");
|
|
9
|
+
const registry_1 = require("../registry");
|
|
10
|
+
const systemContracts = require("../system-contracts");
|
|
11
|
+
const tool_catalog_1 = require("../tool-catalog");
|
|
12
|
+
const tool_catalog_identity_1 = require("../tool-catalog-identity");
|
|
13
|
+
const tool_catalog_migrations_1 = require("../tool-catalog-migrations");
|
|
14
|
+
function required(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
throw new Error("Missing fixture value");
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
const contractId = "00mp287gdl5atltgs5bh0vm9z";
|
|
20
|
+
function definition(names = ["read"], version = 1) {
|
|
21
|
+
return {
|
|
22
|
+
contractId,
|
|
23
|
+
version,
|
|
24
|
+
name: "Test",
|
|
25
|
+
description: "",
|
|
26
|
+
devTag: "dev",
|
|
27
|
+
tables: [],
|
|
28
|
+
observables: [],
|
|
29
|
+
tools: names.map((name) => ({
|
|
30
|
+
name,
|
|
31
|
+
usageDescription: name,
|
|
32
|
+
inputFields: [],
|
|
33
|
+
outputFields: [],
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function legacy(toolId = (0, utils_1.newid)()) {
|
|
38
|
+
return {
|
|
39
|
+
toolId,
|
|
40
|
+
name: "legacy",
|
|
41
|
+
usageDescription: "preserve",
|
|
42
|
+
code: "existing",
|
|
43
|
+
inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
44
|
+
outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
45
|
+
accessLevel: 70,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function fixture(rows = new Map()) {
|
|
49
|
+
const listeners = new Set();
|
|
50
|
+
const table = {
|
|
51
|
+
list: async () => [...rows.values()],
|
|
52
|
+
get: async (id) => rows.get(id),
|
|
53
|
+
update: jest.fn(async (patch) => {
|
|
54
|
+
const row = tools_1.toolSchema.parse({ ...rows.get(patch.toolId), ...patch });
|
|
55
|
+
rows.set(row.toolId, row);
|
|
56
|
+
for (const listener of listeners)
|
|
57
|
+
listener({ dataObject: row, op: "update", source: "local" });
|
|
58
|
+
return row;
|
|
59
|
+
}),
|
|
60
|
+
save: jest.fn(async (row, options) => {
|
|
61
|
+
if (!options?.insertOnly || !rows.has(row.toolId))
|
|
62
|
+
rows.set(row.toolId, tools_1.toolSchema.parse(row));
|
|
63
|
+
for (const listener of listeners)
|
|
64
|
+
listener({ dataObject: rows.get(row.toolId), op: "insert", source: "local" });
|
|
65
|
+
return rows.get(row.toolId);
|
|
66
|
+
}),
|
|
67
|
+
dataChanged: {
|
|
68
|
+
subscribe: (fn) => {
|
|
69
|
+
listeners.add(fn);
|
|
70
|
+
return { unsubscribe: () => listeners.delete(fn) };
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const context = {
|
|
75
|
+
dataContextId: (0, utils_1.newid)(),
|
|
76
|
+
tableContainer: { getTable: () => table },
|
|
77
|
+
};
|
|
78
|
+
const registry = new registry_1.ContractRegistry();
|
|
79
|
+
const catalog = new tool_catalog_1.ToolCatalog(context, registry);
|
|
80
|
+
const publish = async (def = definition(), provider = (0, utils_1.newid)(), prepare = async () => { }) => catalog.publish(provider, [{ definition: def }], prepare, () => registry.replaceBatch(provider, [{ definition: def }]));
|
|
81
|
+
return { rows, table, registry, catalog, publish, listeners };
|
|
82
|
+
}
|
|
83
|
+
test.each([
|
|
84
|
+
"save",
|
|
85
|
+
"snapshot",
|
|
86
|
+
])("system readiness rejects quarantine during a later %s before publishing any provider", async (boundary) => {
|
|
87
|
+
const first = definition();
|
|
88
|
+
const second = { ...definition(), contractId: (0, utils_1.newid)() };
|
|
89
|
+
const inventory = jest.spyOn(systemContracts, "listSystemContracts").mockReturnValue([
|
|
90
|
+
{ providerPackageId: (0, utils_1.newid)(), getDefinition: () => first },
|
|
91
|
+
{ providerPackageId: (0, utils_1.newid)(), getDefinition: () => second },
|
|
92
|
+
]);
|
|
93
|
+
const f = fixture();
|
|
94
|
+
const publish = jest.spyOn(f.registry, "replaceBatch");
|
|
95
|
+
const firstId = (0, tool_catalog_migrations_1.toolCatalogIds)({ contractId: first.contractId, version: 1, toolName: "read" })[0];
|
|
96
|
+
const secondId = (0, tool_catalog_migrations_1.toolCatalogIds)({
|
|
97
|
+
contractId: second.contractId,
|
|
98
|
+
version: 1,
|
|
99
|
+
toolName: "read",
|
|
100
|
+
})[0];
|
|
101
|
+
const quarantine = () => f.table.update({ toolId: firstId, catalogState: "quarantined" });
|
|
102
|
+
if (boundary === "save") {
|
|
103
|
+
const save = f.table.save.getMockImplementation();
|
|
104
|
+
if (!save)
|
|
105
|
+
throw new Error("Missing fixture save");
|
|
106
|
+
f.table.save.mockImplementation(async (row, options) => {
|
|
107
|
+
const saved = await save(row, options);
|
|
108
|
+
if (row.toolId === secondId)
|
|
109
|
+
await quarantine();
|
|
110
|
+
return saved;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const get = f.table.get;
|
|
115
|
+
jest.spyOn(f.table, "get").mockImplementation(async (id) => {
|
|
116
|
+
if (id === secondId)
|
|
117
|
+
await quarantine();
|
|
118
|
+
return get(id);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await expect(f.catalog.ready()).rejects.toThrow(/quarantined/i);
|
|
123
|
+
expect(publish).not.toHaveBeenCalled();
|
|
124
|
+
expect(f.registry.getProviderPackageId(first.contractId, 1)).toBeUndefined();
|
|
125
|
+
expect(f.registry.getProviderPackageId(second.contractId, 1)).toBeUndefined();
|
|
126
|
+
expect(f.rows.get(firstId)?.catalogState).toBe("quarantined");
|
|
127
|
+
expect(f.rows.has(secondId)).toBe(true);
|
|
128
|
+
expect(f.listeners.size).toBe(0);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await f.catalog.dispose();
|
|
132
|
+
inventory.mockRestore();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
describe("permanent Tools identity", () => {
|
|
136
|
+
test("delete validation protects reserved identities before rows exist", () => {
|
|
137
|
+
for (const id of [
|
|
138
|
+
tool_catalog_migrations_1.toolCatalogMigrations[0].toolId,
|
|
139
|
+
(0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }),
|
|
140
|
+
]) {
|
|
141
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(undefined, undefined, id)).toThrow(/permanent/);
|
|
142
|
+
}
|
|
143
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(undefined, undefined, (0, utils_1.newid)())).not.toThrow();
|
|
144
|
+
const target = { contractId, contractVersion: 1, toolName: "read" };
|
|
145
|
+
for (const row of [
|
|
146
|
+
{ ...legacy(), ...target },
|
|
147
|
+
{ ...legacy(), catalogState: "quarantined" },
|
|
148
|
+
])
|
|
149
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, undefined)).toThrow(/permanent/);
|
|
150
|
+
});
|
|
151
|
+
test("unresolved legacy rows are ordinary rows that may be deleted", () => {
|
|
152
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(legacy(), undefined)).not.toThrow();
|
|
153
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...legacy(), catalogState: "unresolved" }, undefined)).not.toThrow();
|
|
154
|
+
});
|
|
155
|
+
test.each([
|
|
156
|
+
[contractId, 1, "read", "000tool0m6n9qqflttd2vhqt0"],
|
|
157
|
+
[contractId, 2, "read", "000tool0hz4s7a3bzw0hx7h6e"],
|
|
158
|
+
[contractId, 1, "Read", "000tool0tkbtut20wcn4p2n9c"],
|
|
159
|
+
[contractId, 1, "é🛠", "000tool0w8lfnvvspj7cl3u9x"],
|
|
160
|
+
[contractId, 1, "é", "000tool0hcab9lwucf1sls3uk"],
|
|
161
|
+
[contractId, 1, "e\u0301", "000tool0m71grpdum3oh5ak0e"],
|
|
162
|
+
[contractId, 1, " read ", "000tool03ja6984o18ycrskkz"],
|
|
163
|
+
[contractId, 1, 'quote"\n\\', "000tool007ny5nm141nkk5puv"],
|
|
164
|
+
["00MP287GDL5ATLTGS5BH0VM9Z", 1, "read", "000tool0xd9tnb6xvxcvtrojg"],
|
|
165
|
+
[contractId, Number.MAX_SAFE_INTEGER, "read", "000tool0yk5ipn4g8azf6c6yt"],
|
|
166
|
+
])("golden %s v%s %s", (id, version, toolName, expected) => {
|
|
167
|
+
const target = {
|
|
168
|
+
contractId: id,
|
|
169
|
+
version: version,
|
|
170
|
+
toolName: toolName,
|
|
171
|
+
};
|
|
172
|
+
expect(Buffer.from((0, tool_catalog_identity_1.encodeToolCatalogTarget)(target)).toString()).toBe(JSON.stringify(["peers.tools-catalog.v1", id, version, toolName]));
|
|
173
|
+
expect((0, tool_catalog_identity_1.deriveToolCatalogId)(target)).toBe(expected);
|
|
174
|
+
expect((0, utils_1.isid)(expected)).toBe(true);
|
|
175
|
+
expect(zod_types_1.zodPeerId.safeParse(expected).success).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
test("rejects malformed Unicode, unsafe versions and invalid time prefixes", () => {
|
|
178
|
+
for (const toolName of ["\ud800", "x\udfff", "\ud800x", ""])
|
|
179
|
+
expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({ contractId, version: 1, toolName })).toThrow();
|
|
180
|
+
for (const version of [0, -1, 1.1, Number.MAX_SAFE_INTEGER + 1])
|
|
181
|
+
expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({ contractId, version, toolName: "ok" })).toThrow();
|
|
182
|
+
expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({ contractId: "z".repeat(25), version: 1, toolName: "ok" })).toThrow();
|
|
183
|
+
expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({
|
|
184
|
+
contractId: { toString: () => contractId },
|
|
185
|
+
version: 1,
|
|
186
|
+
toolName: "ok",
|
|
187
|
+
})).toThrow();
|
|
188
|
+
expect((0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: " read " })).not.toBe((0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }));
|
|
189
|
+
});
|
|
190
|
+
test("canonical bytes preserve UTF-8 and JSON escaping exactly", () => {
|
|
191
|
+
expect(Buffer.from((0, tool_catalog_identity_1.encodeToolCatalogTarget)({ contractId, version: 1, toolName: "é🛠" })).toString("hex")).toBe("5b2270656572732e746f6f6c732d636174616c6f672e7631222c2230306d7032383767646c3561746c74677335626830766d397a222c312c22c3a9f09f9ba0225d");
|
|
192
|
+
expect(Buffer.from((0, tool_catalog_identity_1.encodeToolCatalogTarget)({ contractId, version: 1, toolName: 'quote"\n\\' })).toString("hex")).toBe("5b2270656572732e746f6f6c732d636174616c6f672e7631222c2230306d7032383767646c3561746c74677335626830766d397a222c312c2271756f74655c225c6e5c5c225d");
|
|
193
|
+
});
|
|
194
|
+
test("reserves literal inventory and preserves narrow shipped exceptions", () => {
|
|
195
|
+
for (const entry of tool_catalog_migrations_1.toolCatalogMigrations) {
|
|
196
|
+
expect((0, tool_catalog_migrations_1.resolveLegacyToolId)(entry.toolId)).toEqual({
|
|
197
|
+
contractId: entry.contractId,
|
|
198
|
+
version: entry.version,
|
|
199
|
+
toolName: entry.toolName,
|
|
200
|
+
});
|
|
201
|
+
expect(tools_1.toolSchema.safeParse(legacy(entry.toolId)).success).toBe(true);
|
|
202
|
+
(0, tool_catalog_identity_1.validateToolCatalogTarget)(entry);
|
|
203
|
+
}
|
|
204
|
+
expect(zod_types_1.zodPeerId.safeParse("000peers0toolaccessctl0001").success).toBe(false);
|
|
205
|
+
expect(zod_types_1.zodPeerId.safeParse("00muxgugn661g0qmpuwafrmsi4").success).toBe(false);
|
|
206
|
+
expect((0, tool_catalog_migrations_1.resolveLegacyToolId)("000peers0tool00querydb001")?.toolName).toBe("query-database");
|
|
207
|
+
});
|
|
208
|
+
test("many-to-one aliases have no winner; ambiguity fails closed", () => {
|
|
209
|
+
const target = { contractId: "00muxgugn6xvs8f2r80ru205t", version: 1, toolName: "invoke" };
|
|
210
|
+
expect((0, tool_catalog_migrations_1.toolCatalogIds)(target)).toEqual([
|
|
211
|
+
"00muxoa8xeet2lpi8bdq55q03",
|
|
212
|
+
"00muxob3hbo3f6g49mipy1hyt",
|
|
213
|
+
]);
|
|
214
|
+
const entry = tool_catalog_migrations_1.toolCatalogMigrations[0];
|
|
215
|
+
expect(() => (0, tool_catalog_migrations_1.resolveLegacyToolId)(entry.toolId, [entry, { ...entry, toolName: "other" }])).toThrow(/Ambiguous/);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe("per-context catalog lifecycle", () => {
|
|
219
|
+
test("system metadata changes only supplemental fields, preserving policy and history", async () => {
|
|
220
|
+
const f = fixture();
|
|
221
|
+
await f.catalog.ready();
|
|
222
|
+
const toolId = "000peers0tool00000runner1";
|
|
223
|
+
const stored = {
|
|
224
|
+
...required(f.rows.get(toolId)),
|
|
225
|
+
code: "user source",
|
|
226
|
+
packageId: (0, utils_1.newid)(),
|
|
227
|
+
accessLevel: 90,
|
|
228
|
+
remoteAccessLevel: 80,
|
|
229
|
+
authorityPropagation: "cap",
|
|
230
|
+
};
|
|
231
|
+
f.rows.set(toolId, stored);
|
|
232
|
+
const metadata = {
|
|
233
|
+
...stored,
|
|
234
|
+
code: "replacement",
|
|
235
|
+
packageId: (0, utils_1.newid)(),
|
|
236
|
+
name: "replacement",
|
|
237
|
+
accessLevel: 0,
|
|
238
|
+
remoteAccessLevel: 0,
|
|
239
|
+
authorityPropagation: "preserve",
|
|
240
|
+
isAssistantRunner: true,
|
|
241
|
+
configSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
|
|
242
|
+
detailedDescription: "runner details",
|
|
243
|
+
};
|
|
244
|
+
await f.catalog.saveToolMetadata([metadata]);
|
|
245
|
+
expect(f.rows.get(toolId)).toEqual({
|
|
246
|
+
...stored,
|
|
247
|
+
isAssistantRunner: true,
|
|
248
|
+
configSchema: metadata.configSchema,
|
|
249
|
+
detailedDescription: metadata.detailedDescription,
|
|
250
|
+
});
|
|
251
|
+
const count = f.table.update.mock.calls.length;
|
|
252
|
+
await f.catalog.saveToolMetadata([metadata]);
|
|
253
|
+
expect(f.table.update).toHaveBeenCalledTimes(count);
|
|
254
|
+
await f.catalog.dispose();
|
|
255
|
+
});
|
|
256
|
+
test("system metadata rejects unresolved, quarantined, missing and non-system targets", async () => {
|
|
257
|
+
const f = fixture();
|
|
258
|
+
await f.publish();
|
|
259
|
+
const id = "000peers0tool00000runner1";
|
|
260
|
+
const stored = required(f.rows.get(id));
|
|
261
|
+
for (const catalogState of ["unresolved", "quarantined"]) {
|
|
262
|
+
f.rows.set(id, { ...stored, catalogState });
|
|
263
|
+
await expect(f.catalog.saveToolMetadata([{ toolId: id }])).rejects.toThrow(/resolved system member/);
|
|
264
|
+
}
|
|
265
|
+
f.rows.delete(id);
|
|
266
|
+
for (const toolId of [
|
|
267
|
+
id,
|
|
268
|
+
(0, utils_1.newid)(),
|
|
269
|
+
(0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }),
|
|
270
|
+
])
|
|
271
|
+
await expect(f.catalog.saveToolMetadata([{ toolId }])).rejects.toThrow(/resolved system member/);
|
|
272
|
+
await f.catalog.dispose();
|
|
273
|
+
});
|
|
274
|
+
test("publication refreshes runner metadata only after successful preparation", async () => {
|
|
275
|
+
const f = fixture();
|
|
276
|
+
const def = definition();
|
|
277
|
+
const provider = (0, utils_1.newid)();
|
|
278
|
+
const metadata = { contractId, version: 1, toolName: "read", isAssistantRunner: true };
|
|
279
|
+
const id = (0, tool_catalog_migrations_1.toolCatalogIds)(metadata)[0];
|
|
280
|
+
await f.publish(def, provider);
|
|
281
|
+
const before = f.rows.get(id);
|
|
282
|
+
await expect(f.catalog.publish(provider, [{ definition: def }], async () => {
|
|
283
|
+
throw new Error("prepare failed");
|
|
284
|
+
}, () => { }, [metadata])).rejects.toThrow("prepare failed");
|
|
285
|
+
expect(f.rows.get(id)).toEqual(before);
|
|
286
|
+
await f.catalog.publish(provider, [{ definition: def }], async () => { }, () => { }, [metadata]);
|
|
287
|
+
expect(f.rows.get(id)?.isAssistantRunner).toBe(true);
|
|
288
|
+
await f.catalog.dispose();
|
|
289
|
+
});
|
|
290
|
+
test("contract upgrades and dev members refresh unclaimed descriptions only after preparation", async () => {
|
|
291
|
+
const f = fixture();
|
|
292
|
+
const provider = (0, utils_1.newid)();
|
|
293
|
+
const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
|
|
294
|
+
await f.publish(definition(), provider);
|
|
295
|
+
f.rows.set(toolId, { ...f.rows.get(toolId), accessLevel: 40 });
|
|
296
|
+
const changed = definition();
|
|
297
|
+
changed.tools[0] = {
|
|
298
|
+
...changed.tools[0],
|
|
299
|
+
usageDescription: "changed",
|
|
300
|
+
inputFields: [{ name: "path", type: field_type_1.FieldType.string }],
|
|
301
|
+
};
|
|
302
|
+
const before = f.rows.get(toolId);
|
|
303
|
+
await expect(f.publish(changed, provider, async () => {
|
|
304
|
+
throw new Error("prepare failed");
|
|
305
|
+
})).rejects.toThrow("prepare failed");
|
|
306
|
+
expect(f.rows.get(toolId)).toEqual(before);
|
|
307
|
+
await f.publish(changed, provider);
|
|
308
|
+
expect(f.rows.get(toolId)).toMatchObject({
|
|
309
|
+
usageDescription: "changed",
|
|
310
|
+
inputSchema: { type: tools_1.IOSchemaType.simple, fields: changed.tools[0].inputFields },
|
|
311
|
+
accessLevel: 40,
|
|
312
|
+
catalogIdentity: "deterministic",
|
|
313
|
+
});
|
|
314
|
+
const updates = f.table.update.mock.calls.length;
|
|
315
|
+
await f.publish(changed, (0, utils_1.newid)());
|
|
316
|
+
expect(f.table.update).toHaveBeenCalledTimes(updates);
|
|
317
|
+
await f.catalog.dispose();
|
|
318
|
+
});
|
|
319
|
+
test("historical provenance limits description refresh to its provider", async () => {
|
|
320
|
+
const owner = (0, utils_1.newid)();
|
|
321
|
+
const old = { ...legacy("00muxoa8xeet2lpi8bdq55q03"), packageId: owner };
|
|
322
|
+
const f = fixture(new Map([[old.toolId, old]]));
|
|
323
|
+
const def = { ...definition(["invoke"]), contractId: "00muxgugn6xvs8f2r80ru205t" };
|
|
324
|
+
await f.publish(def, (0, utils_1.newid)());
|
|
325
|
+
expect(f.rows.get(old.toolId)?.usageDescription).toBe("preserve");
|
|
326
|
+
await f.publish(def, owner);
|
|
327
|
+
expect(f.rows.get(old.toolId)?.usageDescription).toBe("invoke");
|
|
328
|
+
await f.catalog.dispose();
|
|
329
|
+
});
|
|
330
|
+
test.each([
|
|
331
|
+
0, 1,
|
|
332
|
+
])("disposal during initialization neither publishes %s system providers nor subscribes", async (count) => {
|
|
333
|
+
const system = definition();
|
|
334
|
+
const inventory = jest
|
|
335
|
+
.spyOn(systemContracts, "listSystemContracts")
|
|
336
|
+
.mockReturnValue(count ? [{ providerPackageId: (0, utils_1.newid)(), getDefinition: () => system }] : []);
|
|
337
|
+
const f = fixture();
|
|
338
|
+
let release;
|
|
339
|
+
let entered;
|
|
340
|
+
const held = new Promise((resolve) => {
|
|
341
|
+
release = resolve;
|
|
342
|
+
});
|
|
343
|
+
const entering = new Promise((resolve) => {
|
|
344
|
+
entered = resolve;
|
|
345
|
+
});
|
|
346
|
+
const list = f.table.list;
|
|
347
|
+
jest.spyOn(f.table, "list").mockImplementationOnce(async () => {
|
|
348
|
+
entered();
|
|
349
|
+
await held;
|
|
350
|
+
return list();
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
const ready = f.catalog.ready();
|
|
354
|
+
await entering;
|
|
355
|
+
const disposing = f.catalog.dispose();
|
|
356
|
+
release();
|
|
357
|
+
await expect(ready).rejects.toThrow(/disposed/);
|
|
358
|
+
await disposing;
|
|
359
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
360
|
+
expect(f.listeners.size).toBe(0);
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
inventory.mockRestore();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
test("identical targets across providers and contexts; reversed install order", async () => {
|
|
367
|
+
const a = fixture();
|
|
368
|
+
const b = fixture();
|
|
369
|
+
const providers = [(0, utils_1.newid)(), (0, utils_1.newid)()];
|
|
370
|
+
for (const provider of providers)
|
|
371
|
+
await a.publish(definition(), provider);
|
|
372
|
+
for (const provider of [...providers].reverse())
|
|
373
|
+
await b.publish(definition(), provider);
|
|
374
|
+
expect([...a.rows.keys()].sort()).toEqual([...b.rows.keys()].sort());
|
|
375
|
+
await a.catalog.dispose();
|
|
376
|
+
await b.catalog.dispose();
|
|
377
|
+
});
|
|
378
|
+
test.each([false, true])("both aliases on fresh/upgraded database: %s", async (upgraded) => {
|
|
379
|
+
const old = legacy("00muxob3hbo3f6g49mipy1hyt");
|
|
380
|
+
const f = fixture(new Map(upgraded ? [[old.toolId, old]] : []));
|
|
381
|
+
await f.publish({ ...definition(["invoke"]), contractId: "00muxgugn6xvs8f2r80ru205t" });
|
|
382
|
+
expect(f.rows.has("00muxoa8xeet2lpi8bdq55q03")).toBe(true);
|
|
383
|
+
expect(f.rows.get(old.toolId)?.catalogIdentity).toBe("legacy-alias");
|
|
384
|
+
// No package claims the upgraded row, so the definition describes it; the
|
|
385
|
+
// primary key, legacy code and policy are preserved.
|
|
386
|
+
if (upgraded)
|
|
387
|
+
expect(f.rows.get(old.toolId)).toMatchObject({
|
|
388
|
+
...old,
|
|
389
|
+
name: "invoke",
|
|
390
|
+
usageDescription: "invoke",
|
|
391
|
+
inputSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
|
|
392
|
+
outputSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
|
|
393
|
+
});
|
|
394
|
+
expect([...f.rows.values()].filter((r) => r.toolName === "invoke")).toHaveLength(2);
|
|
395
|
+
await f.catalog.dispose();
|
|
396
|
+
});
|
|
397
|
+
test("backfills unknown legacy rows without inventing targets and quarantines unknown mappings", async () => {
|
|
398
|
+
const old = legacy();
|
|
399
|
+
const bad = { ...legacy(), contractId, contractVersion: 1, toolName: "read" };
|
|
400
|
+
const f = fixture(new Map([
|
|
401
|
+
[old.toolId, old],
|
|
402
|
+
[bad.toolId, bad],
|
|
403
|
+
]));
|
|
404
|
+
await f.catalog.ready();
|
|
405
|
+
expect(f.rows.get(old.toolId)).toMatchObject({ catalogState: "unresolved", code: "existing" });
|
|
406
|
+
expect(f.rows.get(bad.toolId)?.catalogState).toBe("quarantined");
|
|
407
|
+
await f.catalog.dispose();
|
|
408
|
+
});
|
|
409
|
+
test("persistence failures retry; replacement failure leaves active metadata untouched", async () => {
|
|
410
|
+
const f = fixture();
|
|
411
|
+
const provider = (0, utils_1.newid)();
|
|
412
|
+
await f.catalog.ready();
|
|
413
|
+
f.table.save.mockRejectedValueOnce(new Error("disk full"));
|
|
414
|
+
await expect(f.publish(definition(), provider)).rejects.toThrow("disk full");
|
|
415
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
416
|
+
await f.publish(definition(), provider);
|
|
417
|
+
const before = [...f.rows.values()];
|
|
418
|
+
await expect(f.publish(definition(["read", "new"]), provider, async () => {
|
|
419
|
+
throw new Error("prepare failed");
|
|
420
|
+
})).rejects.toThrow("prepare failed");
|
|
421
|
+
expect(f.registry.resolve(contractId, 1)?.tools.map((t) => t.name)).toEqual(["read"]);
|
|
422
|
+
for (const row of before)
|
|
423
|
+
expect(f.rows.get(row.toolId)).toEqual(row);
|
|
424
|
+
expect([...f.rows.values()].some((r) => r.toolName === "new")).toBe(true);
|
|
425
|
+
await f.catalog.dispose();
|
|
426
|
+
const restarted = fixture(f.rows);
|
|
427
|
+
await restarted.publish();
|
|
428
|
+
await restarted.catalog.dispose();
|
|
429
|
+
});
|
|
430
|
+
test("concurrent providers cannot activate before serialized preparation finishes", async () => {
|
|
431
|
+
const f = fixture();
|
|
432
|
+
const order = [];
|
|
433
|
+
let release;
|
|
434
|
+
const held = new Promise((resolve) => {
|
|
435
|
+
release = resolve;
|
|
436
|
+
});
|
|
437
|
+
const first = f.publish(definition(), (0, utils_1.newid)(), async () => {
|
|
438
|
+
order.push("first");
|
|
439
|
+
await held;
|
|
440
|
+
});
|
|
441
|
+
const second = f.publish(definition(), (0, utils_1.newid)(), async () => {
|
|
442
|
+
order.push("second");
|
|
443
|
+
});
|
|
444
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
445
|
+
expect(order).toEqual(["first"]);
|
|
446
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
447
|
+
release();
|
|
448
|
+
await Promise.all([first, second]);
|
|
449
|
+
expect(order).toEqual(["first", "second"]);
|
|
450
|
+
await f.catalog.dispose();
|
|
451
|
+
});
|
|
452
|
+
test("forced hash collision and insert-only loser fail closed", async () => {
|
|
453
|
+
const f = fixture();
|
|
454
|
+
await f.catalog.ready();
|
|
455
|
+
const hash = jest.spyOn(nacl, "hash").mockReturnValue(new Uint8Array(64));
|
|
456
|
+
try {
|
|
457
|
+
await expect(f.publish(definition(["one", "two"]))).rejects.toThrow(/collision/);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
hash.mockRestore();
|
|
461
|
+
}
|
|
462
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
463
|
+
f.table.save.mockImplementationOnce(async (row) => ({ ...row, toolName: "wrong" }));
|
|
464
|
+
await expect(f.publish()).rejects.toThrow(/identity cannot change/);
|
|
465
|
+
await f.catalog.dispose();
|
|
466
|
+
});
|
|
467
|
+
test("alsoImplements indexes only older public members, and removal retains history", async () => {
|
|
468
|
+
const f = fixture();
|
|
469
|
+
const provider = (0, utils_1.newid)();
|
|
470
|
+
const older = definition(["old"]);
|
|
471
|
+
const newer = definition(["old", "new"], 2);
|
|
472
|
+
f.registry.register((0, utils_1.newid)(), older);
|
|
473
|
+
const regs = [{ definition: newer, alsoImplements: [{ contractId, version: 1 }] }];
|
|
474
|
+
await f.catalog.publish(provider, regs, async () => { }, () => f.registry.replaceBatch(provider, regs));
|
|
475
|
+
expect([...f.rows.values()]
|
|
476
|
+
.filter((r) => r.contractId === contractId)
|
|
477
|
+
.map((r) => [r.contractVersion, r.toolName])
|
|
478
|
+
.sort()).toEqual([
|
|
479
|
+
[1, "old"],
|
|
480
|
+
[2, "new"],
|
|
481
|
+
[2, "old"],
|
|
482
|
+
]);
|
|
483
|
+
await f.publish(definition([], 2), provider);
|
|
484
|
+
f.registry.unregister(provider);
|
|
485
|
+
const count = f.rows.size;
|
|
486
|
+
await f.catalog.dispose();
|
|
487
|
+
expect(f.rows.size).toBe(count);
|
|
488
|
+
expect(f.listeners.size).toBe(0);
|
|
489
|
+
});
|
|
490
|
+
test("shared write guard rejects retargeting, stripping, deletion, and unverified aliases", () => {
|
|
491
|
+
const target = { contractId, version: 1, toolName: "read" };
|
|
492
|
+
const row = {
|
|
493
|
+
...legacy((0, tool_catalog_identity_1.deriveToolCatalogId)(target)),
|
|
494
|
+
contractId,
|
|
495
|
+
contractVersion: 1,
|
|
496
|
+
toolName: "read",
|
|
497
|
+
catalogState: "resolved",
|
|
498
|
+
};
|
|
499
|
+
for (const candidate of [
|
|
500
|
+
undefined,
|
|
501
|
+
legacy(row.toolId),
|
|
502
|
+
{ ...row, toolName: "other" },
|
|
503
|
+
{ ...row, toolId: (0, utils_1.newid)() },
|
|
504
|
+
])
|
|
505
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, candidate)).toThrow();
|
|
506
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, { ...row, usageDescription: "updated" })).not.toThrow();
|
|
507
|
+
});
|
|
508
|
+
test("pre-catalog full rows keep established identity; partial stripping still fails", () => {
|
|
509
|
+
const target = { contractId, version: 1, toolName: "read" };
|
|
510
|
+
const row = {
|
|
511
|
+
...legacy((0, tool_catalog_identity_1.deriveToolCatalogId)(target)),
|
|
512
|
+
contractId,
|
|
513
|
+
contractVersion: 1,
|
|
514
|
+
toolName: "read",
|
|
515
|
+
catalogIdentity: "deterministic",
|
|
516
|
+
catalogState: "quarantined",
|
|
517
|
+
catalogReason: "conflict",
|
|
518
|
+
};
|
|
519
|
+
const older = { ...legacy(row.toolId), usageDescription: "from an older peer" };
|
|
520
|
+
const retained = (0, tool_catalog_1.retainToolCatalogFields)(row, older);
|
|
521
|
+
expect(retained).toEqual({ ...row, usageDescription: "from an older peer" });
|
|
522
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, retained)).not.toThrow();
|
|
523
|
+
const stripped = { ...row, contractId: undefined };
|
|
524
|
+
expect((0, tool_catalog_1.retainToolCatalogFields)(row, stripped)).toBe(stripped);
|
|
525
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, stripped)).toThrow();
|
|
526
|
+
expect((0, tool_catalog_1.retainToolCatalogFields)(undefined, older)).toBe(older);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
test("Tools dataChanged reconciles new legacy evidence without recursive writes or provider activation", async () => {
|
|
530
|
+
const f = fixture();
|
|
531
|
+
await f.catalog.ready();
|
|
532
|
+
const row = legacy();
|
|
533
|
+
const count = f.table.save.mock.calls.length;
|
|
534
|
+
await f.table.save(row);
|
|
535
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
536
|
+
expect(f.rows.get(row.toolId)?.catalogState).toBe("unresolved");
|
|
537
|
+
expect(f.table.save.mock.calls.length - count).toBe(1);
|
|
538
|
+
expect(f.table.update).toHaveBeenCalledTimes(1);
|
|
539
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
540
|
+
await f.catalog.dispose();
|
|
541
|
+
});
|
|
542
|
+
test("reconciliation does not overwrite metadata changed after its snapshot", async () => {
|
|
543
|
+
const old = legacy();
|
|
544
|
+
const f = fixture(new Map([[old.toolId, old]]));
|
|
545
|
+
f.table.update.mockImplementationOnce(async (patch) => {
|
|
546
|
+
expect(Object.keys(patch).sort()).toEqual(["catalogState", "toolId"]);
|
|
547
|
+
const current = { ...old, usageDescription: "synced while reconciling", accessLevel: 90 };
|
|
548
|
+
const saved = { ...current, ...patch };
|
|
549
|
+
f.rows.set(old.toolId, saved);
|
|
550
|
+
return saved;
|
|
551
|
+
});
|
|
552
|
+
await f.catalog.ready();
|
|
553
|
+
expect(f.rows.get(old.toolId)).toMatchObject({
|
|
554
|
+
catalogState: "unresolved",
|
|
555
|
+
usageDescription: "synced while reconciling",
|
|
556
|
+
accessLevel: 90,
|
|
557
|
+
});
|
|
558
|
+
await f.catalog.dispose();
|
|
559
|
+
});
|
|
560
|
+
test("untargeted deterministic IDs and quarantined rows cannot be claimed by a provider", async () => {
|
|
561
|
+
const target = { contractId, version: 1, toolName: "read" };
|
|
562
|
+
const old = legacy((0, tool_catalog_identity_1.deriveToolCatalogId)(target));
|
|
563
|
+
const f = fixture(new Map([[old.toolId, old]]));
|
|
564
|
+
await f.catalog.ready();
|
|
565
|
+
expect(f.rows.get(old.toolId)?.catalogState).toBe("quarantined");
|
|
566
|
+
await expect(f.publish()).rejects.toThrow(/Quarantined/);
|
|
567
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
568
|
+
const row = { ...old, contractId, contractVersion: 1, toolName: "read" };
|
|
569
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(old, row)).toThrow(/cannot be claimed/);
|
|
570
|
+
expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...row, catalogState: "quarantined" }, { ...row, catalogState: "resolved" })).toThrow(/Quarantined/);
|
|
571
|
+
await f.catalog.dispose();
|
|
572
|
+
});
|
|
573
|
+
test("host removal waits for publication and disposal cancels queued activation", async () => {
|
|
574
|
+
const f = fixture();
|
|
575
|
+
const provider = (0, utils_1.newid)();
|
|
576
|
+
let release;
|
|
577
|
+
let prepared;
|
|
578
|
+
const preparing = new Promise((resolve) => {
|
|
579
|
+
prepared = resolve;
|
|
580
|
+
});
|
|
581
|
+
const held = new Promise((resolve) => {
|
|
582
|
+
release = resolve;
|
|
583
|
+
});
|
|
584
|
+
const installing = f.publish(definition(), provider, async () => {
|
|
585
|
+
prepared();
|
|
586
|
+
await held;
|
|
587
|
+
});
|
|
588
|
+
await preparing;
|
|
589
|
+
const deactivate = jest.fn();
|
|
590
|
+
const removing = f.catalog.removeProvider(provider, deactivate);
|
|
591
|
+
expect(deactivate).not.toHaveBeenCalled();
|
|
592
|
+
release();
|
|
593
|
+
await installing;
|
|
594
|
+
await removing;
|
|
595
|
+
expect(deactivate).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(f.registry.resolve(contractId, 1)).toBeUndefined();
|
|
597
|
+
expect(f.rows.has((0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }))).toBe(true);
|
|
598
|
+
await f.catalog.dispose();
|
|
599
|
+
await expect(f.publish()).rejects.toThrow(/disposed/);
|
|
600
|
+
});
|
|
601
|
+
test("activation guard rejects quarantine after asynchronous activation work", async () => {
|
|
602
|
+
const f = fixture();
|
|
603
|
+
const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
|
|
604
|
+
const commit = jest.fn();
|
|
605
|
+
try {
|
|
606
|
+
await expect(f.catalog.publish((0, utils_1.newid)(), [{ definition: definition() }], async () => { }, async (assertCurrent) => {
|
|
607
|
+
await f.table.update({ toolId, catalogState: "quarantined" });
|
|
608
|
+
assertCurrent();
|
|
609
|
+
commit();
|
|
610
|
+
})).rejects.toThrow(/quarantined/);
|
|
611
|
+
expect(commit).not.toHaveBeenCalled();
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
await f.catalog.dispose();
|
|
615
|
+
expect(f.listeners.size).toBe(0);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
test("removal requested during cold initialization follows the earlier publication", async () => {
|
|
619
|
+
const f = fixture();
|
|
620
|
+
const provider = (0, utils_1.newid)();
|
|
621
|
+
const installing = f.publish(definition(), provider);
|
|
622
|
+
const removing = f.catalog.removeProvider(provider, () => { });
|
|
623
|
+
await Promise.all([installing, removing]);
|
|
624
|
+
expect(f.registry.getProviderPackageId(contractId, 1)).toBeUndefined();
|
|
625
|
+
expect([...f.rows.values()].some((row) => row.contractId === contractId)).toBe(true);
|
|
626
|
+
await f.catalog.dispose();
|
|
627
|
+
});
|
|
628
|
+
test("ready upgrades unresolved host aliases, keeps policy, refreshes descriptions and restores missing rows", async () => {
|
|
629
|
+
const old = {
|
|
630
|
+
...legacy("000peers0tool0creategroup"),
|
|
631
|
+
accessLevel: 40,
|
|
632
|
+
catalogState: "unresolved",
|
|
633
|
+
};
|
|
634
|
+
const f = fixture(new Map([[old.toolId, old]]));
|
|
635
|
+
await f.catalog.ready();
|
|
636
|
+
expect(f.rows.get(old.toolId)).toMatchObject({
|
|
637
|
+
catalogState: "resolved",
|
|
638
|
+
catalogIdentity: "legacy-alias",
|
|
639
|
+
name: "create-group",
|
|
640
|
+
accessLevel: 40,
|
|
641
|
+
});
|
|
642
|
+
expect(f.rows.get(old.toolId)?.usageDescription).not.toBe(old.usageDescription);
|
|
643
|
+
f.rows.delete(old.toolId);
|
|
644
|
+
await f.catalog.dispose();
|
|
645
|
+
const restarted = fixture(f.rows);
|
|
646
|
+
await restarted.catalog.ready();
|
|
647
|
+
expect(restarted.rows.get(old.toolId)?.catalogState).toBe("resolved");
|
|
648
|
+
await restarted.catalog.dispose();
|
|
649
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../../index";
|