@deveye/types 0.15.2 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/domain/device.ts +4 -29
- package/src/domain/featureRegistry.ts +40 -108
- package/src/domain/home.ts +40 -101
- package/src/domain/live.ts +36 -87
- package/src/domain/metrics.ts +10 -14
- package/src/domain/notifications.ts +39 -110
- package/src/domain/project.ts +3 -162
- package/src/domain/report.ts +54 -95
- package/src/domain/secrecy.ts +3 -5
- package/src/domain/sharing.ts +33 -70
- package/src/domain/syncProtocol.ts +5 -12
- package/src/domain/user.ts +8 -14
- package/src/domain/workspace.ts +0 -3
- package/src/domain/workspaceRole.ts +34 -82
- package/src/features/agent.ts +306 -0
- package/src/features/live.ts +17 -37
- package/src/features/notify.ts +19 -57
- package/src/features/registry.ts +7 -44
- package/src/features/secrecy.ts +4 -9
- package/src/features/sharing.ts +12 -26
- package/src/features/user.ts +6 -11
- package/src/features/workspace.ts +6 -11
- package/src/http/auth.ts +6 -13
- package/src/http/device.ts +13 -17
- package/src/http/status.ts +6 -10
- package/src/index.ts +29 -932
- package/src/protocol/agent.ts +40 -70
- package/src/protocol/envelope.ts +3 -10
- package/src/sdk/client-ambient.d.ts +228 -21
- package/src/sdk/client.ts +277 -17
- package/src/sdk/devb.ts +120 -0
- package/src/sdk/manifest.test.ts +0 -1
- package/src/sdk/manifest.ts +94 -25
- package/src/sdk/providers.ts +310 -5
- package/src/sdk/server.ts +483 -19
- package/src/sdk/testing.test.ts +2 -2
- package/src/sdk/testing.ts +302 -26
- package/src/utils/version.ts +5 -8
- package/src/domain/audience.ts +0 -549
- package/src/domain/backup.ts +0 -355
- package/src/domain/credential.ts +0 -55
- package/src/domain/database.ts +0 -467
- package/src/domain/deploy.ts +0 -231
- package/src/domain/finance.ts +0 -477
- package/src/domain/git.ts +0 -419
- package/src/domain/mail.ts +0 -394
- package/src/domain/note.ts +0 -202
- package/src/domain/password.ts +0 -36
- package/src/domain/projectBoard.ts +0 -130
- package/src/domain/projectChat.ts +0 -46
- package/src/domain/projectHistory.ts +0 -82
- package/src/domain/projectLink.ts +0 -87
- package/src/domain/projectPlan.ts +0 -68
- package/src/domain/sentinel.ts +0 -623
- package/src/domain/uptime.ts +0 -216
- package/src/features/audience.ts +0 -275
- package/src/features/backup.ts +0 -230
- package/src/features/database.ts +0 -461
- package/src/features/deploy.ts +0 -245
- package/src/features/device.ts +0 -292
- package/src/features/deviceFiles.ts +0 -83
- package/src/features/deviceLogs.ts +0 -36
- package/src/features/deviceTerminal.ts +0 -57
- package/src/features/finance.ts +0 -360
- package/src/features/git.ts +0 -368
- package/src/features/mail.ts +0 -374
- package/src/features/metrics.ts +0 -185
- package/src/features/note.ts +0 -189
- package/src/features/password.ts +0 -67
- package/src/features/project.ts +0 -709
- package/src/features/sentinel.ts +0 -233
- package/src/features/uptime.ts +0 -186
package/src/sdk/testing.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { ZodType } from 'zod';
|
|
2
|
+
import type { ItemAccess } from '../domain/sharing';
|
|
2
3
|
import { resolveExtras, type FeatureManifest } from './manifest';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
import {
|
|
5
|
+
FeatureError,
|
|
6
|
+
type DevEyeFacade,
|
|
7
|
+
type FeatureServiceDeps,
|
|
8
|
+
type FeatureStore,
|
|
9
|
+
type SdkCipher,
|
|
10
|
+
type SdkDevice,
|
|
11
|
+
type SdkFeatureContext,
|
|
12
|
+
type SdkLogger,
|
|
13
|
+
type SdkProviders,
|
|
14
|
+
type SdkTelemetry,
|
|
15
|
+
type SdkTelemetrySnapshot,
|
|
16
|
+
type StorageEncryption,
|
|
17
|
+
SdkWorkspaceSummary
|
|
12
18
|
} from './server';
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -34,6 +40,17 @@ const identityCipher: SdkCipher = {
|
|
|
34
40
|
tryDecrypt: (blob) => Promise.resolve(blob)
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The guarded cipher of a SEALED session: `encrypt` and `decrypt` throw
|
|
45
|
+
* `locked`, `tryDecrypt` answers null. Handed out for `'private'` when the
|
|
46
|
+
* harness says `unlocked: false`.
|
|
47
|
+
*/
|
|
48
|
+
const sealedCipher: SdkCipher = {
|
|
49
|
+
encrypt: () => Promise.reject(new FeatureError('locked', 'Password encryption is locked')),
|
|
50
|
+
decrypt: () => Promise.reject(new FeatureError('locked', 'Password encryption is locked')),
|
|
51
|
+
tryDecrypt: () => Promise.resolve(null)
|
|
52
|
+
};
|
|
53
|
+
|
|
37
54
|
const silentLogger: SdkLogger = {
|
|
38
55
|
debug: () => undefined,
|
|
39
56
|
info: () => undefined,
|
|
@@ -78,37 +95,96 @@ function memoryStore(): TestFeatureStore {
|
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
export interface RecordedCalls {
|
|
81
|
-
notifications: {
|
|
98
|
+
notifications: {
|
|
99
|
+
subject: string;
|
|
100
|
+
body: string;
|
|
101
|
+
itemId?: number;
|
|
102
|
+
embeds?: number;
|
|
103
|
+
except?: readonly number[];
|
|
104
|
+
}[];
|
|
105
|
+
/** Live messages posted (`messageId: null`) or edited through `notify.postLive`. */
|
|
106
|
+
liveMessages: { channelId: number; messageId: string | null; embeds?: number }[];
|
|
82
107
|
audits: { action: string; description: string }[];
|
|
83
108
|
/** Outbound agent frames, as `{ method, deviceId }` (payloads dropped for brevity). */
|
|
84
109
|
agentRequests: { method: string; deviceId: string }[];
|
|
110
|
+
/** Instants pinned through `telemetry.pinInstant`. */
|
|
111
|
+
pinnedInstants: { deviceId: string; ts: number }[];
|
|
85
112
|
}
|
|
86
113
|
|
|
87
|
-
function recordingNotify(
|
|
114
|
+
function recordingNotify(
|
|
115
|
+
recorded: RecordedCalls,
|
|
116
|
+
hasRoute: boolean,
|
|
117
|
+
accepted: boolean,
|
|
118
|
+
liveChannels: readonly number[]
|
|
119
|
+
): DevEyeFacade['notify'] {
|
|
120
|
+
let posted = 0;
|
|
88
121
|
return {
|
|
89
122
|
hasRoute: () => Promise.resolve(hasRoute),
|
|
90
123
|
send(alert, opts) {
|
|
91
124
|
recorded.notifications.push({
|
|
92
125
|
subject: alert.subject,
|
|
93
126
|
body: alert.body,
|
|
94
|
-
itemId: opts?.itemId
|
|
127
|
+
itemId: opts?.itemId,
|
|
128
|
+
// Only when the alert carries a layout: a test that
|
|
129
|
+
// deep-equals the plain record must not see the key appear.
|
|
130
|
+
...(alert.embeds ? { embeds: alert.embeds.length } : {}),
|
|
131
|
+
...(opts?.except ? { except: opts.except } : {})
|
|
132
|
+
});
|
|
133
|
+
// Recorded either way (the module did try), but a refused delivery
|
|
134
|
+
// answers false, so a test sees what the module does with it.
|
|
135
|
+
return Promise.resolve(accepted);
|
|
136
|
+
},
|
|
137
|
+
liveChannels: () => Promise.resolve(liveChannels.map((id) => ({ id }))),
|
|
138
|
+
postLive(channelId, message, messageId) {
|
|
139
|
+
recorded.liveMessages.push({
|
|
140
|
+
channelId,
|
|
141
|
+
messageId: messageId ?? null,
|
|
142
|
+
...(message.embeds ? { embeds: message.embeds.length } : {})
|
|
95
143
|
});
|
|
96
|
-
|
|
144
|
+
// A post mints an id (`live-1`, `live-2`...), an edit keeps the
|
|
145
|
+
// one it was given; a refusing host answers null either way.
|
|
146
|
+
if (!accepted) return Promise.resolve(null);
|
|
147
|
+
return Promise.resolve(messageId ?? `live-${++posted}`);
|
|
97
148
|
}
|
|
98
149
|
};
|
|
99
150
|
}
|
|
100
151
|
|
|
152
|
+
/** A device the harness invents for an id nothing listed: active, online, unreported. */
|
|
153
|
+
export function testDevice(over: Partial<SdkDevice> & { id: string }): SdkDevice {
|
|
154
|
+
return {
|
|
155
|
+
name: 'Test device',
|
|
156
|
+
online: true,
|
|
157
|
+
status: 'active',
|
|
158
|
+
ownerUserId: 1,
|
|
159
|
+
workspaceId: 1,
|
|
160
|
+
metricIntervalSeconds: null,
|
|
161
|
+
report: null,
|
|
162
|
+
...over
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
101
166
|
function recordingDevices(devices: readonly SdkDevice[]): DevEyeFacade['devices'] {
|
|
102
167
|
return {
|
|
103
|
-
authorize: (id) =>
|
|
104
|
-
Promise.resolve(
|
|
105
|
-
devices.find((d) => d.id === id) ?? { id, name: 'Test device', online: true }
|
|
106
|
-
),
|
|
168
|
+
authorize: (id) => Promise.resolve(devices.find((d) => d.id === id) ?? testDevice({ id })),
|
|
107
169
|
list: () => Promise.resolve([...devices]),
|
|
108
170
|
isOnline: (id) => devices.find((d) => d.id === id)?.online ?? true
|
|
109
171
|
};
|
|
110
172
|
}
|
|
111
173
|
|
|
174
|
+
function recordingTelemetry(
|
|
175
|
+
recorded: RecordedCalls,
|
|
176
|
+
snapshots: readonly SdkTelemetrySnapshot[]
|
|
177
|
+
): SdkTelemetry {
|
|
178
|
+
return {
|
|
179
|
+
snapshot: (_deviceId, ts) =>
|
|
180
|
+
Promise.resolve(snapshots.find((s) => Math.abs(s.ts - ts) <= 1000) ?? null),
|
|
181
|
+
pinInstant(deviceId, ts) {
|
|
182
|
+
recorded.pinnedInstants.push({ deviceId, ts });
|
|
183
|
+
return Promise.resolve();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
112
188
|
function recordingAgents(recorded: RecordedCalls): DevEyeFacade['agents'] {
|
|
113
189
|
const req = (method: string) => (deviceId: string) => {
|
|
114
190
|
recorded.agentRequests.push({ method, deviceId });
|
|
@@ -116,6 +192,8 @@ function recordingAgents(recorded: RecordedCalls): DevEyeFacade['agents'] {
|
|
|
116
192
|
};
|
|
117
193
|
return {
|
|
118
194
|
isOnline: () => true,
|
|
195
|
+
requestScan: req('requestScan'),
|
|
196
|
+
pushConfig: (deviceId) => Promise.resolve(req('pushConfig')(deviceId)),
|
|
119
197
|
requestSyncConfig: req('requestSyncConfig'),
|
|
120
198
|
requestSyncScan: req('requestSyncScan'),
|
|
121
199
|
requestSyncPush: req('requestSyncPush'),
|
|
@@ -126,13 +204,46 @@ function recordingAgents(recorded: RecordedCalls): DevEyeFacade['agents'] {
|
|
|
126
204
|
requestSyncMove: req('requestSyncMove'),
|
|
127
205
|
requestSyncDelete: req('requestSyncDelete'),
|
|
128
206
|
publishSyncProgress: () => undefined,
|
|
129
|
-
publishSyncState: () => undefined
|
|
207
|
+
publishSyncState: () => undefined,
|
|
208
|
+
requestDestroy: req('requestDestroy'),
|
|
209
|
+
disconnectAgent: req('disconnectAgent'),
|
|
210
|
+
resetAgentSession: req('resetAgentSession'),
|
|
211
|
+
// Nothing synced: a test of the self-update flag feeds a manifest to
|
|
212
|
+
// the module's own pure helper.
|
|
213
|
+
servedManifest: () => Promise.resolve(null),
|
|
214
|
+
requestFilesMutate: req('requestFilesMutate'),
|
|
215
|
+
requestFilesUpload: req('requestFilesUpload'),
|
|
216
|
+
// Every file order succeeds at once: a test of what a module does
|
|
217
|
+
// with a refusal injects its own facade through `deveye`.
|
|
218
|
+
awaitFilesOp: () => Promise.resolve({ ok: true }),
|
|
219
|
+
cancelFilesOp: () => undefined,
|
|
220
|
+
buffered: () => 0
|
|
130
221
|
};
|
|
131
222
|
}
|
|
132
223
|
|
|
224
|
+
/**
|
|
225
|
+
* A deterministic stand-in for `keys.derive`: the same (salt, info) yields
|
|
226
|
+
* the same bytes, distinct pairs distinct bytes, and nothing here is secret.
|
|
227
|
+
*/
|
|
228
|
+
function fakeDerive(salt: string, info: string, length: number): Uint8Array {
|
|
229
|
+
const out = new Uint8Array(length);
|
|
230
|
+
const seed = `${salt}|${info}`;
|
|
231
|
+
for (let i = 0; i < length; i += 1) {
|
|
232
|
+
out[i] = (seed.charCodeAt(i % seed.length) * (i + 1)) & 0xff;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The named contracts a test hands to the module (`providers` override). */
|
|
238
|
+
function fakeProviders(table: Readonly<Record<string, unknown>>): SdkProviders {
|
|
239
|
+
return { get: <T>(key: string) => table[key] as T | undefined };
|
|
240
|
+
}
|
|
241
|
+
|
|
133
242
|
export interface TestContext<Repo> extends SdkFeatureContext<Repo> {
|
|
134
243
|
recorded: RecordedCalls;
|
|
135
244
|
store: TestFeatureStore;
|
|
245
|
+
/** Item ids passed to `items.forget`, in order. */
|
|
246
|
+
forgotten: number[];
|
|
136
247
|
}
|
|
137
248
|
|
|
138
249
|
export interface TestContextOverrides<Repo> {
|
|
@@ -141,6 +252,10 @@ export interface TestContextOverrides<Repo> {
|
|
|
141
252
|
workspaceId?: number;
|
|
142
253
|
kind?: 'personal' | 'shared';
|
|
143
254
|
isOwner?: boolean;
|
|
255
|
+
/** Global administrator. Default false. */
|
|
256
|
+
isAdmin?: boolean;
|
|
257
|
+
/** What `deveye.workspaces.list()` answers. Default none. */
|
|
258
|
+
workspaces?: readonly SdkWorkspaceSummary[];
|
|
144
259
|
canWrite?: boolean;
|
|
145
260
|
/** Extra permissions the caller holds, as the grant would carry them. */
|
|
146
261
|
extras?: Record<string, boolean | string>;
|
|
@@ -152,40 +267,135 @@ export interface TestContextOverrides<Repo> {
|
|
|
152
267
|
manifest?: Pick<FeatureManifest, 'extraPermissions'>;
|
|
153
268
|
/** What `deveye.notify.hasRoute` answers. Default true. */
|
|
154
269
|
hasRoute?: boolean;
|
|
270
|
+
/** What `deveye.notify.send` resolves (no usable channel: false). Default true; recorded either way. */
|
|
271
|
+
notifyAccepted?: boolean;
|
|
272
|
+
/** What `ctx.origins` answers. Default `https://deveye.test` / `https://public.deveye.test`. */
|
|
273
|
+
origins?: { app: string; public: string };
|
|
274
|
+
/** The channel ids `deveye.notify.liveChannels` lists. Default none. */
|
|
275
|
+
liveChannels?: readonly number[];
|
|
155
276
|
/** Devices `deveye.devices` reveals. Default none listed, any id authorized. */
|
|
156
277
|
devices?: readonly SdkDevice[];
|
|
278
|
+
/** Instants `deveye.telemetry.snapshot` answers (matched within a second). Default none. */
|
|
279
|
+
snapshots?: readonly SdkTelemetrySnapshot[];
|
|
157
280
|
/** Override facade members entirely when the defaults are not enough. */
|
|
158
281
|
deveye?: Partial<DevEyeFacade>;
|
|
282
|
+
/**
|
|
283
|
+
* What `secrecy.isUnlocked` answers. Default true. When false, the
|
|
284
|
+
* `'private'` cipher is sealed too (`decrypt` throws `locked`,
|
|
285
|
+
* `tryDecrypt` answers null), exactly like the app's guarded tier in a
|
|
286
|
+
* locked session; the `'server'` cipher stays the identity.
|
|
287
|
+
*/
|
|
288
|
+
unlocked?: boolean;
|
|
289
|
+
/** The caller's role restrictions on items, by item id. Default none. */
|
|
290
|
+
itemRestrictions?: Readonly<Record<number, ItemAccess>>;
|
|
291
|
+
/**
|
|
292
|
+
* Items projected INTO the workspace, as `itemId → home workspace id`.
|
|
293
|
+
* Default none: every item is at home. `sharing.scope().cipherFor` is the
|
|
294
|
+
* identity cipher either way.
|
|
295
|
+
*/
|
|
296
|
+
shares?: Readonly<Record<number, number>>;
|
|
297
|
+
/** The named contracts the host holds (`ctx.providers.get(key)`). */
|
|
298
|
+
providers?: Readonly<Record<string, unknown>>;
|
|
159
299
|
}
|
|
160
300
|
|
|
161
301
|
export function createTestContext<Repo = undefined>(
|
|
162
302
|
overrides: TestContextOverrides<Repo> = {}
|
|
163
303
|
): TestContext<Repo> {
|
|
164
|
-
const recorded: RecordedCalls = {
|
|
304
|
+
const recorded: RecordedCalls = {
|
|
305
|
+
notifications: [],
|
|
306
|
+
liveMessages: [],
|
|
307
|
+
audits: [],
|
|
308
|
+
agentRequests: [],
|
|
309
|
+
pinnedInstants: []
|
|
310
|
+
};
|
|
165
311
|
const isOwner = overrides.isOwner ?? true;
|
|
166
312
|
const workspaceId = overrides.workspaceId ?? 1;
|
|
313
|
+
const canWrite = overrides.canWrite ?? true;
|
|
167
314
|
const deveye: DevEyeFacade = {
|
|
168
|
-
notify: recordingNotify(
|
|
315
|
+
notify: recordingNotify(
|
|
316
|
+
recorded,
|
|
317
|
+
overrides.hasRoute ?? true,
|
|
318
|
+
overrides.notifyAccepted ?? true,
|
|
319
|
+
overrides.liveChannels ?? []
|
|
320
|
+
),
|
|
169
321
|
mail: { listAccounts: () => Promise.resolve([]) },
|
|
170
322
|
members: {
|
|
171
323
|
list: () =>
|
|
172
|
-
Promise.resolve([
|
|
324
|
+
Promise.resolve([
|
|
325
|
+
{ userId: overrides.userId ?? 1, name: 'Test', isOwner: true, color: null }
|
|
326
|
+
])
|
|
173
327
|
},
|
|
328
|
+
workspaces: { list: () => Promise.resolve(overrides.workspaces ?? []) },
|
|
174
329
|
devices: recordingDevices(overrides.devices ?? []),
|
|
330
|
+
telemetry: recordingTelemetry(recorded, overrides.snapshots ?? []),
|
|
175
331
|
agents: recordingAgents(recorded),
|
|
176
332
|
...overrides.deveye
|
|
177
333
|
};
|
|
334
|
+
const restrictions = new Map<number, ItemAccess>(
|
|
335
|
+
Object.entries(overrides.itemRestrictions ?? {}).map(([id, access]) => [Number(id), access])
|
|
336
|
+
);
|
|
337
|
+
const homes = new Map<number, number>(
|
|
338
|
+
Object.entries(overrides.shares ?? {}).map(([id, home]) => [Number(id), home])
|
|
339
|
+
);
|
|
340
|
+
const forgotten: number[] = [];
|
|
178
341
|
return {
|
|
179
342
|
recorded,
|
|
343
|
+
forgotten,
|
|
344
|
+
secrecy: {
|
|
345
|
+
isUnlocked: () => Promise.resolve(overrides.unlocked ?? true),
|
|
346
|
+
// Un ticket lisible tel quel : le harnais ne signe rien, il
|
|
347
|
+
// sérialise, et `createTestServiceDeps().secrecy.redeem` relit.
|
|
348
|
+
ticket: (payload) =>
|
|
349
|
+
Promise.resolve(
|
|
350
|
+
`ticket:${JSON.stringify({ userId: overrides.userId ?? 1, workspaceId, payload, unlocked: overrides.unlocked ?? true })}`
|
|
351
|
+
)
|
|
352
|
+
},
|
|
353
|
+
keys: {
|
|
354
|
+
sealBytes: (plain) => `sealed:${Buffer.from(plain).toString('base64')}`,
|
|
355
|
+
openBytes: () => null,
|
|
356
|
+
derive: fakeDerive
|
|
357
|
+
},
|
|
358
|
+
items: {
|
|
359
|
+
restrictions: () => Promise.resolve(restrictions),
|
|
360
|
+
// The exact rule of the app's dispatcher: the feature first (a
|
|
361
|
+
// restriction can only lower), then the row.
|
|
362
|
+
assert(itemId, level = 'read') {
|
|
363
|
+
if (level === 'write' && !canWrite) {
|
|
364
|
+
return Promise.reject(new FeatureError('forbidden', 'write required'));
|
|
365
|
+
}
|
|
366
|
+
const restriction = restrictions.get(itemId);
|
|
367
|
+
if (restriction === 'none') {
|
|
368
|
+
return Promise.reject(new FeatureError('forbidden', 'item hidden'));
|
|
369
|
+
}
|
|
370
|
+
if (restriction === 'read' && level === 'write') {
|
|
371
|
+
return Promise.reject(new FeatureError('forbidden', 'item read-only'));
|
|
372
|
+
}
|
|
373
|
+
return Promise.resolve();
|
|
374
|
+
},
|
|
375
|
+
forget(itemId) {
|
|
376
|
+
forgotten.push(itemId);
|
|
377
|
+
return Promise.resolve();
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
sharing: {
|
|
381
|
+
scope: () =>
|
|
382
|
+
Promise.resolve({
|
|
383
|
+
foreignIds: new Set(homes.keys()),
|
|
384
|
+
homeOf: (itemId) => homes.get(itemId) ?? null,
|
|
385
|
+
cipherFor: () => Promise.resolve(identityCipher)
|
|
386
|
+
})
|
|
387
|
+
},
|
|
180
388
|
userId: overrides.userId ?? 1,
|
|
181
389
|
workspaceId,
|
|
182
390
|
workspace: { id: workspaceId, kind: overrides.kind ?? 'personal', name: 'Test' },
|
|
183
391
|
isOwner,
|
|
392
|
+
isAdmin: overrides.isAdmin ?? false,
|
|
184
393
|
canWrite: overrides.canWrite ?? true,
|
|
185
394
|
...resolveExtras(overrides.manifest?.extraPermissions, isOwner, overrides.extras ?? {}),
|
|
186
395
|
repo: overrides.repo as Repo,
|
|
187
396
|
store: memoryStore(),
|
|
188
|
-
cipher: () =>
|
|
397
|
+
cipher: (mode) =>
|
|
398
|
+
mode === 'private' && overrides.unlocked === false ? sealedCipher : identityCipher,
|
|
189
399
|
deveye,
|
|
190
400
|
transport: {
|
|
191
401
|
subscribeSync: () => undefined,
|
|
@@ -193,17 +403,26 @@ export function createTestContext<Repo = undefined>(
|
|
|
193
403
|
sendSyncChunk: () => 0,
|
|
194
404
|
syncChunkBuffered: () => 0
|
|
195
405
|
},
|
|
406
|
+
providers: fakeProviders(overrides.providers ?? {}),
|
|
196
407
|
audit: (entry) => {
|
|
197
408
|
recorded.audits.push({ action: entry.action, description: entry.description });
|
|
198
409
|
},
|
|
199
410
|
logger: silentLogger,
|
|
200
|
-
requestId: 'test'
|
|
411
|
+
requestId: 'test',
|
|
412
|
+
origins: overrides.origins ?? {
|
|
413
|
+
app: 'https://deveye.test',
|
|
414
|
+
public: 'https://public.deveye.test'
|
|
415
|
+
}
|
|
201
416
|
};
|
|
202
417
|
}
|
|
203
418
|
|
|
204
419
|
export interface RecordedServiceCalls extends RecordedCalls {
|
|
205
420
|
/** Every `createTicker` call, so a test drives ticks by hand: `await tickers[0].tick()`. */
|
|
206
421
|
tickers: { intervalMs: number; tick(): Promise<void> }[];
|
|
422
|
+
/** Workspaces passed to `live.changed`, in order. */
|
|
423
|
+
liveChanges: number[];
|
|
424
|
+
/** `live.changed` calls that named topics, as `{ workspaceId, topics }` (a bare beat is not listed here). */
|
|
425
|
+
liveTopicChanges: { workspaceId: number; topics: readonly string[] }[];
|
|
207
426
|
}
|
|
208
427
|
|
|
209
428
|
export interface TestServiceDeps<Repo> extends FeatureServiceDeps<Repo> {
|
|
@@ -220,6 +439,16 @@ export interface TestServiceOverrides<Repo> {
|
|
|
220
439
|
devices?: readonly SdkDevice[];
|
|
221
440
|
/** What `deveyeFor(...).notify.hasRoute` answers. Default true. */
|
|
222
441
|
hasRoute?: boolean;
|
|
442
|
+
/** What `deveyeFor(...).notify.send` resolves. Default true; recorded either way. */
|
|
443
|
+
notifyAccepted?: boolean;
|
|
444
|
+
/** The channel ids `deveyeFor(...).notify.liveChannels` lists. Default none. */
|
|
445
|
+
liveChannels?: readonly number[];
|
|
446
|
+
/** What `deps.origins` answers. Default `https://deveye.test` / `https://public.deveye.test`. */
|
|
447
|
+
origins?: { app: string; public: string };
|
|
448
|
+
/** Instants `telemetry.snapshot` answers (matched within a second). Default none. */
|
|
449
|
+
snapshots?: readonly SdkTelemetrySnapshot[];
|
|
450
|
+
/** The named contracts the host holds (`deps.providers.get(key)`). */
|
|
451
|
+
providers?: Readonly<Record<string, unknown>>;
|
|
223
452
|
}
|
|
224
453
|
|
|
225
454
|
/**
|
|
@@ -232,13 +461,22 @@ export function createTestServiceDeps<Repo = undefined>(
|
|
|
232
461
|
): TestServiceDeps<Repo> {
|
|
233
462
|
const recorded: RecordedServiceCalls = {
|
|
234
463
|
notifications: [],
|
|
464
|
+
liveMessages: [],
|
|
235
465
|
audits: [],
|
|
236
466
|
agentRequests: [],
|
|
237
|
-
|
|
467
|
+
pinnedInstants: [],
|
|
468
|
+
tickers: [],
|
|
469
|
+
liveChanges: [],
|
|
470
|
+
liveTopicChanges: []
|
|
238
471
|
};
|
|
239
472
|
const stores = new Map<number, TestFeatureStore>();
|
|
240
473
|
const sealedBytes = new Map<string, Uint8Array>();
|
|
241
|
-
const notify = recordingNotify(
|
|
474
|
+
const notify = recordingNotify(
|
|
475
|
+
recorded,
|
|
476
|
+
overrides.hasRoute ?? true,
|
|
477
|
+
overrides.notifyAccepted ?? true,
|
|
478
|
+
overrides.liveChannels ?? []
|
|
479
|
+
);
|
|
242
480
|
const devices = recordingDevices(overrides.devices ?? []);
|
|
243
481
|
return {
|
|
244
482
|
recorded,
|
|
@@ -255,7 +493,43 @@ export function createTestServiceDeps<Repo = undefined>(
|
|
|
255
493
|
},
|
|
256
494
|
cipherFor: () => identityCipher,
|
|
257
495
|
deveyeFor: () => ({ notify }),
|
|
496
|
+
origins: overrides.origins ?? {
|
|
497
|
+
app: 'https://deveye.test',
|
|
498
|
+
public: 'https://public.deveye.test'
|
|
499
|
+
},
|
|
500
|
+
secrecy: {
|
|
501
|
+
redeem: (ticket) => {
|
|
502
|
+
if (!ticket.startsWith('ticket:')) return Promise.resolve(null);
|
|
503
|
+
const parsed = JSON.parse(ticket.slice('ticket:'.length)) as {
|
|
504
|
+
userId: number;
|
|
505
|
+
workspaceId: number;
|
|
506
|
+
payload: unknown;
|
|
507
|
+
unlocked: boolean;
|
|
508
|
+
};
|
|
509
|
+
return Promise.resolve({
|
|
510
|
+
userId: parsed.userId,
|
|
511
|
+
workspaceId: parsed.workspaceId,
|
|
512
|
+
payload: parsed.payload,
|
|
513
|
+
cipher: {
|
|
514
|
+
server: identityCipher,
|
|
515
|
+
private: parsed.unlocked ? identityCipher : null
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
},
|
|
258
520
|
devicesFor: () => ({ list: devices.list, isOnline: devices.isOnline }),
|
|
521
|
+
devices: {
|
|
522
|
+
find: (id) =>
|
|
523
|
+
Promise.resolve((overrides.devices ?? []).find((d) => d.id === id) ?? null),
|
|
524
|
+
isOnline: devices.isOnline
|
|
525
|
+
},
|
|
526
|
+
telemetry: recordingTelemetry(recorded, overrides.snapshots ?? []),
|
|
527
|
+
live: {
|
|
528
|
+
changed(workspaceId, topics) {
|
|
529
|
+
recorded.liveChanges.push(workspaceId);
|
|
530
|
+
if (topics) recorded.liveTopicChanges.push({ workspaceId, topics });
|
|
531
|
+
}
|
|
532
|
+
},
|
|
259
533
|
audit: (entry) => {
|
|
260
534
|
recorded.audits.push({ action: entry.action, description: entry.description });
|
|
261
535
|
},
|
|
@@ -268,8 +542,10 @@ export function createTestServiceDeps<Repo = undefined>(
|
|
|
268
542
|
sealedBytes.set(handle, Uint8Array.from(plain));
|
|
269
543
|
return handle;
|
|
270
544
|
},
|
|
271
|
-
openBytes: (sealed) => sealedBytes.get(sealed) ?? null
|
|
545
|
+
openBytes: (sealed) => sealedBytes.get(sealed) ?? null,
|
|
546
|
+
derive: fakeDerive
|
|
272
547
|
},
|
|
548
|
+
providers: fakeProviders(overrides.providers ?? {}),
|
|
273
549
|
createTicker({ intervalMs, tick }) {
|
|
274
550
|
recorded.tickers.push({ intervalMs, tick });
|
|
275
551
|
return { start: () => undefined, stop: () => undefined };
|
package/src/utils/version.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dotted-numeric version helpers shared by the server (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* on what "newer" means — a self-update is only ever offered/pushed as an UPGRADE,
|
|
6
|
-
* never a downgrade, even if the served manifest happens to lag a running agent.
|
|
2
|
+
* Dotted-numeric version helpers shared by the server (offers/pushes an agent
|
|
3
|
+
* self-update) and the web client (shows the affordance), so both agree on what
|
|
4
|
+
* "newer" means: a self-update is only ever an upgrade, never a downgrade.
|
|
7
5
|
*
|
|
8
|
-
* Non-numeric
|
|
9
|
-
*
|
|
10
|
-
* come from a single `package.json`, so plain dotted integers are enough.
|
|
6
|
+
* Non-numeric and missing segments count as 0, so `1.2` equals `1.2.0`. No
|
|
7
|
+
* pre-release handling: DevEye versions are plain dotted integers.
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
/** Compare dotted numeric versions: <0 if a<b, >0 if a>b, 0 if equal. */
|