@peers-app/peers-sdk 0.25.21 → 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-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-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 +674 -1
- 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
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const mockBeforeToolSave = jest.fn();
|
|
4
|
+
jest.mock("../data/tools", () => {
|
|
5
|
+
const actual = jest.requireActual("../data/tools");
|
|
6
|
+
const contexts = new WeakMap();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
Tools: (context) => {
|
|
10
|
+
let rows = contexts.get(context);
|
|
11
|
+
if (!rows) {
|
|
12
|
+
rows = new Map();
|
|
13
|
+
contexts.set(context, rows);
|
|
14
|
+
}
|
|
15
|
+
const records = rows;
|
|
16
|
+
return {
|
|
17
|
+
list: async () => [...records.values()],
|
|
18
|
+
get: async (id) => records.get(id),
|
|
19
|
+
save: async (row, opts) => {
|
|
20
|
+
await mockBeforeToolSave(row);
|
|
21
|
+
if (!opts?.insertOnly || !records.has(row.toolId))
|
|
22
|
+
records.set(row.toolId, row);
|
|
23
|
+
return records.get(row.toolId);
|
|
24
|
+
},
|
|
25
|
+
update: async (patch) => {
|
|
26
|
+
const row = { ...records.get(patch.toolId), ...patch };
|
|
27
|
+
records.set(patch.toolId, row);
|
|
28
|
+
return row;
|
|
29
|
+
},
|
|
30
|
+
dataChanged: { subscribe: () => ({ unsubscribe: () => true }) },
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
3
35
|
const mockGet = jest.fn();
|
|
4
36
|
const mockSignAndSave = jest.fn();
|
|
5
37
|
const mockComputePackageVersionHash = jest.fn((version, _tag, packageBundleFileHash, _routesBundleFileHash, _uiBundleFileHash) => `hash-${version}-${packageBundleFileHash}`);
|
|
@@ -10,14 +42,530 @@ jest.mock("../data/package-versions", () => ({
|
|
|
10
42
|
}),
|
|
11
43
|
computePackageVersionHash: (version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) => mockComputePackageVersionHash(version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash),
|
|
12
44
|
}));
|
|
45
|
+
const builder_1 = require("../contracts/builder");
|
|
46
|
+
const contract_provider_router_1 = require("../contracts/contract-provider-router");
|
|
47
|
+
const contract_proxy_1 = require("../contracts/contract-proxy");
|
|
13
48
|
const contract_resolvers_1 = require("../contracts/contract-resolvers");
|
|
49
|
+
const contract_transport_1 = require("../contracts/contract-transport");
|
|
14
50
|
const registry_1 = require("../contracts/registry");
|
|
51
|
+
const tool_catalog_identity_1 = require("../contracts/tool-catalog-identity");
|
|
15
52
|
const access_level_1 = require("../data/access-level");
|
|
53
|
+
const groupPermissions = require("../data/group-permissions");
|
|
54
|
+
const tools_1 = require("../data/tools");
|
|
16
55
|
const system_ids_1 = require("../system-ids");
|
|
17
56
|
const utils_1 = require("../utils");
|
|
18
57
|
const isolated_artifact_1 = require("./isolated-artifact");
|
|
19
58
|
const isolated_runtime_1 = require("./isolated-runtime");
|
|
20
59
|
const package_loader_1 = require("./package-loader");
|
|
60
|
+
const consumedContracts = require("./resolve-consumed-contracts");
|
|
61
|
+
describe("host load invalidation", () => {
|
|
62
|
+
it.each([
|
|
63
|
+
["contract", "quarantine"],
|
|
64
|
+
["contract", "unload"],
|
|
65
|
+
["legacy", "quarantine"],
|
|
66
|
+
["legacy", "unload"],
|
|
67
|
+
])("rejects %s tool invocation when %s completes during authorization", async (kind, action) => {
|
|
68
|
+
const packageId = (0, utils_1.newid)();
|
|
69
|
+
const contractId = (0, utils_1.newid)();
|
|
70
|
+
const context = { dataContextId: (0, utils_1.newid)() };
|
|
71
|
+
const loader = new package_loader_1.PackageLoader(context);
|
|
72
|
+
const toolFn = jest.fn(() => "must not execute");
|
|
73
|
+
loader.require = (() => {
|
|
74
|
+
const instance = {
|
|
75
|
+
tool: { name: "read", usageDescription: "Read", accessLevel: access_level_1.AccessLevel.Self },
|
|
76
|
+
toolFn,
|
|
77
|
+
};
|
|
78
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
|
|
79
|
+
contractId,
|
|
80
|
+
version: 1,
|
|
81
|
+
tools: { read: instance },
|
|
82
|
+
}));
|
|
83
|
+
return kind === "contract"
|
|
84
|
+
? (0, builder_1.definePackage)((pkg) => {
|
|
85
|
+
pkg.packageId = packageId;
|
|
86
|
+
pkg.version = "1.0.0";
|
|
87
|
+
pkg.contract(contractId, 1, "Authorization race").tools = [instance];
|
|
88
|
+
})
|
|
89
|
+
: { packageId };
|
|
90
|
+
});
|
|
91
|
+
let entered;
|
|
92
|
+
const waiting = new Promise((resolve) => {
|
|
93
|
+
entered = resolve;
|
|
94
|
+
});
|
|
95
|
+
let release;
|
|
96
|
+
const permission = jest
|
|
97
|
+
.spyOn(groupPermissions, "getUserAccessLevel")
|
|
98
|
+
.mockImplementationOnce(() => {
|
|
99
|
+
entered();
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
release = resolve;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
105
|
+
let endpoint;
|
|
106
|
+
try {
|
|
107
|
+
await loader._loadBundle({ packageId, name: "Authorization race" }, kind === "contract"
|
|
108
|
+
? 'module.exports.packageDefinition = require("fixture");'
|
|
109
|
+
: 'module.exports = require("fixture");', undefined, false, false);
|
|
110
|
+
// Legacy resolver-only bundles have no manifest to index; model an existing row.
|
|
111
|
+
const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
|
|
112
|
+
if (kind === "legacy")
|
|
113
|
+
await (0, tools_1.Tools)(context).save({
|
|
114
|
+
toolId,
|
|
115
|
+
name: "read",
|
|
116
|
+
code: "",
|
|
117
|
+
usageDescription: "Read",
|
|
118
|
+
inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
119
|
+
outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
120
|
+
contractId,
|
|
121
|
+
contractVersion: 1,
|
|
122
|
+
toolName: "read",
|
|
123
|
+
catalogIdentity: "deterministic",
|
|
124
|
+
catalogState: "resolved",
|
|
125
|
+
});
|
|
126
|
+
endpoint = await loader.resolveContractEndpoint({
|
|
127
|
+
contractId,
|
|
128
|
+
version: 1,
|
|
129
|
+
context: { dataContext: context, callerUserId: "self", localUserId: "self" },
|
|
130
|
+
notify: async () => undefined,
|
|
131
|
+
});
|
|
132
|
+
const call = endpoint.handleCall({
|
|
133
|
+
contractId,
|
|
134
|
+
version: 1,
|
|
135
|
+
member: "tool",
|
|
136
|
+
name: "read",
|
|
137
|
+
operation: "run",
|
|
138
|
+
args: [],
|
|
139
|
+
});
|
|
140
|
+
const rejection = expect(call).rejects.toThrow(action === "quarantine" ? /quarantined/i : /retired/);
|
|
141
|
+
await waiting;
|
|
142
|
+
if (action === "quarantine")
|
|
143
|
+
await (0, tools_1.Tools)(context).update({ toolId, catalogState: "quarantined" });
|
|
144
|
+
else
|
|
145
|
+
await loader.unloadPackage(packageId);
|
|
146
|
+
release(access_level_1.AccessLevel.Self);
|
|
147
|
+
await rejection;
|
|
148
|
+
expect(toolFn).not.toHaveBeenCalled();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
release?.(access_level_1.AccessLevel.Self);
|
|
152
|
+
endpoint?.dispose();
|
|
153
|
+
permission.mockRestore();
|
|
154
|
+
warn.mockRestore();
|
|
155
|
+
await loader.dispose();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
it("settles a fresh package's remote and consume handles after its own contract activates", async () => {
|
|
159
|
+
const packageId = (0, utils_1.newid)();
|
|
160
|
+
const contractId = (0, utils_1.newid)();
|
|
161
|
+
const context = { dataContextId: (0, utils_1.newid)() };
|
|
162
|
+
const loader = new package_loader_1.PackageLoader(context);
|
|
163
|
+
Object.assign(context, { packageLoader: loader });
|
|
164
|
+
let definition;
|
|
165
|
+
loader.require = (() => {
|
|
166
|
+
const instance = {
|
|
167
|
+
tool: { name: "read", usageDescription: "Read", accessLevel: access_level_1.AccessLevel.None },
|
|
168
|
+
toolFn: () => "own provider",
|
|
169
|
+
};
|
|
170
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
|
|
171
|
+
contractId,
|
|
172
|
+
version: 1,
|
|
173
|
+
tools: { read: instance },
|
|
174
|
+
}));
|
|
175
|
+
definition = (0, builder_1.definePackage)((pkg) => {
|
|
176
|
+
pkg.packageId = packageId;
|
|
177
|
+
pkg.version = "1.0.0";
|
|
178
|
+
pkg.contract(contractId, 1, "Own contract").tools = [instance];
|
|
179
|
+
pkg.remote(contractId, 1);
|
|
180
|
+
pkg.consumes(contractId, 1);
|
|
181
|
+
});
|
|
182
|
+
return definition;
|
|
183
|
+
});
|
|
184
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
185
|
+
try {
|
|
186
|
+
expect(loader.contractRegistry.getDefinition(contractId, 1)).toBeUndefined();
|
|
187
|
+
await expect(loader._loadBundle({ packageId, name: "Own contract" }, 'module.exports.packageDefinition = require("fixture");', undefined, false, false)).resolves.toMatchObject({ packageId });
|
|
188
|
+
const remoteHandle = definition.remotes?.[0].handle;
|
|
189
|
+
await expect(remoteHandle?.consumer).resolves.toMatchObject({ contractId, version: 1 });
|
|
190
|
+
const remote = (await remoteHandle?.consumer);
|
|
191
|
+
expect(typeof remote?.device((0, utils_1.newid)()).tools.read).toBe("function");
|
|
192
|
+
remote?.dispose();
|
|
193
|
+
const consumeHandle = definition.consumes[0].handle;
|
|
194
|
+
await expect(consumeHandle?.consumer).resolves.toMatchObject({ contractId, version: 1 });
|
|
195
|
+
const consumer = (await consumeHandle?.consumer);
|
|
196
|
+
await expect(consumer?.tools.read()).resolves.toBe("own provider");
|
|
197
|
+
consumer?.dispose();
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
warn.mockRestore();
|
|
201
|
+
await loader.dispose();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
it.each([
|
|
205
|
+
"persistence",
|
|
206
|
+
"evaluation",
|
|
207
|
+
"invalidation",
|
|
208
|
+
])("stages host resolvers until activation and retains the old implementation on %s failure", async (failure) => {
|
|
209
|
+
const packageId = (0, utils_1.newid)();
|
|
210
|
+
const contractId = (0, utils_1.newid)();
|
|
211
|
+
const definition = {
|
|
212
|
+
packageId,
|
|
213
|
+
version: "1.0.0",
|
|
214
|
+
contracts: [
|
|
215
|
+
{
|
|
216
|
+
contractId,
|
|
217
|
+
version: 1,
|
|
218
|
+
name: "Host",
|
|
219
|
+
description: "",
|
|
220
|
+
devTag: "dev",
|
|
221
|
+
tables: [],
|
|
222
|
+
observables: [],
|
|
223
|
+
tools: [{ name: "read", usageDescription: "", inputFields: [], outputFields: [] }],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
consumes: [],
|
|
227
|
+
alsoImplements: new Map(),
|
|
228
|
+
assistants: [],
|
|
229
|
+
appNavs: [],
|
|
230
|
+
tableDefinitions: [],
|
|
231
|
+
};
|
|
232
|
+
const context = { dataContextId: (0, utils_1.newid)() };
|
|
233
|
+
const otherContext = { dataContextId: (0, utils_1.newid)() };
|
|
234
|
+
const loader = new package_loader_1.PackageLoader(context);
|
|
235
|
+
const otherLoader = new package_loader_1.PackageLoader(otherContext);
|
|
236
|
+
let value = "OLD";
|
|
237
|
+
let failEvaluation = false;
|
|
238
|
+
let current = true;
|
|
239
|
+
const fixture = () => {
|
|
240
|
+
const captured = value;
|
|
241
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
|
|
242
|
+
contractId,
|
|
243
|
+
version: 1,
|
|
244
|
+
tools: {
|
|
245
|
+
read: {
|
|
246
|
+
tool: { name: "read", accessLevel: access_level_1.AccessLevel.None },
|
|
247
|
+
toolFn: () => captured,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
}), { providerPackageId: packageId });
|
|
251
|
+
if (failEvaluation)
|
|
252
|
+
throw new Error("evaluation failed");
|
|
253
|
+
return definition;
|
|
254
|
+
};
|
|
255
|
+
loader.require = fixture;
|
|
256
|
+
otherLoader.require = fixture;
|
|
257
|
+
const load = (target = loader) => target._loadBundle({ packageId, name: "Host" }, 'module.exports.packageDefinition = require("fixture");', undefined, false, true, () => {
|
|
258
|
+
if (!current)
|
|
259
|
+
throw new Error("load invalidated");
|
|
260
|
+
});
|
|
261
|
+
const call = async (target = loader) => {
|
|
262
|
+
const endpoint = await target.resolveContractEndpoint({
|
|
263
|
+
contractId,
|
|
264
|
+
version: 1,
|
|
265
|
+
context: { callerUserId: "self", localUserId: "self" },
|
|
266
|
+
notify: async () => undefined,
|
|
267
|
+
});
|
|
268
|
+
try {
|
|
269
|
+
return await endpoint.handleCall({
|
|
270
|
+
contractId,
|
|
271
|
+
version: 1,
|
|
272
|
+
member: "tool",
|
|
273
|
+
name: "read",
|
|
274
|
+
operation: "run",
|
|
275
|
+
args: [],
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
endpoint.dispose();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
283
|
+
let release = () => { };
|
|
284
|
+
let consumer;
|
|
285
|
+
let endpoint;
|
|
286
|
+
// External host transports own their routers, so loader-side invalidation
|
|
287
|
+
// cannot evict their cached endpoints directly.
|
|
288
|
+
const [clientTransport, serverTransport] = (0, contract_transport_1.inProcessTransportPair)();
|
|
289
|
+
const externalRouter = (0, contract_provider_router_1.createContractProviderRouter)(serverTransport, {
|
|
290
|
+
authorizeCall: () => ({ context: { dataContext: context } }),
|
|
291
|
+
resolveProviderPackageId: () => packageId,
|
|
292
|
+
resolveEndpoint: (request) => loader.resolveContractEndpoint(request),
|
|
293
|
+
});
|
|
294
|
+
const externalConsumer = (0, contract_proxy_1.createContractConsumer)(definition.contracts[0], clientTransport);
|
|
295
|
+
try {
|
|
296
|
+
await load();
|
|
297
|
+
consumer = await loader.resolveConsumedContract({ contractId, version: 1 });
|
|
298
|
+
expect(await consumer?.tools.read()).toBe("OLD");
|
|
299
|
+
expect(await externalConsumer.tools.read()).toBe("OLD");
|
|
300
|
+
endpoint = await loader.resolveContractEndpoint({
|
|
301
|
+
contractId,
|
|
302
|
+
version: 1,
|
|
303
|
+
context: {},
|
|
304
|
+
notify: async () => undefined,
|
|
305
|
+
});
|
|
306
|
+
expect(await call()).toBe("OLD");
|
|
307
|
+
value = "OTHER CONTEXT";
|
|
308
|
+
await load(otherLoader);
|
|
309
|
+
expect(await call()).toBe("OLD");
|
|
310
|
+
expect(await call(otherLoader)).toBe("OTHER CONTEXT");
|
|
311
|
+
value = "NEW";
|
|
312
|
+
let entered;
|
|
313
|
+
const waiting = new Promise((resolve) => {
|
|
314
|
+
entered = resolve;
|
|
315
|
+
});
|
|
316
|
+
const gate = new Promise((resolve) => {
|
|
317
|
+
release = resolve;
|
|
318
|
+
});
|
|
319
|
+
if (failure === "evaluation")
|
|
320
|
+
failEvaluation = true;
|
|
321
|
+
else
|
|
322
|
+
mockBeforeToolSave.mockImplementationOnce(async () => {
|
|
323
|
+
entered();
|
|
324
|
+
await gate;
|
|
325
|
+
if (failure === "persistence")
|
|
326
|
+
throw new Error("catalog disk failure");
|
|
327
|
+
current = false;
|
|
328
|
+
});
|
|
329
|
+
const loading = load();
|
|
330
|
+
if (failure !== "evaluation") {
|
|
331
|
+
await waiting;
|
|
332
|
+
// Candidate resolver registrations are invisible even while persistence waits.
|
|
333
|
+
expect(await call()).toBe("OLD");
|
|
334
|
+
release();
|
|
335
|
+
}
|
|
336
|
+
expect(await loading).toBeUndefined();
|
|
337
|
+
expect(await consumer?.tools.read()).toBe("OLD");
|
|
338
|
+
expect(await call()).toBe("OLD");
|
|
339
|
+
expect(await call(otherLoader)).toBe("OTHER CONTEXT");
|
|
340
|
+
expect((0, contract_resolvers_1.getContractResolver)(contractId, 1, packageId)).toBeUndefined();
|
|
341
|
+
current = true;
|
|
342
|
+
failEvaluation = false;
|
|
343
|
+
mockBeforeToolSave.mockReset();
|
|
344
|
+
await load();
|
|
345
|
+
expect(await call()).toBe("NEW");
|
|
346
|
+
expect(await consumer?.tools.read()).toBe("NEW");
|
|
347
|
+
expect(await externalConsumer.tools.read()).toBe("NEW");
|
|
348
|
+
await expect(endpoint.handleCall({
|
|
349
|
+
contractId,
|
|
350
|
+
version: 1,
|
|
351
|
+
member: "tool",
|
|
352
|
+
name: "read",
|
|
353
|
+
operation: "run",
|
|
354
|
+
args: [],
|
|
355
|
+
})).rejects.toThrow(/retired/);
|
|
356
|
+
await loader.unloadPackage(packageId);
|
|
357
|
+
await expect(consumer?.tools.read()).rejects.toThrow(/not a registered provider/);
|
|
358
|
+
await expect(externalConsumer.tools.read()).rejects.toThrow(/not a registered provider/);
|
|
359
|
+
expect((0, contract_resolvers_1.getContractResolver)(contractId, 1, packageId, context)).toBeUndefined();
|
|
360
|
+
expect(await call(otherLoader)).toBe("OTHER CONTEXT");
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
release();
|
|
364
|
+
consumer?.dispose();
|
|
365
|
+
externalConsumer.dispose();
|
|
366
|
+
externalRouter.dispose();
|
|
367
|
+
endpoint?.dispose();
|
|
368
|
+
mockBeforeToolSave.mockReset();
|
|
369
|
+
warn.mockRestore();
|
|
370
|
+
await loader.dispose();
|
|
371
|
+
await otherLoader.dispose();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
it("retires cached legacy endpoints even when a bundle reuses its resolver function", async () => {
|
|
375
|
+
const packageId = (0, utils_1.newid)();
|
|
376
|
+
const contractId = (0, utils_1.newid)();
|
|
377
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
378
|
+
let value = "OLD";
|
|
379
|
+
const resolver = () => {
|
|
380
|
+
const captured = value;
|
|
381
|
+
return {
|
|
382
|
+
contractId,
|
|
383
|
+
version: 1,
|
|
384
|
+
tools: {
|
|
385
|
+
read: {
|
|
386
|
+
tool: { name: "read", accessLevel: access_level_1.AccessLevel.None },
|
|
387
|
+
toolFn: () => captured,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
loader.require = (() => {
|
|
393
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, resolver);
|
|
394
|
+
return { packageId };
|
|
395
|
+
});
|
|
396
|
+
const load = () => loader._loadBundle({ packageId, name: "legacy" }, 'module.exports = require("fixture");', undefined, false, true);
|
|
397
|
+
const resolve = () => loader.resolveContractEndpoint({
|
|
398
|
+
contractId,
|
|
399
|
+
version: 1,
|
|
400
|
+
context: {},
|
|
401
|
+
notify: async () => undefined,
|
|
402
|
+
});
|
|
403
|
+
const call = (endpoint) => endpoint.handleCall({
|
|
404
|
+
contractId,
|
|
405
|
+
version: 1,
|
|
406
|
+
member: "tool",
|
|
407
|
+
name: "read",
|
|
408
|
+
operation: "run",
|
|
409
|
+
args: [],
|
|
410
|
+
});
|
|
411
|
+
const endpoints = [];
|
|
412
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
413
|
+
let release = () => { };
|
|
414
|
+
try {
|
|
415
|
+
await load();
|
|
416
|
+
const old = await resolve();
|
|
417
|
+
endpoints.push(old);
|
|
418
|
+
expect(await call(old)).toBe("OLD");
|
|
419
|
+
value = "NEW";
|
|
420
|
+
await load();
|
|
421
|
+
await expect(call(old)).rejects.toThrow(/retired/);
|
|
422
|
+
const current = await resolve();
|
|
423
|
+
endpoints.push(current);
|
|
424
|
+
expect(await call(current)).toBe("NEW");
|
|
425
|
+
let entered;
|
|
426
|
+
const waiting = new Promise((resolve) => {
|
|
427
|
+
entered = resolve;
|
|
428
|
+
});
|
|
429
|
+
const gate = new Promise((resolve) => {
|
|
430
|
+
release = resolve;
|
|
431
|
+
});
|
|
432
|
+
const assertTarget = loader.toolCatalog.assertTargetUsable.bind(loader.toolCatalog);
|
|
433
|
+
jest
|
|
434
|
+
.spyOn(loader.toolCatalog, "assertTargetUsable")
|
|
435
|
+
.mockImplementationOnce(async (target) => {
|
|
436
|
+
await assertTarget(target);
|
|
437
|
+
entered();
|
|
438
|
+
await gate;
|
|
439
|
+
});
|
|
440
|
+
const pending = call(current);
|
|
441
|
+
const rejected = expect(pending).rejects.toThrow(/retired/);
|
|
442
|
+
await waiting;
|
|
443
|
+
await loader.unloadPackage(packageId);
|
|
444
|
+
release();
|
|
445
|
+
await rejected;
|
|
446
|
+
await expect(call(current)).rejects.toThrow(/retired/);
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
release();
|
|
450
|
+
for (const endpoint of endpoints)
|
|
451
|
+
endpoint.dispose();
|
|
452
|
+
await loader.dispose();
|
|
453
|
+
warn.mockRestore();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
it("loads a tool-free legacy bundle and rejects executable replacement without side effects", async () => {
|
|
457
|
+
const packageId = (0, utils_1.newid)();
|
|
458
|
+
const contractId = (0, utils_1.newid)();
|
|
459
|
+
const context = {
|
|
460
|
+
dataContextId: (0, utils_1.newid)(),
|
|
461
|
+
tableContainer: { registerTableDefinitions: jest.fn() },
|
|
462
|
+
};
|
|
463
|
+
const loader = new package_loader_1.PackageLoader(context);
|
|
464
|
+
const original = { packageId, appNavs: [] };
|
|
465
|
+
let bundle = original;
|
|
466
|
+
loader.require = ((id) => id === "@peers-app/peers-sdk"
|
|
467
|
+
? { registerContractResolver: contract_resolvers_1.registerContractResolver }
|
|
468
|
+
: bundle);
|
|
469
|
+
const load = (code = 'module.exports = require("fixture");') => loader._loadBundle({ packageId, name: "legacy" }, code, undefined, false, true);
|
|
470
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
471
|
+
try {
|
|
472
|
+
expect(await load()).toBe(original);
|
|
473
|
+
const before = await (0, tools_1.Tools)(context).list();
|
|
474
|
+
bundle = {
|
|
475
|
+
packageId,
|
|
476
|
+
tableDefinitions: [{}],
|
|
477
|
+
toolInstances: [{ tool: { toolId: (0, utils_1.newid)() }, toolFn: jest.fn() }],
|
|
478
|
+
};
|
|
479
|
+
expect(await load(`require("@peers-app/peers-sdk").registerContractResolver("${contractId}", 1, () => ({})); module.exports = require("fixture");`)).toBeUndefined();
|
|
480
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Failed to evaluate"), expect.stringContaining("ships legacy executable tools"));
|
|
481
|
+
expect(await (0, tools_1.Tools)(context).list()).toEqual(before);
|
|
482
|
+
expect(context.tableContainer.registerTableDefinitions).not.toHaveBeenCalled();
|
|
483
|
+
expect((0, contract_resolvers_1.getContractResolver)(contractId, 1, packageId, context)).toBeUndefined();
|
|
484
|
+
expect(loader.packageInstances[packageId]).toBe(original);
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
warn.mockRestore();
|
|
488
|
+
await loader.dispose();
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
it.each([
|
|
492
|
+
"dependencies",
|
|
493
|
+
"preparation",
|
|
494
|
+
])("unload invalidates a host load waiting on %s", async (boundary) => {
|
|
495
|
+
const packageId = (0, utils_1.newid)();
|
|
496
|
+
const contractId = (0, utils_1.newid)();
|
|
497
|
+
const context = {
|
|
498
|
+
dataContextId: (0, utils_1.newid)(),
|
|
499
|
+
tableContainer: { registerTableDefinition: jest.fn() },
|
|
500
|
+
};
|
|
501
|
+
const loader = new package_loader_1.PackageLoader(context);
|
|
502
|
+
const definition = {
|
|
503
|
+
packageId,
|
|
504
|
+
version: "1.0.0",
|
|
505
|
+
contracts: [
|
|
506
|
+
{
|
|
507
|
+
contractId,
|
|
508
|
+
version: 1,
|
|
509
|
+
name: "Pending",
|
|
510
|
+
description: "",
|
|
511
|
+
tables: [],
|
|
512
|
+
observables: [],
|
|
513
|
+
tools: [],
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
consumes: [],
|
|
517
|
+
alsoImplements: new Map(),
|
|
518
|
+
assistants: [],
|
|
519
|
+
appNavs: [],
|
|
520
|
+
tableDefinitions: [],
|
|
521
|
+
};
|
|
522
|
+
loader.require = (() => definition);
|
|
523
|
+
let release;
|
|
524
|
+
let entered;
|
|
525
|
+
const waiting = new Promise((resolve) => {
|
|
526
|
+
entered = resolve;
|
|
527
|
+
});
|
|
528
|
+
const gate = new Promise((resolve) => {
|
|
529
|
+
release = resolve;
|
|
530
|
+
});
|
|
531
|
+
const hold = async () => {
|
|
532
|
+
entered();
|
|
533
|
+
await gate;
|
|
534
|
+
};
|
|
535
|
+
const dependencySpy = jest
|
|
536
|
+
.spyOn(consumedContracts, "resolveConsumedContracts")
|
|
537
|
+
.mockResolvedValue(undefined);
|
|
538
|
+
const preparationSpy = jest
|
|
539
|
+
.spyOn(loader, "correctPackageVersion")
|
|
540
|
+
.mockResolvedValue(undefined);
|
|
541
|
+
if (boundary === "dependencies")
|
|
542
|
+
dependencySpy.mockImplementationOnce(hold);
|
|
543
|
+
else
|
|
544
|
+
preparationSpy.mockImplementationOnce(hold);
|
|
545
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
546
|
+
const load = () => loader._loadBundle({ packageId, name: "Pending" }, 'module.exports.packageDefinition = require("fixture");', undefined, false, false);
|
|
547
|
+
try {
|
|
548
|
+
const installing = load();
|
|
549
|
+
await waiting;
|
|
550
|
+
const removing = loader.unloadPackage(packageId);
|
|
551
|
+
if (boundary === "dependencies")
|
|
552
|
+
await removing;
|
|
553
|
+
release();
|
|
554
|
+
await Promise.all([installing, removing]);
|
|
555
|
+
expect(loader.contractRegistry.getProviderPackageId(contractId, 1)).toBeUndefined();
|
|
556
|
+
expect(loader.isContractInstalled(packageId)).toBe(false);
|
|
557
|
+
await load();
|
|
558
|
+
expect(loader.contractRegistry.getProviderPackageId(contractId, 1)).toBe(packageId);
|
|
559
|
+
}
|
|
560
|
+
finally {
|
|
561
|
+
release();
|
|
562
|
+
dependencySpy.mockRestore();
|
|
563
|
+
preparationSpy.mockRestore();
|
|
564
|
+
warn.mockRestore();
|
|
565
|
+
await loader.dispose();
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
});
|
|
21
569
|
describe("PackageLoader.correctPackageVersion", () => {
|
|
22
570
|
const activePvId = "pv-active";
|
|
23
571
|
const pkg = {
|
|
@@ -88,8 +636,10 @@ describe("PackageLoader.contractRegistry", () => {
|
|
|
88
636
|
expect(loaderB.contractRegistry).toBeInstanceOf(registry_1.ContractRegistry);
|
|
89
637
|
expect(loaderA.contractRegistry).not.toBe(loaderB.contractRegistry);
|
|
90
638
|
});
|
|
91
|
-
it("installs system contracts
|
|
639
|
+
it("installs system contracts after explicit readiness", async () => {
|
|
92
640
|
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-system" });
|
|
641
|
+
expect(loader.contractRegistry.resolve(system_ids_1.logsContractId, 1)).toBeUndefined();
|
|
642
|
+
await loader.catalogReady;
|
|
93
643
|
expect(loader.contractRegistry.getProviderPackageId(system_ids_1.logsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
|
|
94
644
|
expect(loader.contractRegistry.resolve(system_ids_1.logsContractId, 1)?.name).toBe("System Logs");
|
|
95
645
|
expect(loader.contractRegistry.getProviderPackageId(system_ids_1.deviceOperationsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
|
|
@@ -143,6 +693,51 @@ describe("PackageLoader.contractRegistry", () => {
|
|
|
143
693
|
expect(resolver).toHaveBeenCalledWith({ dataContext });
|
|
144
694
|
endpoint.dispose();
|
|
145
695
|
});
|
|
696
|
+
it("checks retained identity evidence even for an unscoped legacy resolver", async () => {
|
|
697
|
+
const dataContext = { dataContextId: "ctx-legacy-quarantine" };
|
|
698
|
+
const loader = new package_loader_1.PackageLoader(dataContext);
|
|
699
|
+
const contractId = (0, utils_1.newid)();
|
|
700
|
+
const toolFn = jest.fn();
|
|
701
|
+
(0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
|
|
702
|
+
contractId,
|
|
703
|
+
version: 1,
|
|
704
|
+
tools: { read: { toolFn, tool: {} } },
|
|
705
|
+
}));
|
|
706
|
+
const endpoint = await loader.resolveContractEndpoint({
|
|
707
|
+
contractId,
|
|
708
|
+
version: 1,
|
|
709
|
+
context: {},
|
|
710
|
+
notify: async () => undefined,
|
|
711
|
+
});
|
|
712
|
+
const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
|
|
713
|
+
await (0, tools_1.Tools)(dataContext).save({
|
|
714
|
+
toolId,
|
|
715
|
+
contractId,
|
|
716
|
+
contractVersion: 1,
|
|
717
|
+
toolName: "read",
|
|
718
|
+
name: "read",
|
|
719
|
+
usageDescription: "",
|
|
720
|
+
code: "",
|
|
721
|
+
inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
722
|
+
outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
|
|
723
|
+
catalogState: "quarantined",
|
|
724
|
+
});
|
|
725
|
+
try {
|
|
726
|
+
await expect(endpoint.handleCall({
|
|
727
|
+
contractId,
|
|
728
|
+
version: 1,
|
|
729
|
+
member: "tool",
|
|
730
|
+
name: "read",
|
|
731
|
+
operation: "run",
|
|
732
|
+
args: [],
|
|
733
|
+
})).rejects.toThrow(/Quarantined/);
|
|
734
|
+
expect(toolFn).not.toHaveBeenCalled();
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
endpoint.dispose();
|
|
738
|
+
await loader.dispose();
|
|
739
|
+
}
|
|
740
|
+
});
|
|
146
741
|
it("does not supply local identity to ordinary callers that omit it", async () => {
|
|
147
742
|
const userId = (0, utils_1.newid)();
|
|
148
743
|
const dataContext = {
|
|
@@ -252,6 +847,84 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
252
847
|
(0, isolated_runtime_1.setIsolatedPackageRuntimeFactory)(() => runtime);
|
|
253
848
|
return runtime;
|
|
254
849
|
}
|
|
850
|
+
it.each([
|
|
851
|
+
"unload",
|
|
852
|
+
"dispose",
|
|
853
|
+
])("%s invalidates isolated loads awaiting dev version correction", async (action) => {
|
|
854
|
+
const runtime = makeRuntime();
|
|
855
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
856
|
+
let release;
|
|
857
|
+
let entered;
|
|
858
|
+
const gate = new Promise((resolve) => {
|
|
859
|
+
release = resolve;
|
|
860
|
+
});
|
|
861
|
+
const waiting = new Promise((resolve) => {
|
|
862
|
+
entered = resolve;
|
|
863
|
+
});
|
|
864
|
+
mockSignAndSave.mockImplementationOnce(async () => {
|
|
865
|
+
entered();
|
|
866
|
+
await gate;
|
|
867
|
+
});
|
|
868
|
+
const load = () => loader._loadBundle(pkg, (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact), selectedVersion({ version: "0.0.1", versionTag: "dev" }), true, true);
|
|
869
|
+
try {
|
|
870
|
+
const installing = load();
|
|
871
|
+
await waiting;
|
|
872
|
+
if (action === "unload")
|
|
873
|
+
await loader.unloadPackage(packageId);
|
|
874
|
+
else
|
|
875
|
+
await loader.dispose();
|
|
876
|
+
release();
|
|
877
|
+
await expect(installing).rejects.toThrow(/invalidated|disposed/);
|
|
878
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
879
|
+
expect(runtime.replace).not.toHaveBeenCalled();
|
|
880
|
+
expect(runtime.isInstalled(packageId)).toBe(false);
|
|
881
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
882
|
+
expect(loader.isContractInstalled(packageId)).toBe(false);
|
|
883
|
+
if (action === "unload") {
|
|
884
|
+
await load();
|
|
885
|
+
expect(runtime.isInstalled(packageId)).toBe(true);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
finally {
|
|
889
|
+
release();
|
|
890
|
+
await loader.dispose();
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
it("does not restore activation bookkeeping when unload overlaps an isolated install", async () => {
|
|
894
|
+
const runtime = makeRuntime();
|
|
895
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
|
|
896
|
+
let release;
|
|
897
|
+
let entered;
|
|
898
|
+
const gate = new Promise((resolve) => {
|
|
899
|
+
release = resolve;
|
|
900
|
+
});
|
|
901
|
+
const waiting = new Promise((resolve) => {
|
|
902
|
+
entered = resolve;
|
|
903
|
+
});
|
|
904
|
+
const install = runtime.install.getMockImplementation();
|
|
905
|
+
if (!install)
|
|
906
|
+
throw new Error("Missing runtime install fixture");
|
|
907
|
+
runtime.install.mockImplementationOnce(async (next) => {
|
|
908
|
+
const result = await install(next);
|
|
909
|
+
entered();
|
|
910
|
+
await gate;
|
|
911
|
+
return result;
|
|
912
|
+
});
|
|
913
|
+
const installing = loader._loadBundle(pkg, (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact), selectedVersion(), false, true);
|
|
914
|
+
try {
|
|
915
|
+
await waiting;
|
|
916
|
+
await loader.unloadPackage(packageId);
|
|
917
|
+
release();
|
|
918
|
+
await expect(installing).rejects.toThrow(/invalidated/);
|
|
919
|
+
expect(loader.isIsolatedInstalled(packageId)).toBe(false);
|
|
920
|
+
expect(loader.isContractInstalled(packageId)).toBe(false);
|
|
921
|
+
expect(runtime.isInstalled(packageId)).toBe(false);
|
|
922
|
+
}
|
|
923
|
+
finally {
|
|
924
|
+
release();
|
|
925
|
+
await loader.dispose();
|
|
926
|
+
}
|
|
927
|
+
});
|
|
255
928
|
it.each([
|
|
256
929
|
undefined,
|
|
257
930
|
"sandbox",
|