@selvajs/platform 0.14.2 → 0.15.0-beta.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/LICENSE +1 -1
- package/dist/auth.d.ts +60 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +12 -0
- package/dist/auth.js.map +1 -0
- package/dist/compute.d.ts +47 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +11 -0
- package/dist/compute.js.map +1 -0
- package/dist/computeServer/index.d.ts +2 -0
- package/dist/computeServer/index.d.ts.map +1 -1
- package/dist/computeServer/index.js +6 -0
- package/dist/computeServer/index.js.map +1 -1
- package/dist/computeServer/interface.d.ts +10 -0
- package/dist/computeServer/interface.d.ts.map +1 -1
- package/dist/computeServer/secrets.d.ts +53 -0
- package/dist/computeServer/secrets.d.ts.map +1 -0
- package/dist/computeServer/secrets.js +67 -0
- package/dist/computeServer/secrets.js.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -1
- package/dist/data/interface.d.ts +33 -0
- package/dist/data/interface.d.ts.map +1 -1
- package/dist/definitions/interface.d.ts +18 -0
- package/dist/definitions/interface.d.ts.map +1 -1
- package/dist/definitions/schemas.d.ts +1 -0
- package/dist/definitions/schemas.d.ts.map +1 -1
- package/dist/definitions/schemas.js +3 -0
- package/dist/definitions/schemas.js.map +1 -1
- package/dist/definitions/service.d.ts +62 -0
- package/dist/definitions/service.d.ts.map +1 -0
- package/dist/definitions/service.js +149 -0
- package/dist/definitions/service.js.map +1 -0
- package/dist/definitions/types.d.ts +24 -1
- package/dist/definitions/types.d.ts.map +1 -1
- package/dist/definitions.d.ts +146 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +23 -0
- package/dist/definitions.js.map +1 -0
- package/dist/errors/interface.d.ts +44 -0
- package/dist/errors/interface.d.ts.map +1 -0
- package/dist/errors/interface.js +9 -0
- package/dist/errors/interface.js.map +1 -0
- package/dist/events/interface.d.ts +23 -0
- package/dist/events/interface.d.ts.map +1 -1
- package/dist/events/interface.js.map +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/invites/schemas.d.ts +24 -0
- package/dist/invites/schemas.d.ts.map +1 -0
- package/dist/invites/schemas.js +17 -0
- package/dist/invites/schemas.js.map +1 -0
- package/dist/metrics/interface.d.ts +6 -0
- package/dist/metrics/interface.d.ts.map +1 -1
- package/dist/metrics/interface.js.map +1 -1
- package/dist/organizations/index.d.ts +4 -3
- package/dist/organizations/index.d.ts.map +1 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/index.js.map +1 -1
- package/dist/organizations/interface.d.ts +1 -1
- package/dist/organizations/interface.d.ts.map +1 -1
- package/dist/organizations/paths.d.ts +22 -0
- package/dist/organizations/paths.d.ts.map +1 -0
- package/dist/organizations/paths.js +43 -0
- package/dist/organizations/paths.js.map +1 -0
- package/dist/organizations/schemas.d.ts +10 -0
- package/dist/organizations/schemas.d.ts.map +1 -1
- package/dist/organizations/schemas.js +6 -0
- package/dist/organizations/schemas.js.map +1 -1
- package/dist/organizations/types.d.ts +10 -1
- package/dist/organizations/types.d.ts.map +1 -1
- package/dist/organizations.d.ts +93 -0
- package/dist/organizations.d.ts.map +1 -0
- package/dist/organizations.js +14 -0
- package/dist/organizations.js.map +1 -0
- package/dist/solveCache/interface.d.ts +86 -0
- package/dist/solveCache/interface.d.ts.map +1 -0
- package/dist/solveCache/interface.js +12 -0
- package/dist/solveCache/interface.js.map +1 -0
- package/dist/storage/assetClasses.d.ts +59 -0
- package/dist/storage/assetClasses.d.ts.map +1 -0
- package/dist/storage/assetClasses.js +86 -0
- package/dist/storage/assetClasses.js.map +1 -0
- package/dist/storage/cacheBust.d.ts +21 -0
- package/dist/storage/cacheBust.d.ts.map +1 -0
- package/dist/storage/cacheBust.js +29 -0
- package/dist/storage/cacheBust.js.map +1 -0
- package/dist/storage/image.d.ts +6 -1
- package/dist/storage/image.d.ts.map +1 -1
- package/dist/storage/image.js +8 -3
- package/dist/storage/image.js.map +1 -1
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/testing/adapterSuite.d.ts +33 -0
- package/dist/testing/adapterSuite.d.ts.map +1 -0
- package/dist/testing/adapterSuite.js +166 -0
- package/dist/testing/adapterSuite.js.map +1 -0
- package/dist/testing/suites/definitionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/definitionStoreSuite.js +87 -0
- package/dist/testing/suites/definitionStoreSuite.js.map +1 -1
- package/dist/testing/suites/eventSinkSuite.js +1 -0
- package/dist/testing/suites/eventSinkSuite.js.map +1 -1
- package/dist/testing/suites/orgStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/orgStoreSuite.js +12 -0
- package/dist/testing/suites/orgStoreSuite.js.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts +9 -0
- package/dist/testing/suites/platformPermissionStoreSuite.d.ts.map +1 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js +34 -1
- package/dist/testing/suites/platformPermissionStoreSuite.js.map +1 -1
- package/dist/testing/suites/runner.d.ts +21 -0
- package/dist/testing/suites/runner.d.ts.map +1 -0
- package/dist/testing/suites/runner.js +7 -0
- package/dist/testing/suites/runner.js.map +1 -0
- package/dist/testing/suites/storageProviderSuite.d.ts.map +1 -1
- package/dist/testing/suites/storageProviderSuite.js +18 -0
- package/dist/testing/suites/storageProviderSuite.js.map +1 -1
- package/package.json +9 -24
- package/src/access/index.ts +0 -23
- package/src/access/rules.ts +0 -276
- package/src/auth/index.ts +0 -8
- package/src/auth/interface.ts +0 -279
- package/src/auth/types.ts +0 -32
- package/src/bindings/index.ts +0 -2
- package/src/bindings/interface.ts +0 -61
- package/src/computeServer/index.ts +0 -17
- package/src/computeServer/interface.ts +0 -62
- package/src/computeServer/types.ts +0 -74
- package/src/computeServer/utils.ts +0 -112
- package/src/config.ts +0 -99
- package/src/context.ts +0 -66
- package/src/data/index.ts +0 -1
- package/src/data/interface.ts +0 -70
- package/src/definitions/index.ts +0 -26
- package/src/definitions/interface.ts +0 -78
- package/src/definitions/paths.ts +0 -31
- package/src/definitions/schemas.ts +0 -64
- package/src/definitions/types.ts +0 -111
- package/src/errors.ts +0 -16
- package/src/events/audit.ts +0 -67
- package/src/events/index.ts +0 -9
- package/src/events/interface.ts +0 -102
- package/src/index.ts +0 -219
- package/src/invites/index.ts +0 -2
- package/src/invites/interface.ts +0 -38
- package/src/invites/types.ts +0 -39
- package/src/metrics/interface.ts +0 -86
- package/src/organizations/index.ts +0 -14
- package/src/organizations/interface.ts +0 -68
- package/src/organizations/schemas.ts +0 -52
- package/src/organizations/types.ts +0 -31
- package/src/pagination.ts +0 -45
- package/src/permissions/index.ts +0 -3
- package/src/permissions/interface.ts +0 -60
- package/src/permissions/types.ts +0 -21
- package/src/platformProjects/index.ts +0 -3
- package/src/platformProjects/interface.ts +0 -27
- package/src/platformProjects/schemas.ts +0 -13
- package/src/platformProjects/types.ts +0 -22
- package/src/projects/index.ts +0 -4
- package/src/projects/interface.ts +0 -58
- package/src/projects/schemas.ts +0 -28
- package/src/projects/types.ts +0 -36
- package/src/shareLinks/index.ts +0 -5
- package/src/shareLinks/interface.ts +0 -35
- package/src/shareLinks/schemas.ts +0 -21
- package/src/shareLinks/types.ts +0 -34
- package/src/storage/image.ts +0 -77
- package/src/storage/index.ts +0 -9
- package/src/storage/interface.ts +0 -27
- package/src/testing/index.ts +0 -47
- package/src/testing/suites/authProviderSuite.ts +0 -207
- package/src/testing/suites/computeServerStoreSuite.ts +0 -195
- package/src/testing/suites/definitionStoreSuite.ts +0 -585
- package/src/testing/suites/emailLinkAuthSuite.ts +0 -83
- package/src/testing/suites/eventSinkSuite.ts +0 -379
- package/src/testing/suites/helpers.ts +0 -87
- package/src/testing/suites/inviteStoreSuite.ts +0 -143
- package/src/testing/suites/orgStoreSuite.ts +0 -412
- package/src/testing/suites/platformPermissionStoreSuite.ts +0 -196
- package/src/testing/suites/platformProjectGrantStoreSuite.ts +0 -166
- package/src/testing/suites/projectStoreSuite.ts +0 -381
- package/src/testing/suites/shareLinkStoreSuite.ts +0 -203
- package/src/testing/suites/solveMetricSinkSuite.ts +0 -135
- package/src/testing/suites/storageProviderSuite.ts +0 -175
- package/src/testing/suites/userProfileStoreSuite.ts +0 -134
- package/src/userProfile/index.ts +0 -3
- package/src/userProfile/interface.ts +0 -48
- package/src/userProfile/types.ts +0 -24
- package/src/utils/audit.ts +0 -46
- package/src/utils/index.ts +0 -2
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IShareLinkStore.
|
|
3
|
-
*
|
|
4
|
-
* Covers create + get + list, hash lookup, revocation semantics, and the
|
|
5
|
-
* atomic check-and-increment that enforces the per-link cap. Adapters that
|
|
6
|
-
* use real DB tables seed a parent definition through `createScope`; the
|
|
7
|
-
* local adapter can stub.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect } from 'vitest';
|
|
11
|
-
import type { IShareLinkStore } from '../../shareLinks/interface.js';
|
|
12
|
-
import type { ShareLink } from '../../shareLinks/types.js';
|
|
13
|
-
import { makeCtx, makeUuid } from './helpers.js';
|
|
14
|
-
import type { RequestContext } from '../../context.js';
|
|
15
|
-
|
|
16
|
-
// Suite-wide token map. `scopeFor` populates it; `ctx` auto-attaches the
|
|
17
|
-
// matching token so DB-side auth (Supabase RLS) sees the right user.
|
|
18
|
-
const tokens = new Map<string, string>();
|
|
19
|
-
|
|
20
|
-
function ctx(
|
|
21
|
-
userId: string,
|
|
22
|
-
opts: {
|
|
23
|
-
actingOrgId?: string;
|
|
24
|
-
platformPermissions?: RequestContext['platformPermissions'];
|
|
25
|
-
orgPermissions?: RequestContext['orgPermissions'];
|
|
26
|
-
} = {}
|
|
27
|
-
): RequestContext {
|
|
28
|
-
return makeCtx(userId, { ...opts, sessionToken: tokens.get(userId) });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ShareLinkTestScope {
|
|
32
|
-
/** Owner of the parent definition. Used as `createdBy`. */
|
|
33
|
-
ownerId: string;
|
|
34
|
-
/**
|
|
35
|
-
* Optional access token for `ownerId`. Required for adapters with DB-side
|
|
36
|
-
* auth (Supabase RLS); local providers ignore it.
|
|
37
|
-
*/
|
|
38
|
-
ownerSessionToken?: string;
|
|
39
|
-
/** Existing definition the conformance suite mints links against. */
|
|
40
|
-
definitionId: string;
|
|
41
|
-
/** A second definition id for cross-definition isolation tests. */
|
|
42
|
-
otherDefinitionId: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ShareLinkStoreConformanceOptions {
|
|
46
|
-
name: string;
|
|
47
|
-
createStore: () => Promise<IShareLinkStore> | IShareLinkStore;
|
|
48
|
-
/**
|
|
49
|
-
* Seed any parent rows the adapter requires (FK-enforced backends seed a
|
|
50
|
-
* `definitions` row + parent project). Local adapters can return stub ids.
|
|
51
|
-
*/
|
|
52
|
-
createScope?: () => Promise<ShareLinkTestScope> | ShareLinkTestScope;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const DEFAULT_SCOPE: ShareLinkTestScope = {
|
|
56
|
-
ownerId: 'user-1',
|
|
57
|
-
definitionId: 'def-1',
|
|
58
|
-
otherDefinitionId: 'def-2'
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
function link(scope: ShareLinkTestScope, overrides: Partial<ShareLink> = {}): ShareLink {
|
|
62
|
-
const now = new Date().toISOString();
|
|
63
|
-
return {
|
|
64
|
-
id: overrides.id ?? makeUuid(),
|
|
65
|
-
definitionId: overrides.definitionId ?? scope.definitionId,
|
|
66
|
-
channel: overrides.channel ?? 'live',
|
|
67
|
-
tokenHash: overrides.tokenHash ?? `hash-${makeUuid()}`,
|
|
68
|
-
name: overrides.name,
|
|
69
|
-
createdBy: overrides.createdBy ?? scope.ownerId,
|
|
70
|
-
createdAt: overrides.createdAt ?? now,
|
|
71
|
-
expiresAt: overrides.expiresAt ?? null,
|
|
72
|
-
revokedAt: overrides.revokedAt ?? null,
|
|
73
|
-
allowSolve: overrides.allowSolve ?? true,
|
|
74
|
-
maxSolves: overrides.maxSolves === undefined ? null : overrides.maxSolves,
|
|
75
|
-
solveCount: overrides.solveCount ?? 0
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function runShareLinkStoreConformance(opts: ShareLinkStoreConformanceOptions): void {
|
|
80
|
-
const { name, createStore, createScope } = opts;
|
|
81
|
-
const scopeFor = async (): Promise<ShareLinkTestScope> => {
|
|
82
|
-
const s = createScope ? await createScope() : DEFAULT_SCOPE;
|
|
83
|
-
if (s.ownerSessionToken) tokens.set(s.ownerId, s.ownerSessionToken);
|
|
84
|
-
return s;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
describe(`IShareLinkStore conformance: ${name}`, () => {
|
|
88
|
-
it('create + getById round-trips', async () => {
|
|
89
|
-
const store = await createStore();
|
|
90
|
-
const scope = await scopeFor();
|
|
91
|
-
const l = link(scope);
|
|
92
|
-
await store.create(ctx(scope.ownerId), l);
|
|
93
|
-
const got = await store.getById(ctx(scope.ownerId), l.id);
|
|
94
|
-
expect(got?.id).toBe(l.id);
|
|
95
|
-
expect(got?.tokenHash).toBe(l.tokenHash);
|
|
96
|
-
expect(got?.allowSolve).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('listByDefinition returns links for that definition only, newest first', async () => {
|
|
100
|
-
const store = await createStore();
|
|
101
|
-
const scope = await scopeFor();
|
|
102
|
-
const a = link(scope, { createdAt: '2026-04-01T00:00:00.000Z' });
|
|
103
|
-
const b = link(scope, { createdAt: '2026-04-02T00:00:00.000Z' });
|
|
104
|
-
const other = link(scope, { definitionId: scope.otherDefinitionId });
|
|
105
|
-
await store.create(ctx(scope.ownerId), a);
|
|
106
|
-
await store.create(ctx(scope.ownerId), b);
|
|
107
|
-
await store.create(ctx(scope.ownerId), other);
|
|
108
|
-
|
|
109
|
-
const page = await store.listByDefinition(ctx(scope.ownerId), scope.definitionId);
|
|
110
|
-
expect(page.items.map((l) => l.id)).toEqual([b.id, a.id]);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('getByTokenHash returns the matching link', async () => {
|
|
114
|
-
const store = await createStore();
|
|
115
|
-
const scope = await scopeFor();
|
|
116
|
-
const l = link(scope, { tokenHash: 'unique-hash' });
|
|
117
|
-
await store.create(ctx(scope.ownerId), l);
|
|
118
|
-
const got = await store.getByTokenHash(ctx(scope.ownerId), 'unique-hash');
|
|
119
|
-
expect(got?.id).toBe(l.id);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('getByTokenHash returns null for unknown hash', async () => {
|
|
123
|
-
const store = await createStore();
|
|
124
|
-
const scope = await scopeFor();
|
|
125
|
-
const got = await store.getByTokenHash(ctx(scope.ownerId), 'nonexistent');
|
|
126
|
-
expect(got).toBeNull();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('revoke sets revokedAt; subsequent listByDefinition excludes it', async () => {
|
|
130
|
-
const store = await createStore();
|
|
131
|
-
const scope = await scopeFor();
|
|
132
|
-
const l = link(scope);
|
|
133
|
-
await store.create(ctx(scope.ownerId), l);
|
|
134
|
-
await store.revoke(ctx(scope.ownerId), l.id);
|
|
135
|
-
|
|
136
|
-
const page = await store.listByDefinition(ctx(scope.ownerId), scope.definitionId);
|
|
137
|
-
expect(page.items.map((x) => x.id)).not.toContain(l.id);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('revoke makes getByTokenHash return null', async () => {
|
|
141
|
-
const store = await createStore();
|
|
142
|
-
const scope = await scopeFor();
|
|
143
|
-
const l = link(scope, { tokenHash: 'will-revoke' });
|
|
144
|
-
await store.create(ctx(scope.ownerId), l);
|
|
145
|
-
await store.revoke(ctx(scope.ownerId), l.id);
|
|
146
|
-
|
|
147
|
-
const got = await store.getByTokenHash(ctx(scope.ownerId), 'will-revoke');
|
|
148
|
-
expect(got).toBeNull();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('revoke is idempotent (no error on double-revoke)', async () => {
|
|
152
|
-
const store = await createStore();
|
|
153
|
-
const scope = await scopeFor();
|
|
154
|
-
const l = link(scope);
|
|
155
|
-
await store.create(ctx(scope.ownerId), l);
|
|
156
|
-
await store.revoke(ctx(scope.ownerId), l.id);
|
|
157
|
-
await store.revoke(ctx(scope.ownerId), l.id); // should not throw
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('tryIncrementSolveCount with null cap always increments', async () => {
|
|
161
|
-
const store = await createStore();
|
|
162
|
-
const scope = await scopeFor();
|
|
163
|
-
const l = link(scope, { maxSolves: null });
|
|
164
|
-
await store.create(ctx(scope.ownerId), l);
|
|
165
|
-
|
|
166
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), l.id)).toBe(1);
|
|
167
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), l.id)).toBe(2);
|
|
168
|
-
|
|
169
|
-
const after = await store.getById(ctx(scope.ownerId), l.id);
|
|
170
|
-
expect(after?.solveCount).toBe(2);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('tryIncrementSolveCount enforces a cap', async () => {
|
|
174
|
-
const store = await createStore();
|
|
175
|
-
const scope = await scopeFor();
|
|
176
|
-
const l = link(scope, { maxSolves: 2 });
|
|
177
|
-
await store.create(ctx(scope.ownerId), l);
|
|
178
|
-
|
|
179
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), l.id)).toBe(1);
|
|
180
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), l.id)).toBe(2);
|
|
181
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), l.id)).toBeNull();
|
|
182
|
-
|
|
183
|
-
const after = await store.getById(ctx(scope.ownerId), l.id);
|
|
184
|
-
expect(after?.solveCount).toBe(2);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('tryIncrementSolveCount returns null for revoked links', async () => {
|
|
188
|
-
const store = await createStore();
|
|
189
|
-
const scope = await scopeFor();
|
|
190
|
-
const l = link(scope, { maxSolves: null });
|
|
191
|
-
await store.create(ctx(scope.ownerId), l);
|
|
192
|
-
await store.revoke(ctx(scope.ownerId), l.id);
|
|
193
|
-
|
|
194
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), l.id)).toBeNull();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('tryIncrementSolveCount returns null for unknown id', async () => {
|
|
198
|
-
const store = await createStore();
|
|
199
|
-
const scope = await scopeFor();
|
|
200
|
-
expect(await store.tryIncrementSolveCount(ctx(scope.ownerId), makeUuid())).toBeNull();
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for the solve-metric-sink contract.
|
|
3
|
-
*
|
|
4
|
-
* Unlike `IEventSink`, an `ISolveMetricSink` is not driven by store mutations —
|
|
5
|
-
* the compute route calls `record()` directly, once per solve attempt. So this
|
|
6
|
-
* suite is a round-trip: hand a `SolveMetric` to the sink, then read the row
|
|
7
|
-
* back through an adapter-supplied `readRows` and assert the persisted shape.
|
|
8
|
-
*
|
|
9
|
-
* It also pins the contract's hard guarantee: `record` MUST NOT throw, even on
|
|
10
|
-
* a backend failure — the compute route depends on that to keep serving the
|
|
11
|
-
* response after a solve.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
-
import type { ISolveMetricSink, SolveMetric, RequestContext } from '../../index.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Adapter's view of a persisted metric, normalized back to the `SolveMetric`
|
|
19
|
-
* shape plus the recorded `actorId`. Implementations map their column names
|
|
20
|
-
* onto these fields so the suite stays storage-agnostic.
|
|
21
|
-
*/
|
|
22
|
-
export interface RecordedSolveMetric extends SolveMetric {
|
|
23
|
-
/** The actor the sink derived from the `RequestContext` it was handed. */
|
|
24
|
-
actorId: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SolveMetricSinkConformanceOptions {
|
|
28
|
-
name: string;
|
|
29
|
-
/** Build a fresh sink against the adapter's backend. */
|
|
30
|
-
createSink: () => Promise<ISolveMetricSink> | ISolveMetricSink;
|
|
31
|
-
/** Read every persisted metric back, normalized to `RecordedSolveMetric`. */
|
|
32
|
-
readRows: () => Promise<RecordedSolveMetric[]>;
|
|
33
|
-
/**
|
|
34
|
-
* Build a sink whose backing store is broken so writes fail. Used to prove
|
|
35
|
-
* `record` swallows errors. Omit if the adapter can't simulate a failure;
|
|
36
|
-
* the throw-safety test is skipped when absent.
|
|
37
|
-
*/
|
|
38
|
-
createFailingSink?: () => Promise<ISolveMetricSink> | ISolveMetricSink;
|
|
39
|
-
/** Optional per-test cleanup (truncate the metrics table, etc.). */
|
|
40
|
-
cleanup?: () => Promise<void> | void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function sysCtx(userId: string): RequestContext {
|
|
44
|
-
return { userId, platformPermissions: [], orgPermissions: [], system: true };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function baseMetric(overrides: Partial<SolveMetric> = {}): SolveMetric {
|
|
48
|
-
return {
|
|
49
|
-
definitionUrl: 'local:00000000-0000-0000-0000-0000000000aa',
|
|
50
|
-
definitionId: '00000000-0000-0000-0000-0000000000aa',
|
|
51
|
-
versionId: '00000000-0000-0000-0000-0000000000bb',
|
|
52
|
-
channel: 'live',
|
|
53
|
-
orgId: '00000000-0000-0000-0000-0000000000cc',
|
|
54
|
-
durationMs: 123.5,
|
|
55
|
-
ok: true,
|
|
56
|
-
failureKind: 'ok',
|
|
57
|
-
errorCount: 0,
|
|
58
|
-
warningCount: 0,
|
|
59
|
-
...overrides
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function runSolveMetricSinkConformance(opts: SolveMetricSinkConformanceOptions): void {
|
|
64
|
-
const { name, createSink, readRows, createFailingSink, cleanup } = opts;
|
|
65
|
-
|
|
66
|
-
describe(`ISolveMetricSink conformance: ${name}`, () => {
|
|
67
|
-
let sink: ISolveMetricSink;
|
|
68
|
-
|
|
69
|
-
beforeEach(async () => {
|
|
70
|
-
sink = await createSink();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
afterEach(async () => {
|
|
74
|
-
if (cleanup) await cleanup();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('persists a successful solve with all fields intact', async () => {
|
|
78
|
-
const metric = baseMetric();
|
|
79
|
-
await sink.record(sysCtx('user-1'), metric);
|
|
80
|
-
|
|
81
|
-
const rows = await readRows();
|
|
82
|
-
expect(rows).toHaveLength(1);
|
|
83
|
-
expect(rows[0]).toMatchObject({ ...metric, actorId: 'user-1' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('records the actor from the RequestContext', async () => {
|
|
87
|
-
await sink.record(sysCtx('user-xyz'), baseMetric());
|
|
88
|
-
const rows = await readRows();
|
|
89
|
-
expect(rows[0].actorId).toBe('user-xyz');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('falls back to "system" when the context has no user id', async () => {
|
|
93
|
-
await sink.record(sysCtx(''), baseMetric());
|
|
94
|
-
const rows = await readRows();
|
|
95
|
-
expect(rows[0].actorId).toBe('system');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('persists a failed solve with its failureKind and zero duration', async () => {
|
|
99
|
-
await sink.record(
|
|
100
|
-
sysCtx('user-1'),
|
|
101
|
-
baseMetric({ ok: false, failureKind: 'timeout', durationMs: 0 })
|
|
102
|
-
);
|
|
103
|
-
const rows = await readRows();
|
|
104
|
-
expect(rows[0]).toMatchObject({ ok: false, failureKind: 'timeout', durationMs: 0 });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('persists error/warning counts on an otherwise-ok solve', async () => {
|
|
108
|
-
await sink.record(sysCtx('user-1'), baseMetric({ errorCount: 2, warningCount: 5 }));
|
|
109
|
-
const rows = await readRows();
|
|
110
|
-
expect(rows[0]).toMatchObject({ ok: true, errorCount: 2, warningCount: 5 });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('persists null definition/version/org for remote-URL solves', async () => {
|
|
114
|
-
await sink.record(
|
|
115
|
-
sysCtx('user-1'),
|
|
116
|
-
baseMetric({
|
|
117
|
-
definitionUrl: 'https://example.com/def.gh',
|
|
118
|
-
definitionId: null,
|
|
119
|
-
versionId: null,
|
|
120
|
-
orgId: null
|
|
121
|
-
})
|
|
122
|
-
);
|
|
123
|
-
const rows = await readRows();
|
|
124
|
-
expect(rows[0]).toMatchObject({ definitionId: null, versionId: null, orgId: null });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (createFailingSink) {
|
|
128
|
-
it('does not throw when the backing write fails', async () => {
|
|
129
|
-
const failing = await createFailingSink();
|
|
130
|
-
// The contract: record swallows the failure and resolves.
|
|
131
|
-
await expect(failing.record(sysCtx('user-1'), baseMetric())).resolves.toBeUndefined();
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IStorageProvider.
|
|
3
|
-
*
|
|
4
|
-
* Tests blob storage operations (get, put, delete, deletePrefix, getPublicUrl)
|
|
5
|
-
* to ensure all adapters behave identically.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from 'vitest';
|
|
9
|
-
import type { IStorageProvider } from '../../storage/interface.js';
|
|
10
|
-
|
|
11
|
-
export interface StorageProviderConformanceOptions {
|
|
12
|
-
/** Name to show in test output (e.g. "local-filesystem"). */
|
|
13
|
-
name: string;
|
|
14
|
-
/** Factory that returns a fresh, empty storage per test. */
|
|
15
|
-
createStorage: () => Promise<IStorageProvider> | IStorageProvider;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function bytes(text: string): Uint8Array {
|
|
19
|
-
return new TextEncoder().encode(text);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function runStorageProviderConformance(opts: StorageProviderConformanceOptions): void {
|
|
23
|
-
const { name, createStorage } = opts;
|
|
24
|
-
|
|
25
|
-
describe(`IStorageProvider conformance: ${name}`, () => {
|
|
26
|
-
it('put + get returns the stored data', async () => {
|
|
27
|
-
const storage = await createStorage();
|
|
28
|
-
const data = bytes('Hello, World!');
|
|
29
|
-
await storage.put('test/file.txt', data);
|
|
30
|
-
const got = await storage.get('test/file.txt');
|
|
31
|
-
expect(got).toEqual(data);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('get returns null for missing path', async () => {
|
|
35
|
-
const storage = await createStorage();
|
|
36
|
-
const got = await storage.get('nonexistent/path.txt');
|
|
37
|
-
expect(got).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('put overwrites existing file', async () => {
|
|
41
|
-
const storage = await createStorage();
|
|
42
|
-
const data1 = bytes('First');
|
|
43
|
-
const data2 = bytes('Second');
|
|
44
|
-
await storage.put('test/file.txt', data1);
|
|
45
|
-
await storage.put('test/file.txt', data2);
|
|
46
|
-
const got = await storage.get('test/file.txt');
|
|
47
|
-
expect(got).toEqual(data2);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('put creates nested directories', async () => {
|
|
51
|
-
const storage = await createStorage();
|
|
52
|
-
const data = bytes('nested');
|
|
53
|
-
await storage.put('a/b/c/d/file.txt', data);
|
|
54
|
-
const got = await storage.get('a/b/c/d/file.txt');
|
|
55
|
-
expect(got).toEqual(data);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('delete removes a file', async () => {
|
|
59
|
-
const storage = await createStorage();
|
|
60
|
-
await storage.put('test/file.txt', bytes('data'));
|
|
61
|
-
await storage.delete('test/file.txt');
|
|
62
|
-
const got = await storage.get('test/file.txt');
|
|
63
|
-
expect(got).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('delete is a no-op for missing file', async () => {
|
|
67
|
-
const storage = await createStorage();
|
|
68
|
-
await storage.delete('nonexistent/file.txt');
|
|
69
|
-
// Should not throw
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('deletePrefix removes all files matching prefix', async () => {
|
|
73
|
-
const storage = await createStorage();
|
|
74
|
-
await storage.put('definitions/guid-1/file.gh', bytes('def1'));
|
|
75
|
-
await storage.put('definitions/guid-1/meta.json', bytes('{}'));
|
|
76
|
-
await storage.put('definitions/guid-2/file.gh', bytes('def2'));
|
|
77
|
-
|
|
78
|
-
await storage.deletePrefix('definitions/guid-1/');
|
|
79
|
-
|
|
80
|
-
const file1 = await storage.get('definitions/guid-1/file.gh');
|
|
81
|
-
const meta1 = await storage.get('definitions/guid-1/meta.json');
|
|
82
|
-
const file2 = await storage.get('definitions/guid-2/file.gh');
|
|
83
|
-
|
|
84
|
-
expect(file1).toBeNull();
|
|
85
|
-
expect(meta1).toBeNull();
|
|
86
|
-
expect(file2).toBeTruthy();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('deletePrefix with no matches is a no-op', async () => {
|
|
90
|
-
const storage = await createStorage();
|
|
91
|
-
await storage.put('other/file.txt', bytes('data'));
|
|
92
|
-
await storage.deletePrefix('nonexistent/');
|
|
93
|
-
// Should not throw
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('getPublicUrl returns a non-empty string', async () => {
|
|
97
|
-
const storage = await createStorage();
|
|
98
|
-
const url = storage.getPublicUrl('test/file.txt');
|
|
99
|
-
expect(url).toBeTruthy();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('getPublicUrl is consistent for same path', async () => {
|
|
103
|
-
const storage = await createStorage();
|
|
104
|
-
const url1 = storage.getPublicUrl('test/file.txt');
|
|
105
|
-
const url2 = storage.getPublicUrl('test/file.txt');
|
|
106
|
-
expect(url1).toBe(url2);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('getPublicUrl differs for different paths', async () => {
|
|
110
|
-
const storage = await createStorage();
|
|
111
|
-
const url1 = storage.getPublicUrl('test/file1.txt');
|
|
112
|
-
const url2 = storage.getPublicUrl('test/file2.txt');
|
|
113
|
-
expect(url1).not.toBe(url2);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('handles binary data correctly', async () => {
|
|
117
|
-
const storage = await createStorage();
|
|
118
|
-
const binary = new Uint8Array([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]);
|
|
119
|
-
await storage.put('binary/data.bin', binary);
|
|
120
|
-
const got = await storage.get('binary/data.bin');
|
|
121
|
-
expect(got).toEqual(binary);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('handles large data', async () => {
|
|
125
|
-
const storage = await createStorage();
|
|
126
|
-
const large = new Uint8Array(1024 * 1024); // 1 MB
|
|
127
|
-
for (let i = 0; i < large.length; i++) {
|
|
128
|
-
large[i] = i % 256;
|
|
129
|
-
}
|
|
130
|
-
await storage.put('large/file.bin', large);
|
|
131
|
-
const got = await storage.get('large/file.bin');
|
|
132
|
-
expect(got).toEqual(large);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('put with contentType is stored', async () => {
|
|
136
|
-
const storage = await createStorage();
|
|
137
|
-
const data = bytes('{"type":"octet"}');
|
|
138
|
-
await storage.put('files/test.bin', data, 'application/octet-stream');
|
|
139
|
-
const got = await storage.get('files/test.bin');
|
|
140
|
-
expect(got).toEqual(data);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ============================================================================
|
|
144
|
-
// Image transcoding
|
|
145
|
-
// ============================================================================
|
|
146
|
-
//
|
|
147
|
-
// Every adapter MUST run uploads through the shared `transcodeImageIfNeeded`
|
|
148
|
-
// helper so cover images, archive thumbnails, etc. land as WebP regardless
|
|
149
|
-
// of what the user uploaded. Otherwise a provider that skipped this would
|
|
150
|
-
// produce a pipeline that looks correct until someone uploads a PNG.
|
|
151
|
-
|
|
152
|
-
it('transcodes png input to webp (path rewritten, magic bytes flipped)', async () => {
|
|
153
|
-
const storage = await createStorage();
|
|
154
|
-
// Minimal valid 1×1 transparent PNG — the canonical 67-byte fixture.
|
|
155
|
-
const pngBytes = Uint8Array.from([
|
|
156
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
|
|
157
|
-
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
|
|
158
|
-
0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00,
|
|
159
|
-
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
|
160
|
-
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
|
|
161
|
-
]);
|
|
162
|
-
await storage.put('covers/tiny.png', pngBytes, 'image/png');
|
|
163
|
-
|
|
164
|
-
// Image transcoding rewrites the path to .webp — the original path is empty.
|
|
165
|
-
const atPng = await storage.get('covers/tiny.png');
|
|
166
|
-
expect(atPng).toBeNull();
|
|
167
|
-
|
|
168
|
-
// At the webp path we get a valid RIFF/WEBP container.
|
|
169
|
-
const atWebp = await storage.get('covers/tiny.webp');
|
|
170
|
-
expect(atWebp).toBeTruthy();
|
|
171
|
-
expect(atWebp?.slice(0, 4)).toEqual(bytes('RIFF'));
|
|
172
|
-
expect(atWebp?.slice(8, 12)).toEqual(bytes('WEBP'));
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapter conformance suite for IUserProfileStore.
|
|
3
|
-
*
|
|
4
|
-
* Tests the user-profile mutations (display name, starred definitions,
|
|
5
|
-
* recent runs) that used to live on IAuthProvider. Adapters must seed
|
|
6
|
-
* a real user before each test since profile mutations target a userId.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import type { IUserProfileStore } from '../../userProfile/interface.js';
|
|
11
|
-
import type { AuthUser } from '../../auth/index.js';
|
|
12
|
-
import type { RecentRun } from '../../userProfile/index.js';
|
|
13
|
-
import { SYSTEM_CONTEXT } from '../../context.js';
|
|
14
|
-
import { makeUuid } from './helpers.js';
|
|
15
|
-
|
|
16
|
-
// Profile mutations need a ctx scoped to the target user (Permissions.md §9 +
|
|
17
|
-
// the `IUserProfileStore` JSDoc auth-boundary note). The conformance suite
|
|
18
|
-
// uses SYSTEM_CONTEXT throughout so it doesn't need to mint per-user ctxs;
|
|
19
|
-
// the per-user authorization is exercised in adapter-specific tests.
|
|
20
|
-
const ctx = SYSTEM_CONTEXT;
|
|
21
|
-
|
|
22
|
-
export interface UserProfileStoreConformanceOptions {
|
|
23
|
-
/** Name to show in test output. */
|
|
24
|
-
name: string;
|
|
25
|
-
/**
|
|
26
|
-
* Factory that returns a fresh, empty store and a `seedUser(email)` helper
|
|
27
|
-
* that creates a real user the test can mutate. The helper is provider-
|
|
28
|
-
* specific because IUserProfileStore itself doesn't create users.
|
|
29
|
-
*/
|
|
30
|
-
createStore: () => Promise<{
|
|
31
|
-
store: IUserProfileStore;
|
|
32
|
-
seedUser: (email: string) => Promise<AuthUser>;
|
|
33
|
-
}>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function runUserProfileStoreConformance(opts: UserProfileStoreConformanceOptions): void {
|
|
37
|
-
const { name, createStore } = opts;
|
|
38
|
-
|
|
39
|
-
describe(`IUserProfileStore conformance: ${name}`, () => {
|
|
40
|
-
it('getProfile returns null for unknown user', async () => {
|
|
41
|
-
const { store } = await createStore();
|
|
42
|
-
const profile = await store.getProfile(ctx, makeUuid());
|
|
43
|
-
expect(profile).toBeNull();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('getProfile returns a profile for a seeded user', async () => {
|
|
47
|
-
const { store, seedUser } = await createStore();
|
|
48
|
-
const user = await seedUser('get@example.com');
|
|
49
|
-
const profile = await store.getProfile(ctx, user.id);
|
|
50
|
-
expect(profile).toBeTruthy();
|
|
51
|
-
expect(profile?.userId).toBe(user.id);
|
|
52
|
-
expect(profile?.starredDefinitions).toEqual([]);
|
|
53
|
-
expect(profile?.recentRuns).toEqual([]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('getProfiles batch-loads seeded users and skips unknowns', async () => {
|
|
57
|
-
const { store, seedUser } = await createStore();
|
|
58
|
-
const a = await seedUser('a-batch@example.com');
|
|
59
|
-
const b = await seedUser('b-batch@example.com');
|
|
60
|
-
const ghost = makeUuid();
|
|
61
|
-
const profiles = await store.getProfiles(ctx, [a.id, b.id, ghost]);
|
|
62
|
-
const ids = new Set(profiles.map((p) => p.userId));
|
|
63
|
-
expect(ids.has(a.id)).toBe(true);
|
|
64
|
-
expect(ids.has(b.id)).toBe(true);
|
|
65
|
-
expect(ids.has(ghost)).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('updateProfile changes displayName and getProfile returns it', async () => {
|
|
69
|
-
const { store, seedUser } = await createStore();
|
|
70
|
-
const user = await seedUser('p@example.com');
|
|
71
|
-
const result = await store.updateProfile(ctx, user.id, { displayName: 'Felix' });
|
|
72
|
-
expect(result).toBe('ok');
|
|
73
|
-
const profile = await store.getProfile(ctx, user.id);
|
|
74
|
-
expect(profile?.displayName).toBe('Felix');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('updateProfile returns not_found for unknown user', async () => {
|
|
78
|
-
const { store } = await createStore();
|
|
79
|
-
const result = await store.updateProfile(ctx, makeUuid(), { displayName: 'X' });
|
|
80
|
-
expect(result).toBe('not_found');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('starDefinition + unstarDefinition round-trip reflects in profile', async () => {
|
|
84
|
-
const { store, seedUser } = await createStore();
|
|
85
|
-
const user = await seedUser('star@example.com');
|
|
86
|
-
const defId = makeUuid();
|
|
87
|
-
expect(await store.starDefinition(ctx, user.id, defId)).toBe('ok');
|
|
88
|
-
expect((await store.getProfile(ctx, user.id))?.starredDefinitions).toContain(defId);
|
|
89
|
-
expect(await store.unstarDefinition(ctx, user.id, defId)).toBe('ok');
|
|
90
|
-
expect((await store.getProfile(ctx, user.id))?.starredDefinitions).not.toContain(defId);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('starDefinition is idempotent (no duplicates)', async () => {
|
|
94
|
-
const { store, seedUser } = await createStore();
|
|
95
|
-
const user = await seedUser('idem@example.com');
|
|
96
|
-
const defId = makeUuid();
|
|
97
|
-
expect(await store.starDefinition(ctx, user.id, defId)).toBe('ok');
|
|
98
|
-
expect(await store.starDefinition(ctx, user.id, defId)).toBe('ok');
|
|
99
|
-
const profile = await store.getProfile(ctx, user.id);
|
|
100
|
-
const occurrences = profile?.starredDefinitions.filter((d) => d === defId).length;
|
|
101
|
-
expect(occurrences).toBe(1);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('recordRun returns ok and profile surfaces the entry', async () => {
|
|
105
|
-
const { store, seedUser } = await createStore();
|
|
106
|
-
const user = await seedUser('runs@example.com');
|
|
107
|
-
const run: RecentRun = {
|
|
108
|
-
definitionId: makeUuid(),
|
|
109
|
-
runId: makeUuid(),
|
|
110
|
-
definitionName: 'D',
|
|
111
|
-
timestamp: new Date().toISOString()
|
|
112
|
-
};
|
|
113
|
-
expect(await store.recordRun(ctx, user.id, run)).toBe('ok');
|
|
114
|
-
const profile = await store.getProfile(ctx, user.id);
|
|
115
|
-
expect(profile?.recentRuns.some((r) => r.runId === run.runId)).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('recordRun returns not_found for unknown user', async () => {
|
|
119
|
-
const { store } = await createStore();
|
|
120
|
-
const run: RecentRun = {
|
|
121
|
-
definitionId: makeUuid(),
|
|
122
|
-
runId: makeUuid(),
|
|
123
|
-
definitionName: 'D',
|
|
124
|
-
timestamp: new Date().toISOString()
|
|
125
|
-
};
|
|
126
|
-
expect(await store.recordRun(ctx, makeUuid(), run)).toBe('not_found');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('starDefinition returns not_found for unknown user', async () => {
|
|
130
|
-
const { store } = await createStore();
|
|
131
|
-
expect(await store.starDefinition(ctx, makeUuid(), makeUuid())).toBe('not_found');
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
package/src/userProfile/index.ts
DELETED