@selvajs/server 0.2.0-beta.3 → 0.2.0-beta.4
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/README.md +32 -0
- package/dist/compute/client-cache.d.ts.map +1 -1
- package/dist/compute/client-cache.js +6 -0
- package/dist/compute/client-cache.js.map +1 -1
- package/dist/compute/safe-url.d.ts.map +1 -1
- package/dist/compute/safe-url.js +16 -0
- package/dist/compute/safe-url.js.map +1 -1
- package/dist/compute/solve-cache-single-flight.d.ts +8 -1
- package/dist/compute/solve-cache-single-flight.d.ts.map +1 -1
- package/dist/compute/solve-cache-single-flight.js +4 -2
- package/dist/compute/solve-cache-single-flight.js.map +1 -1
- package/dist/compute/solve-pipeline.d.ts.map +1 -1
- package/dist/compute/solve-pipeline.js +3 -1
- package/dist/compute/solve-pipeline.js.map +1 -1
- package/package.json +14 -18
- package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
- package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
- package/dist/access/__tests__/project-access-input.test.js +0 -146
- package/dist/access/__tests__/project-access-input.test.js.map +0 -1
- package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
- package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/client-cache.test.js +0 -218
- package/dist/compute/__tests__/client-cache.test.js.map +0 -1
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts +0 -2
- package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/definition-byte-cache.test.js +0 -107
- package/dist/compute/__tests__/definition-byte-cache.test.js.map +0 -1
- package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
- package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
- package/dist/compute/__tests__/rate-limit.test.js +0 -98
- package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
- package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
- package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
- package/dist/compute/__tests__/remote-definition.test.js +0 -114
- package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
- package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
- package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
- package/dist/compute/__tests__/safe-url.test.js +0 -71
- package/dist/compute/__tests__/safe-url.test.js.map +0 -1
- package/dist/compute/__tests__/solve-cache.test.d.ts +0 -13
- package/dist/compute/__tests__/solve-cache.test.d.ts.map +0 -1
- package/dist/compute/__tests__/solve-cache.test.js +0 -234
- package/dist/compute/__tests__/solve-cache.test.js.map +0 -1
- package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
- package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
- package/dist/compute/__tests__/solve-pipeline.test.js +0 -557
- package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
- package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
- package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
- package/dist/compute/__tests__/transform-input.test.js +0 -120
- package/dist/compute/__tests__/transform-input.test.js.map +0 -1
- package/dist/definitions/__tests__/definition-service.test.d.ts +0 -14
- package/dist/definitions/__tests__/definition-service.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/definition-service.test.js +0 -95
- package/dist/definitions/__tests__/definition-service.test.js.map +0 -1
- package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
- package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/load-for-render.test.js +0 -196
- package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
- package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
- package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
- package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
- package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
- package/dist/http/__tests__/body-size.test.d.ts +0 -2
- package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
- package/dist/http/__tests__/body-size.test.js +0 -24
- package/dist/http/__tests__/body-size.test.js.map +0 -1
- package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
- package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
- package/dist/http/__tests__/route-classifier.test.js +0 -54
- package/dist/http/__tests__/route-classifier.test.js.map +0 -1
- package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
- package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
- package/dist/http/__tests__/safe-redirect.test.js +0 -44
- package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
- package/dist/http/__tests__/security-headers.test.d.ts +0 -2
- package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
- package/dist/http/__tests__/security-headers.test.js +0 -24
- package/dist/http/__tests__/security-headers.test.js.map +0 -1
- package/dist/ops/__tests__/semver.test.d.ts +0 -2
- package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
- package/dist/ops/__tests__/semver.test.js +0 -38
- package/dist/ops/__tests__/semver.test.js.map +0 -1
- package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
- package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
- package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
- package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
- package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
- package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
- package/dist/tokens/__tests__/token-codec.test.js +0 -50
- package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
- package/src/access/__tests__/project-access-input.test.ts +0 -175
- package/src/access/index.ts +0 -9
- package/src/access/project-access-input.ts +0 -145
- package/src/compute/__tests__/client-cache.test.ts +0 -244
- package/src/compute/__tests__/definition-byte-cache.test.ts +0 -129
- package/src/compute/__tests__/rate-limit.test.ts +0 -106
- package/src/compute/__tests__/remote-definition.test.ts +0 -140
- package/src/compute/__tests__/safe-url.test.ts +0 -72
- package/src/compute/__tests__/solve-cache.test.ts +0 -268
- package/src/compute/__tests__/solve-pipeline.test.ts +0 -660
- package/src/compute/__tests__/transform-input.test.ts +0 -162
- package/src/compute/client-cache.ts +0 -321
- package/src/compute/definition-byte-cache.ts +0 -168
- package/src/compute/index.ts +0 -82
- package/src/compute/limits.ts +0 -237
- package/src/compute/memory-solve-cache.ts +0 -0
- package/src/compute/rate-limit.ts +0 -110
- package/src/compute/remote-definition.ts +0 -149
- package/src/compute/safe-url.ts +0 -186
- package/src/compute/solve-cache-envelope.ts +0 -98
- package/src/compute/solve-cache-key.ts +0 -85
- package/src/compute/solve-cache-single-flight.ts +0 -59
- package/src/compute/solve-pipeline.ts +0 -603
- package/src/compute/transform-input.ts +0 -66
- package/src/definitions/__tests__/definition-service.test.ts +0 -131
- package/src/definitions/__tests__/load-for-render.test.ts +0 -244
- package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
- package/src/definitions/definition-service.ts +0 -247
- package/src/definitions/index.ts +0 -22
- package/src/definitions/load-for-render.ts +0 -272
- package/src/definitions/schema-extraction.ts +0 -117
- package/src/errors/SentryErrorReporter.ts +0 -88
- package/src/errors/index.ts +0 -3
- package/src/http/__tests__/body-size.test.ts +0 -27
- package/src/http/__tests__/route-classifier.test.ts +0 -60
- package/src/http/__tests__/safe-redirect.test.ts +0 -47
- package/src/http/__tests__/security-headers.test.ts +0 -29
- package/src/http/body-size.ts +0 -24
- package/src/http/index.ts +0 -10
- package/src/http/redirect.ts +0 -16
- package/src/http/route-classifier.ts +0 -74
- package/src/http/security-headers.ts +0 -42
- package/src/index.ts +0 -27
- package/src/ops/__tests__/semver.test.ts +0 -42
- package/src/ops/index.ts +0 -3
- package/src/ops/semver.ts +0 -60
- package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
- package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
- package/src/providers/create-selva-providers.ts +0 -241
- package/src/providers/index.ts +0 -12
- package/src/tokens/__tests__/token-codec.test.ts +0 -63
- package/src/tokens/index.ts +0 -8
- package/src/tokens/token-codec.ts +0 -86
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `DefinitionService.uploadVersion` version-number allocation (caching
|
|
3
|
-
* Phase 1.5). The root-cause fix: the next version number comes from the store's
|
|
4
|
-
* monotonic `reserveNextVersionNumber` counter — NOT max(existing)+1, which
|
|
5
|
-
* reused a number (and its `fileKey`) after delete-latest and let a stale storage
|
|
6
|
-
* blob serve the old version's bytes for new content.
|
|
7
|
-
*
|
|
8
|
-
* These run against fakes that record the store/storage calls, so they pin the
|
|
9
|
-
* SERVICE's contract (reserve → derive fileKey from the reserved number). That a
|
|
10
|
-
* real provider's counter is truly monotonic across delete-then-reupload is
|
|
11
|
-
* covered by the definition-store conformance suite (both providers).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect } from 'vitest';
|
|
15
|
-
import type {
|
|
16
|
-
IDataProvider,
|
|
17
|
-
IStorageProvider,
|
|
18
|
-
RequestContext,
|
|
19
|
-
DefinitionRecord,
|
|
20
|
-
DefinitionVersion,
|
|
21
|
-
UISchema
|
|
22
|
-
} from '@selvajs/platform';
|
|
23
|
-
import { DefinitionService } from '../definition-service.js';
|
|
24
|
-
|
|
25
|
-
const CTX = { userId: 'u-1' } as RequestContext;
|
|
26
|
-
const SCHEMA = {} as UISchema;
|
|
27
|
-
|
|
28
|
-
function fakeRecord(guid: string, nextVersionNumber: number): DefinitionRecord {
|
|
29
|
-
const now = new Date().toISOString();
|
|
30
|
-
return {
|
|
31
|
-
guid,
|
|
32
|
-
projectId: 'p-1',
|
|
33
|
-
ownerId: 'u-1',
|
|
34
|
-
createdBy: 'u-1',
|
|
35
|
-
updatedBy: 'u-1',
|
|
36
|
-
displayName: 'Def',
|
|
37
|
-
status: 'draft',
|
|
38
|
-
solveCount: 0,
|
|
39
|
-
nextVersionNumber,
|
|
40
|
-
liveVersionId: 'v-existing',
|
|
41
|
-
draftVersionId: 'v-existing',
|
|
42
|
-
createdAt: now,
|
|
43
|
-
updatedAt: now,
|
|
44
|
-
deletedAt: null
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A minimal data provider whose `definitions` store hands out the reserved
|
|
50
|
-
* number and records the version row it was told to create. Only the methods
|
|
51
|
-
* `uploadVersion` touches are implemented.
|
|
52
|
-
*/
|
|
53
|
-
function fakeDeps(record: DefinitionRecord) {
|
|
54
|
-
let counter = record.nextVersionNumber;
|
|
55
|
-
const created: DefinitionVersion[] = [];
|
|
56
|
-
const puts: string[] = [];
|
|
57
|
-
|
|
58
|
-
const data = {
|
|
59
|
-
definitions: {
|
|
60
|
-
get: async () => record,
|
|
61
|
-
reserveNextVersionNumber: async () => counter++, // monotonic, never reused
|
|
62
|
-
createVersion: async (_ctx: RequestContext, v: DefinitionVersion) => {
|
|
63
|
-
created.push(v);
|
|
64
|
-
},
|
|
65
|
-
setDraftVersion: async () => {}
|
|
66
|
-
}
|
|
67
|
-
} as unknown as IDataProvider;
|
|
68
|
-
|
|
69
|
-
const storage = {
|
|
70
|
-
put: async (key: string) => {
|
|
71
|
-
puts.push(key);
|
|
72
|
-
}
|
|
73
|
-
} as unknown as IStorageProvider;
|
|
74
|
-
|
|
75
|
-
return { data, storage, created, puts };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
describe('DefinitionService.uploadVersion — version-number allocation', () => {
|
|
79
|
-
it('reserves the next number from the record counter and derives the fileKey from it', async () => {
|
|
80
|
-
const record = fakeRecord('11111111-1111-1111-1111-111111111111', 3);
|
|
81
|
-
const { data, storage, created, puts } = fakeDeps(record);
|
|
82
|
-
const svc = new DefinitionService(data, storage);
|
|
83
|
-
|
|
84
|
-
const version = await svc.uploadVersion(
|
|
85
|
-
CTX,
|
|
86
|
-
record.guid,
|
|
87
|
-
new Uint8Array([1, 2, 3]),
|
|
88
|
-
'gh',
|
|
89
|
-
'model.gh',
|
|
90
|
-
SCHEMA
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(version.versionNumber).toBe(3);
|
|
94
|
-
// fileKey encodes the reserved number — so a fresh number ⇒ fresh key.
|
|
95
|
-
expect(version.fileKey).toContain('v3');
|
|
96
|
-
expect(puts).toEqual([version.fileKey]);
|
|
97
|
-
expect(created).toHaveLength(1);
|
|
98
|
-
expect(created[0].versionNumber).toBe(3);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('two consecutive uploads mint distinct numbers and distinct fileKeys', async () => {
|
|
102
|
-
const record = fakeRecord('22222222-2222-2222-2222-222222222222', 5);
|
|
103
|
-
const { data, storage } = fakeDeps(record);
|
|
104
|
-
const svc = new DefinitionService(data, storage);
|
|
105
|
-
|
|
106
|
-
const a = await svc.uploadVersion(CTX, record.guid, new Uint8Array([1]), 'gh', 'a.gh', SCHEMA);
|
|
107
|
-
const b = await svc.uploadVersion(CTX, record.guid, new Uint8Array([2]), 'gh', 'b.gh', SCHEMA);
|
|
108
|
-
|
|
109
|
-
expect(a.versionNumber).toBe(5);
|
|
110
|
-
expect(b.versionNumber).toBe(6);
|
|
111
|
-
expect(a.fileKey).not.toBe(b.fileKey);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('does NOT derive the number from the version list (no listVersions call)', async () => {
|
|
115
|
-
// The old bug read the version list; the fix must not. A deps object with no
|
|
116
|
-
// listVersions still works — reserveNextVersionNumber is the only source.
|
|
117
|
-
const record = fakeRecord('33333333-3333-3333-3333-333333333333', 9);
|
|
118
|
-
const { data, storage } = fakeDeps(record);
|
|
119
|
-
const svc = new DefinitionService(data, storage);
|
|
120
|
-
|
|
121
|
-
const version = await svc.uploadVersion(
|
|
122
|
-
CTX,
|
|
123
|
-
record.guid,
|
|
124
|
-
new Uint8Array([1]),
|
|
125
|
-
'gh',
|
|
126
|
-
'a.gh',
|
|
127
|
-
SCHEMA
|
|
128
|
-
);
|
|
129
|
-
expect(version.versionNumber).toBe(9);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `createDefinitionLoader` (K4) — version resolution, error
|
|
3
|
-
* classification, and the ADR 0005 schema-staleness behavior: a cached schema
|
|
4
|
-
* is used only at the app's current format version; anything else re-extracts
|
|
5
|
-
* from compute and persists back (best-effort, current-version only).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
-
import { UI_SCHEMA_VERSION, type UISchema } from '@selvajs/schemas';
|
|
10
|
-
import type {
|
|
11
|
-
ComputeServerConfig,
|
|
12
|
-
DefinitionRecord,
|
|
13
|
-
DefinitionVersion,
|
|
14
|
-
RequestContext
|
|
15
|
-
} from '@selvajs/platform';
|
|
16
|
-
import { createDefinitionLoader, DefinitionLoadError } from '../load-for-render.js';
|
|
17
|
-
|
|
18
|
-
const ctx = { userId: 'user-1' } as RequestContext;
|
|
19
|
-
|
|
20
|
-
function uiSchema(version: string, inputs: unknown[] = []): UISchema {
|
|
21
|
-
return {
|
|
22
|
-
id: 'schema-1',
|
|
23
|
-
name: 'Test',
|
|
24
|
-
schemaVersion: version,
|
|
25
|
-
inputs,
|
|
26
|
-
outputs: [],
|
|
27
|
-
layout: { type: 'tabbed', tabs: [] }
|
|
28
|
-
} as unknown as UISchema;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const record = {
|
|
32
|
-
guid: 'def-1',
|
|
33
|
-
projectId: 'proj-1',
|
|
34
|
-
computeServerId: null,
|
|
35
|
-
liveVersionId: 'v-live',
|
|
36
|
-
draftVersionId: 'v-draft'
|
|
37
|
-
} as unknown as DefinitionRecord;
|
|
38
|
-
|
|
39
|
-
function version(overrides: Partial<DefinitionVersion> = {}): DefinitionVersion {
|
|
40
|
-
return {
|
|
41
|
-
id: 'v-live',
|
|
42
|
-
definitionId: 'def-1',
|
|
43
|
-
versionNumber: 1,
|
|
44
|
-
fileExt: 'gh',
|
|
45
|
-
fileKey: 'definitions/def-1/versions/v1.gh',
|
|
46
|
-
uploadedBy: 'user-1',
|
|
47
|
-
uploadedAt: '2026-07-01T00:00:00Z',
|
|
48
|
-
...overrides
|
|
49
|
-
} as DefinitionVersion;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const computeServer = { id: 'srv-1', serverUrl: 'http://c.example' } as ComputeServerConfig;
|
|
53
|
-
|
|
54
|
-
function makeDeps(overrides: Record<string, unknown> = {}) {
|
|
55
|
-
const getIO = vi.fn(async () => ({ inputs: [{ id: 'a', default: 42 }], outputs: [] }));
|
|
56
|
-
const deps = {
|
|
57
|
-
storage: { get: vi.fn(async () => new Uint8Array([1, 2, 3])) },
|
|
58
|
-
definitions: {
|
|
59
|
-
getVersion: vi.fn(async () => version()),
|
|
60
|
-
setVersionSchema: vi.fn(async () => {})
|
|
61
|
-
},
|
|
62
|
-
projects: { getProject: vi.fn(async () => ({ id: 'proj-1', orgId: 'org-1' })) },
|
|
63
|
-
resolveServer: vi.fn(async () => computeServer),
|
|
64
|
-
getClient: vi.fn(async () => ({ client: { getIO, getRawIO: vi.fn() } })),
|
|
65
|
-
fetchSchema: vi.fn(async () => uiSchema(UI_SCHEMA_VERSION, [{ id: 'a' }])),
|
|
66
|
-
onWarn: vi.fn(),
|
|
67
|
-
...overrides
|
|
68
|
-
};
|
|
69
|
-
return {
|
|
70
|
-
deps: deps as never as Parameters<typeof createDefinitionLoader>[0],
|
|
71
|
-
mocks: deps,
|
|
72
|
-
getIO
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function flushMicrotasks() {
|
|
77
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe('schema cache staleness (ADR 0005)', () => {
|
|
81
|
-
it('uses the cached schema when its version matches, without re-extracting or persisting', async () => {
|
|
82
|
-
const cached = uiSchema(UI_SCHEMA_VERSION, [{ id: 'a' }]);
|
|
83
|
-
const { deps, mocks } = makeDeps();
|
|
84
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ schema: cached }));
|
|
85
|
-
|
|
86
|
-
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
87
|
-
|
|
88
|
-
expect(mocks.fetchSchema).not.toHaveBeenCalled();
|
|
89
|
-
expect(mocks.definitions.setVersionSchema).not.toHaveBeenCalled();
|
|
90
|
-
// Compute defaults are merged into the cached schema's inputs.
|
|
91
|
-
expect((result.schema.inputs[0] as { default?: unknown }).default).toBe(42);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('re-extracts and persists when the cached schema is at an older format version', async () => {
|
|
95
|
-
const { deps, mocks } = makeDeps();
|
|
96
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ schema: uiSchema('2.10.0') }));
|
|
97
|
-
|
|
98
|
-
await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
99
|
-
await flushMicrotasks();
|
|
100
|
-
|
|
101
|
-
expect(mocks.fetchSchema).toHaveBeenCalledTimes(1);
|
|
102
|
-
expect(mocks.definitions.setVersionSchema).toHaveBeenCalledWith(
|
|
103
|
-
ctx,
|
|
104
|
-
'v-live',
|
|
105
|
-
expect.objectContaining({ schemaVersion: UI_SCHEMA_VERSION })
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('re-extracts and persists when no schema is cached (pre-caching row)', async () => {
|
|
110
|
-
const { deps, mocks } = makeDeps();
|
|
111
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
112
|
-
|
|
113
|
-
await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
114
|
-
await flushMicrotasks();
|
|
115
|
-
|
|
116
|
-
expect(mocks.fetchSchema).toHaveBeenCalledTimes(1);
|
|
117
|
-
expect(mocks.definitions.setVersionSchema).toHaveBeenCalledTimes(1);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('does NOT persist a re-extracted schema that is still not at the current version', async () => {
|
|
121
|
-
// Compute plugin behind the app: re-extraction yields an older format.
|
|
122
|
-
const { deps, mocks } = makeDeps({
|
|
123
|
-
fetchSchema: vi.fn(async () => uiSchema('2.10.0'))
|
|
124
|
-
});
|
|
125
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
126
|
-
|
|
127
|
-
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
128
|
-
await flushMicrotasks();
|
|
129
|
-
|
|
130
|
-
expect(mocks.definitions.setVersionSchema).not.toHaveBeenCalled();
|
|
131
|
-
// The older schema still renders (older shapes only lack optional additions).
|
|
132
|
-
expect(result.schema.schemaVersion).toBe('2.10.0');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('a failed persist warns but does not fail the render', async () => {
|
|
136
|
-
const { deps, mocks } = makeDeps();
|
|
137
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
138
|
-
mocks.definitions.setVersionSchema.mockRejectedValue(new Error('RLS: not allowed'));
|
|
139
|
-
|
|
140
|
-
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
141
|
-
await flushMicrotasks();
|
|
142
|
-
|
|
143
|
-
expect(result.schema.schemaVersion).toBe(UI_SCHEMA_VERSION);
|
|
144
|
-
expect(mocks.onWarn).toHaveBeenCalledWith(expect.stringContaining('cache refresh failed'));
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('version resolution', () => {
|
|
149
|
-
it('resolves the draft pointer for the draft channel', async () => {
|
|
150
|
-
const { deps, mocks } = makeDeps();
|
|
151
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ id: 'v-draft' }));
|
|
152
|
-
|
|
153
|
-
await createDefinitionLoader(deps)(ctx, record, 'draft');
|
|
154
|
-
expect(mocks.definitions.getVersion).toHaveBeenCalledWith(ctx, 'v-draft');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('an explicit versionId wins over the channel pointer', async () => {
|
|
158
|
-
const { deps, mocks } = makeDeps();
|
|
159
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ id: 'v-7' }));
|
|
160
|
-
|
|
161
|
-
await createDefinitionLoader(deps)(ctx, record, 'live', 'v-7');
|
|
162
|
-
expect(mocks.definitions.getVersion).toHaveBeenCalledWith(ctx, 'v-7');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("rejects a version that belongs to another definition ('data')", async () => {
|
|
166
|
-
const { deps, mocks } = makeDeps();
|
|
167
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ definitionId: 'other-def' }));
|
|
168
|
-
|
|
169
|
-
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
170
|
-
name: 'DefinitionLoadError',
|
|
171
|
-
kind: 'data'
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('error classification', () => {
|
|
177
|
-
it("no pointer for the channel → 'data'", async () => {
|
|
178
|
-
const { deps } = makeDeps();
|
|
179
|
-
const bare = { ...record, liveVersionId: null } as unknown as DefinitionRecord;
|
|
180
|
-
await expect(createDefinitionLoader(deps)(ctx, bare, 'live')).rejects.toMatchObject({
|
|
181
|
-
kind: 'data'
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("missing blob → 'data'", async () => {
|
|
186
|
-
const { deps, mocks } = makeDeps();
|
|
187
|
-
mocks.storage.get.mockResolvedValue(null as never);
|
|
188
|
-
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
189
|
-
kind: 'data'
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("unresolvable compute server → 'missing-config'", async () => {
|
|
194
|
-
const { deps, mocks } = makeDeps();
|
|
195
|
-
mocks.resolveServer.mockRejectedValue(new Error('none visible'));
|
|
196
|
-
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
197
|
-
kind: 'missing-config'
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("client connect failure → 'connect'", async () => {
|
|
202
|
-
const { deps, mocks } = makeDeps();
|
|
203
|
-
mocks.getClient.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
204
|
-
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
205
|
-
kind: 'connect'
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("IO/schema phase failure → 'schema'", async () => {
|
|
210
|
-
const { deps, mocks } = makeDeps({
|
|
211
|
-
fetchSchema: vi.fn(async () => {
|
|
212
|
-
throw new Error('no Schema output');
|
|
213
|
-
})
|
|
214
|
-
});
|
|
215
|
-
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
216
|
-
const err = await createDefinitionLoader(deps)(ctx, record, 'live').catch((e) => e);
|
|
217
|
-
expect(err).toBeInstanceOf(DefinitionLoadError);
|
|
218
|
-
expect(err.kind).toBe('schema');
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe('compute default merging', () => {
|
|
223
|
-
beforeEach(() => vi.clearAllMocks());
|
|
224
|
-
|
|
225
|
-
it('converts Color defaults to hex', async () => {
|
|
226
|
-
const { deps, mocks } = makeDeps({
|
|
227
|
-
getClient: vi.fn(async () => ({
|
|
228
|
-
client: {
|
|
229
|
-
getIO: vi.fn(async () => ({
|
|
230
|
-
inputs: [{ id: 'col', paramType: 'Color', default: '255,128,0' }],
|
|
231
|
-
outputs: []
|
|
232
|
-
})),
|
|
233
|
-
getRawIO: vi.fn()
|
|
234
|
-
}
|
|
235
|
-
}))
|
|
236
|
-
});
|
|
237
|
-
mocks.definitions.getVersion.mockResolvedValue(
|
|
238
|
-
version({ schema: uiSchema(UI_SCHEMA_VERSION, [{ id: 'col' }]) })
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
242
|
-
expect((result.schema.inputs[0] as { default?: unknown }).default).toBe('#ff8000');
|
|
243
|
-
});
|
|
244
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the compute-schema extraction gate (K4 / ADR 0005).
|
|
3
|
-
*
|
|
4
|
-
* `fetch` is stubbed per test to script the compute `/grasshopper/schema`
|
|
5
|
-
* response. The version-compat gate (`assertSupportedSchemaVersion`) is also
|
|
6
|
-
* exercised directly: newer-than-supported must throw 'unsupported', while
|
|
7
|
-
* older / missing versions pass (compute's C# migrator emits its own current
|
|
8
|
-
* version; older shapes only lack optional additions).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
-
import { UI_SCHEMA_VERSION, type UISchema } from '@selvajs/schemas';
|
|
13
|
-
import type { ComputeServerConfig } from '@selvajs/platform';
|
|
14
|
-
import {
|
|
15
|
-
fetchSchemaFromCompute,
|
|
16
|
-
assertSupportedSchemaVersion,
|
|
17
|
-
SchemaExtractionError
|
|
18
|
-
} from '../schema-extraction.js';
|
|
19
|
-
|
|
20
|
-
function schema(version?: string): UISchema {
|
|
21
|
-
return {
|
|
22
|
-
id: 'schema-1',
|
|
23
|
-
name: 'Test',
|
|
24
|
-
...(version !== undefined && { schemaVersion: version }),
|
|
25
|
-
inputs: [],
|
|
26
|
-
outputs: [],
|
|
27
|
-
layout: { type: 'tabbed', tabs: [] }
|
|
28
|
-
} as unknown as UISchema;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function bump(version: string, part: 0 | 1 | 2): string {
|
|
32
|
-
const nums = version.split('.').map(Number);
|
|
33
|
-
nums[part] += 1;
|
|
34
|
-
return nums.join('.');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const server: ComputeServerConfig = {
|
|
38
|
-
id: 'srv-1',
|
|
39
|
-
serverUrl: 'http://compute.example.com',
|
|
40
|
-
apiKey: 'secret'
|
|
41
|
-
} as ComputeServerConfig;
|
|
42
|
-
|
|
43
|
-
describe('assertSupportedSchemaVersion', () => {
|
|
44
|
-
it('passes the current version', () => {
|
|
45
|
-
expect(() => assertSupportedSchemaVersion(schema(UI_SCHEMA_VERSION))).not.toThrow();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('passes older versions (older shapes only lack optional additions)', () => {
|
|
49
|
-
expect(() => assertSupportedSchemaVersion(schema('1.0.0'))).not.toThrow();
|
|
50
|
-
expect(() => assertSupportedSchemaVersion(schema('2.0.0'))).not.toThrow();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('passes a schema without a version (pre-2.12.0 plugin)', () => {
|
|
54
|
-
expect(() => assertSupportedSchemaVersion(schema())).not.toThrow();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it.each([
|
|
58
|
-
['major', 0],
|
|
59
|
-
['minor', 1],
|
|
60
|
-
['patch', 2]
|
|
61
|
-
] as const)('throws unsupported for a newer %s version', (_label, part) => {
|
|
62
|
-
const newer = bump(UI_SCHEMA_VERSION, part);
|
|
63
|
-
try {
|
|
64
|
-
assertSupportedSchemaVersion(schema(newer));
|
|
65
|
-
expect.unreachable('should have thrown');
|
|
66
|
-
} catch (err) {
|
|
67
|
-
expect(err).toBeInstanceOf(SchemaExtractionError);
|
|
68
|
-
expect((err as SchemaExtractionError).kind).toBe('unsupported');
|
|
69
|
-
expect((err as SchemaExtractionError).message).toContain(newer);
|
|
70
|
-
expect((err as SchemaExtractionError).message).toContain(UI_SCHEMA_VERSION);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('passes an unparseable version string (fail open — invalid is not "newer")', () => {
|
|
75
|
-
expect(() => assertSupportedSchemaVersion(schema('abc'))).not.toThrow();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('fetchSchemaFromCompute', () => {
|
|
80
|
-
let fetchMock: ReturnType<typeof vi.fn>;
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
fetchMock = vi.fn();
|
|
84
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
vi.unstubAllGlobals();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
function computeResponse(schemas: unknown[]): Response {
|
|
92
|
-
// Compute wraps results with PascalCase keys.
|
|
93
|
-
return new Response(JSON.stringify([{ FileName: 'definition.gh', Schemas: schemas }]), {
|
|
94
|
-
status: 200,
|
|
95
|
-
headers: { 'content-type': 'application/json' }
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
it('extracts the first schema and sends the API key header', async () => {
|
|
100
|
-
fetchMock.mockResolvedValue(computeResponse([schema(UI_SCHEMA_VERSION)]));
|
|
101
|
-
const result = await fetchSchemaFromCompute(new Uint8Array([1]), server);
|
|
102
|
-
expect(result.id).toBe('schema-1');
|
|
103
|
-
|
|
104
|
-
const [url, init] = fetchMock.mock.calls[0];
|
|
105
|
-
expect(url).toBe('http://compute.example.com/grasshopper/schema');
|
|
106
|
-
expect((init.headers as Record<string, string>)['RhinoComputeKey']).toBe('secret');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('throws unreachable when the network request fails', async () => {
|
|
110
|
-
fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
111
|
-
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
112
|
-
name: 'SchemaExtractionError',
|
|
113
|
-
kind: 'unreachable'
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('throws unreachable on a non-OK status', async () => {
|
|
118
|
-
fetchMock.mockResolvedValue(new Response('nope', { status: 500 }));
|
|
119
|
-
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
120
|
-
kind: 'unreachable'
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('throws invalid when the definition yields no schemas', async () => {
|
|
125
|
-
fetchMock.mockResolvedValue(computeResponse([]));
|
|
126
|
-
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
127
|
-
kind: 'invalid'
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('applies the version gate to the extracted schema', async () => {
|
|
132
|
-
fetchMock.mockResolvedValue(computeResponse([schema(bump(UI_SCHEMA_VERSION, 1))]));
|
|
133
|
-
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
134
|
-
kind: 'unsupported'
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
});
|