@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.
Files changed (145) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -0
  3. package/dist/compute/client-cache.d.ts.map +1 -1
  4. package/dist/compute/client-cache.js +6 -0
  5. package/dist/compute/client-cache.js.map +1 -1
  6. package/dist/compute/safe-url.d.ts.map +1 -1
  7. package/dist/compute/safe-url.js +16 -0
  8. package/dist/compute/safe-url.js.map +1 -1
  9. package/dist/compute/solve-cache-single-flight.d.ts +8 -1
  10. package/dist/compute/solve-cache-single-flight.d.ts.map +1 -1
  11. package/dist/compute/solve-cache-single-flight.js +4 -2
  12. package/dist/compute/solve-cache-single-flight.js.map +1 -1
  13. package/dist/compute/solve-pipeline.d.ts.map +1 -1
  14. package/dist/compute/solve-pipeline.js +3 -1
  15. package/dist/compute/solve-pipeline.js.map +1 -1
  16. package/package.json +14 -18
  17. package/dist/access/__tests__/project-access-input.test.d.ts +0 -2
  18. package/dist/access/__tests__/project-access-input.test.d.ts.map +0 -1
  19. package/dist/access/__tests__/project-access-input.test.js +0 -146
  20. package/dist/access/__tests__/project-access-input.test.js.map +0 -1
  21. package/dist/compute/__tests__/client-cache.test.d.ts +0 -11
  22. package/dist/compute/__tests__/client-cache.test.d.ts.map +0 -1
  23. package/dist/compute/__tests__/client-cache.test.js +0 -218
  24. package/dist/compute/__tests__/client-cache.test.js.map +0 -1
  25. package/dist/compute/__tests__/definition-byte-cache.test.d.ts +0 -2
  26. package/dist/compute/__tests__/definition-byte-cache.test.d.ts.map +0 -1
  27. package/dist/compute/__tests__/definition-byte-cache.test.js +0 -107
  28. package/dist/compute/__tests__/definition-byte-cache.test.js.map +0 -1
  29. package/dist/compute/__tests__/rate-limit.test.d.ts +0 -2
  30. package/dist/compute/__tests__/rate-limit.test.d.ts.map +0 -1
  31. package/dist/compute/__tests__/rate-limit.test.js +0 -98
  32. package/dist/compute/__tests__/rate-limit.test.js.map +0 -1
  33. package/dist/compute/__tests__/remote-definition.test.d.ts +0 -11
  34. package/dist/compute/__tests__/remote-definition.test.d.ts.map +0 -1
  35. package/dist/compute/__tests__/remote-definition.test.js +0 -114
  36. package/dist/compute/__tests__/remote-definition.test.js.map +0 -1
  37. package/dist/compute/__tests__/safe-url.test.d.ts +0 -2
  38. package/dist/compute/__tests__/safe-url.test.d.ts.map +0 -1
  39. package/dist/compute/__tests__/safe-url.test.js +0 -71
  40. package/dist/compute/__tests__/safe-url.test.js.map +0 -1
  41. package/dist/compute/__tests__/solve-cache.test.d.ts +0 -13
  42. package/dist/compute/__tests__/solve-cache.test.d.ts.map +0 -1
  43. package/dist/compute/__tests__/solve-cache.test.js +0 -234
  44. package/dist/compute/__tests__/solve-cache.test.js.map +0 -1
  45. package/dist/compute/__tests__/solve-pipeline.test.d.ts +0 -11
  46. package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +0 -1
  47. package/dist/compute/__tests__/solve-pipeline.test.js +0 -557
  48. package/dist/compute/__tests__/solve-pipeline.test.js.map +0 -1
  49. package/dist/compute/__tests__/transform-input.test.d.ts +0 -14
  50. package/dist/compute/__tests__/transform-input.test.d.ts.map +0 -1
  51. package/dist/compute/__tests__/transform-input.test.js +0 -120
  52. package/dist/compute/__tests__/transform-input.test.js.map +0 -1
  53. package/dist/definitions/__tests__/definition-service.test.d.ts +0 -14
  54. package/dist/definitions/__tests__/definition-service.test.d.ts.map +0 -1
  55. package/dist/definitions/__tests__/definition-service.test.js +0 -95
  56. package/dist/definitions/__tests__/definition-service.test.js.map +0 -1
  57. package/dist/definitions/__tests__/load-for-render.test.d.ts +0 -8
  58. package/dist/definitions/__tests__/load-for-render.test.d.ts.map +0 -1
  59. package/dist/definitions/__tests__/load-for-render.test.js +0 -196
  60. package/dist/definitions/__tests__/load-for-render.test.js.map +0 -1
  61. package/dist/definitions/__tests__/schema-extraction.test.d.ts +0 -11
  62. package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +0 -1
  63. package/dist/definitions/__tests__/schema-extraction.test.js +0 -115
  64. package/dist/definitions/__tests__/schema-extraction.test.js.map +0 -1
  65. package/dist/http/__tests__/body-size.test.d.ts +0 -2
  66. package/dist/http/__tests__/body-size.test.d.ts.map +0 -1
  67. package/dist/http/__tests__/body-size.test.js +0 -24
  68. package/dist/http/__tests__/body-size.test.js.map +0 -1
  69. package/dist/http/__tests__/route-classifier.test.d.ts +0 -2
  70. package/dist/http/__tests__/route-classifier.test.d.ts.map +0 -1
  71. package/dist/http/__tests__/route-classifier.test.js +0 -54
  72. package/dist/http/__tests__/route-classifier.test.js.map +0 -1
  73. package/dist/http/__tests__/safe-redirect.test.d.ts +0 -2
  74. package/dist/http/__tests__/safe-redirect.test.d.ts.map +0 -1
  75. package/dist/http/__tests__/safe-redirect.test.js +0 -44
  76. package/dist/http/__tests__/safe-redirect.test.js.map +0 -1
  77. package/dist/http/__tests__/security-headers.test.d.ts +0 -2
  78. package/dist/http/__tests__/security-headers.test.d.ts.map +0 -1
  79. package/dist/http/__tests__/security-headers.test.js +0 -24
  80. package/dist/http/__tests__/security-headers.test.js.map +0 -1
  81. package/dist/ops/__tests__/semver.test.d.ts +0 -2
  82. package/dist/ops/__tests__/semver.test.d.ts.map +0 -1
  83. package/dist/ops/__tests__/semver.test.js +0 -38
  84. package/dist/ops/__tests__/semver.test.js.map +0 -1
  85. package/dist/providers/__tests__/create-selva-providers.test.d.ts +0 -7
  86. package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +0 -1
  87. package/dist/providers/__tests__/create-selva-providers.test.js +0 -135
  88. package/dist/providers/__tests__/create-selva-providers.test.js.map +0 -1
  89. package/dist/tokens/__tests__/token-codec.test.d.ts +0 -2
  90. package/dist/tokens/__tests__/token-codec.test.d.ts.map +0 -1
  91. package/dist/tokens/__tests__/token-codec.test.js +0 -50
  92. package/dist/tokens/__tests__/token-codec.test.js.map +0 -1
  93. package/src/access/__tests__/project-access-input.test.ts +0 -175
  94. package/src/access/index.ts +0 -9
  95. package/src/access/project-access-input.ts +0 -145
  96. package/src/compute/__tests__/client-cache.test.ts +0 -244
  97. package/src/compute/__tests__/definition-byte-cache.test.ts +0 -129
  98. package/src/compute/__tests__/rate-limit.test.ts +0 -106
  99. package/src/compute/__tests__/remote-definition.test.ts +0 -140
  100. package/src/compute/__tests__/safe-url.test.ts +0 -72
  101. package/src/compute/__tests__/solve-cache.test.ts +0 -268
  102. package/src/compute/__tests__/solve-pipeline.test.ts +0 -660
  103. package/src/compute/__tests__/transform-input.test.ts +0 -162
  104. package/src/compute/client-cache.ts +0 -321
  105. package/src/compute/definition-byte-cache.ts +0 -168
  106. package/src/compute/index.ts +0 -82
  107. package/src/compute/limits.ts +0 -237
  108. package/src/compute/memory-solve-cache.ts +0 -0
  109. package/src/compute/rate-limit.ts +0 -110
  110. package/src/compute/remote-definition.ts +0 -149
  111. package/src/compute/safe-url.ts +0 -186
  112. package/src/compute/solve-cache-envelope.ts +0 -98
  113. package/src/compute/solve-cache-key.ts +0 -85
  114. package/src/compute/solve-cache-single-flight.ts +0 -59
  115. package/src/compute/solve-pipeline.ts +0 -603
  116. package/src/compute/transform-input.ts +0 -66
  117. package/src/definitions/__tests__/definition-service.test.ts +0 -131
  118. package/src/definitions/__tests__/load-for-render.test.ts +0 -244
  119. package/src/definitions/__tests__/schema-extraction.test.ts +0 -137
  120. package/src/definitions/definition-service.ts +0 -247
  121. package/src/definitions/index.ts +0 -22
  122. package/src/definitions/load-for-render.ts +0 -272
  123. package/src/definitions/schema-extraction.ts +0 -117
  124. package/src/errors/SentryErrorReporter.ts +0 -88
  125. package/src/errors/index.ts +0 -3
  126. package/src/http/__tests__/body-size.test.ts +0 -27
  127. package/src/http/__tests__/route-classifier.test.ts +0 -60
  128. package/src/http/__tests__/safe-redirect.test.ts +0 -47
  129. package/src/http/__tests__/security-headers.test.ts +0 -29
  130. package/src/http/body-size.ts +0 -24
  131. package/src/http/index.ts +0 -10
  132. package/src/http/redirect.ts +0 -16
  133. package/src/http/route-classifier.ts +0 -74
  134. package/src/http/security-headers.ts +0 -42
  135. package/src/index.ts +0 -27
  136. package/src/ops/__tests__/semver.test.ts +0 -42
  137. package/src/ops/index.ts +0 -3
  138. package/src/ops/semver.ts +0 -60
  139. package/src/providers/__tests__/create-selva-providers.test.ts +0 -172
  140. package/src/providers/__tests__/fixtures/override-config.mjs +0 -8
  141. package/src/providers/create-selva-providers.ts +0 -241
  142. package/src/providers/index.ts +0 -12
  143. package/src/tokens/__tests__/token-codec.test.ts +0 -63
  144. package/src/tokens/index.ts +0 -8
  145. 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
- });