@selvajs/server 0.2.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 +21 -0
- package/dist/compute/__tests__/client-cache.test.d.ts +11 -0
- package/dist/compute/__tests__/client-cache.test.d.ts.map +1 -0
- package/dist/compute/__tests__/client-cache.test.js +149 -0
- package/dist/compute/__tests__/client-cache.test.js.map +1 -0
- package/dist/compute/__tests__/rate-limit.test.d.ts +2 -0
- package/dist/compute/__tests__/rate-limit.test.d.ts.map +1 -0
- package/dist/compute/__tests__/rate-limit.test.js +62 -0
- package/dist/compute/__tests__/rate-limit.test.js.map +1 -0
- package/dist/compute/__tests__/remote-definition.test.d.ts +11 -0
- package/dist/compute/__tests__/remote-definition.test.d.ts.map +1 -0
- package/dist/compute/__tests__/remote-definition.test.js +114 -0
- package/dist/compute/__tests__/remote-definition.test.js.map +1 -0
- package/dist/compute/__tests__/safe-url.test.d.ts +2 -0
- package/dist/compute/__tests__/safe-url.test.d.ts.map +1 -0
- package/dist/compute/__tests__/safe-url.test.js +71 -0
- package/dist/compute/__tests__/safe-url.test.js.map +1 -0
- package/dist/compute/__tests__/solve-pipeline.test.d.ts +11 -0
- package/dist/compute/__tests__/solve-pipeline.test.d.ts.map +1 -0
- package/dist/compute/__tests__/solve-pipeline.test.js +245 -0
- package/dist/compute/__tests__/solve-pipeline.test.js.map +1 -0
- package/dist/compute/__tests__/transform-input.test.d.ts +14 -0
- package/dist/compute/__tests__/transform-input.test.d.ts.map +1 -0
- package/dist/compute/__tests__/transform-input.test.js +114 -0
- package/dist/compute/__tests__/transform-input.test.js.map +1 -0
- package/dist/compute/client-cache.d.ts +104 -0
- package/dist/compute/client-cache.d.ts.map +1 -0
- package/dist/compute/client-cache.js +147 -0
- package/dist/compute/client-cache.js.map +1 -0
- package/dist/compute/index.d.ts +8 -0
- package/dist/compute/index.d.ts.map +1 -0
- package/dist/compute/index.js +9 -0
- package/dist/compute/index.js.map +1 -0
- package/dist/compute/limits.d.ts +102 -0
- package/dist/compute/limits.d.ts.map +1 -0
- package/dist/compute/limits.js +76 -0
- package/dist/compute/limits.js.map +1 -0
- package/dist/compute/rate-limit.d.ts +49 -0
- package/dist/compute/rate-limit.d.ts.map +1 -0
- package/dist/compute/rate-limit.js +49 -0
- package/dist/compute/rate-limit.js.map +1 -0
- package/dist/compute/remote-definition.d.ts +53 -0
- package/dist/compute/remote-definition.d.ts.map +1 -0
- package/dist/compute/remote-definition.js +110 -0
- package/dist/compute/remote-definition.js.map +1 -0
- package/dist/compute/safe-url.d.ts +15 -0
- package/dist/compute/safe-url.d.ts.map +1 -0
- package/dist/compute/safe-url.js +195 -0
- package/dist/compute/safe-url.js.map +1 -0
- package/dist/compute/solve-pipeline.d.ts +127 -0
- package/dist/compute/solve-pipeline.d.ts.map +1 -0
- package/dist/compute/solve-pipeline.js +195 -0
- package/dist/compute/solve-pipeline.js.map +1 -0
- package/dist/compute/transform-input.d.ts +20 -0
- package/dist/compute/transform-input.d.ts.map +1 -0
- package/dist/compute/transform-input.js +47 -0
- package/dist/compute/transform-input.js.map +1 -0
- package/dist/definitions/__tests__/load-for-render.test.d.ts +8 -0
- package/dist/definitions/__tests__/load-for-render.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/load-for-render.test.js +196 -0
- package/dist/definitions/__tests__/load-for-render.test.js.map +1 -0
- package/dist/definitions/__tests__/schema-extraction.test.d.ts +11 -0
- package/dist/definitions/__tests__/schema-extraction.test.d.ts.map +1 -0
- package/dist/definitions/__tests__/schema-extraction.test.js +115 -0
- package/dist/definitions/__tests__/schema-extraction.test.js.map +1 -0
- package/dist/definitions/definition-service.d.ts +77 -0
- package/dist/definitions/definition-service.d.ts.map +1 -0
- package/dist/definitions/definition-service.js +184 -0
- package/dist/definitions/definition-service.js.map +1 -0
- package/dist/definitions/index.d.ts +4 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/index.js +8 -0
- package/dist/definitions/index.js.map +1 -0
- package/dist/definitions/load-for-render.d.ts +61 -0
- package/dist/definitions/load-for-render.d.ts.map +1 -0
- package/dist/definitions/load-for-render.js +165 -0
- package/dist/definitions/load-for-render.js.map +1 -0
- package/dist/definitions/schema-extraction.d.ts +22 -0
- package/dist/definitions/schema-extraction.d.ts.map +1 -0
- package/dist/definitions/schema-extraction.js +89 -0
- package/dist/definitions/schema-extraction.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/__tests__/create-selva-providers.test.d.ts +7 -0
- package/dist/providers/__tests__/create-selva-providers.test.d.ts.map +1 -0
- package/dist/providers/__tests__/create-selva-providers.test.js +135 -0
- package/dist/providers/__tests__/create-selva-providers.test.js.map +1 -0
- package/dist/providers/create-selva-providers.d.ts +53 -0
- package/dist/providers/create-selva-providers.d.ts.map +1 -0
- package/dist/providers/create-selva-providers.js +152 -0
- package/dist/providers/create-selva-providers.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +6 -0
- package/dist/providers/index.js.map +1 -0
- package/package.json +60 -0
- package/src/compute/__tests__/client-cache.test.ts +164 -0
- package/src/compute/__tests__/rate-limit.test.ts +69 -0
- package/src/compute/__tests__/remote-definition.test.ts +140 -0
- package/src/compute/__tests__/safe-url.test.ts +72 -0
- package/src/compute/__tests__/solve-pipeline.test.ts +294 -0
- package/src/compute/__tests__/transform-input.test.ts +155 -0
- package/src/compute/client-cache.ts +278 -0
- package/src/compute/index.ts +48 -0
- package/src/compute/limits.ts +147 -0
- package/src/compute/rate-limit.ts +83 -0
- package/src/compute/remote-definition.ts +149 -0
- package/src/compute/safe-url.ts +186 -0
- package/src/compute/solve-pipeline.ts +324 -0
- package/src/compute/transform-input.ts +52 -0
- package/src/definitions/__tests__/load-for-render.test.ts +244 -0
- package/src/definitions/__tests__/schema-extraction.test.ts +137 -0
- package/src/definitions/definition-service.ts +243 -0
- package/src/definitions/index.ts +22 -0
- package/src/definitions/load-for-render.ts +272 -0
- package/src/definitions/schema-extraction.ts +117 -0
- package/src/index.ts +18 -0
- package/src/providers/__tests__/create-selva-providers.test.ts +172 -0
- package/src/providers/__tests__/fixtures/override-config.mjs +8 -0
- package/src/providers/create-selva-providers.ts +241 -0
- package/src/providers/index.ts +12 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic solve pipeline — the callable core lifted out of the app's
|
|
3
|
+
* `/api/compute` route (embeddable-server-layer plan, K3).
|
|
4
|
+
*
|
|
5
|
+
* Given an already-resolved solve context — the `.gh` bytes, the input params +
|
|
6
|
+
* user values, and a warm `SolveScheduler` (from `createClientCache`) — this
|
|
7
|
+
* runs the framework-free half of a solve:
|
|
8
|
+
*
|
|
9
|
+
* input tree build → scheduler.solve (with abort/timeout classification) →
|
|
10
|
+
* JSON serialize (size-guarded) → optional gzip → Server-Timing envelope.
|
|
11
|
+
*
|
|
12
|
+
* It returns a discriminated {@link SolveOutcome}: `ok` carries the ready-to-send
|
|
13
|
+
* {@link SolveEnvelope} (body + headers + phase metrics), while the error
|
|
14
|
+
* variants (`timeout`, `client_abort`, `too_large`, `compute_error`) name a
|
|
15
|
+
* failure the transport maps to its own status code. Nothing here throws for an
|
|
16
|
+
* expected failure and nothing here touches auth, the database, share tokens,
|
|
17
|
+
* rate limits, or metric sinks — those stay app policy in the route.
|
|
18
|
+
*
|
|
19
|
+
* The Server-Timing string it emits IS the versioned wire contract (audit D5):
|
|
20
|
+
* see {@link COMPUTE_CONTRACT_VERSION} and {@link COMPUTE_VERSION_HEADER}.
|
|
21
|
+
*/
|
|
22
|
+
import { TreeBuilder } from '@selvajs/compute';
|
|
23
|
+
import { gzipSync } from 'node:zlib';
|
|
24
|
+
import { transformInputParameter } from './transform-input.js';
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Wire contract version (audit D5)
|
|
27
|
+
// ============================================================================
|
|
28
|
+
//
|
|
29
|
+
// The /api/compute response shape (JSON body + Server-Timing phases) is now an
|
|
30
|
+
// explicit, versioned artifact rather than ad-hoc route output. The version
|
|
31
|
+
// rides an additive response header so a client can branch on it without any
|
|
32
|
+
// change to the existing body — bump this (and document the change) whenever the
|
|
33
|
+
// envelope's shape changes in a way a consumer could observe.
|
|
34
|
+
/** Current compute-response contract version. Additive header; bump on shape change. */
|
|
35
|
+
export const COMPUTE_CONTRACT_VERSION = 1;
|
|
36
|
+
/** Response header carrying {@link COMPUTE_CONTRACT_VERSION}. */
|
|
37
|
+
export const COMPUTE_VERSION_HEADER = 'X-Selva-Compute-Version';
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Pipeline
|
|
40
|
+
// ============================================================================
|
|
41
|
+
/**
|
|
42
|
+
* Run the framework-free half of a solve and produce a typed outcome. See the
|
|
43
|
+
* module doc for the phase sequence and the app/library boundary.
|
|
44
|
+
*/
|
|
45
|
+
export async function runSolvePipeline(args) {
|
|
46
|
+
const { client, signal } = args;
|
|
47
|
+
// --- input tree build ---------------------------------------------------
|
|
48
|
+
const treeBuildStart = performance.now();
|
|
49
|
+
const inputTree = TreeBuilder.fromInputParams(args.inputs
|
|
50
|
+
.filter((input) => input.paramType)
|
|
51
|
+
.map((input) => transformInputParameter(input, args.values[input.id])));
|
|
52
|
+
const treeBuildMs = performance.now() - treeBuildStart;
|
|
53
|
+
// --- solve --------------------------------------------------------------
|
|
54
|
+
// Reset the per-request telemetry holders so a Selva-cache hit (no compute
|
|
55
|
+
// call) isn't attributed a stale timing from an earlier request on this warm
|
|
56
|
+
// client. See the rhinoTiming/solveMeta concurrency caveat in client-cache.
|
|
57
|
+
client.rhinoTiming.last = null;
|
|
58
|
+
client.solveMeta.last = null;
|
|
59
|
+
let result;
|
|
60
|
+
const solveStart = performance.now();
|
|
61
|
+
try {
|
|
62
|
+
result = await client.scheduler.solve(args.definitionSource, inputTree, { signal });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const durationMs = performance.now() - solveStart;
|
|
66
|
+
// Distinguish timeout (scheduler deadline timer) from client disconnect
|
|
67
|
+
// (request signal). AbortError with the request signal NOT aborted means
|
|
68
|
+
// the scheduler's own timeout fired — the solve genuinely timed out.
|
|
69
|
+
const isAbort = err instanceof Error && err.name === 'AbortError';
|
|
70
|
+
if (isAbort) {
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
return { kind: 'client_abort', durationMs };
|
|
73
|
+
return {
|
|
74
|
+
kind: 'timeout',
|
|
75
|
+
durationMs,
|
|
76
|
+
message: `Solve exceeded the ${Math.round(args.maxSolveDurationMs / 1000)}s deadline.`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return { kind: 'compute_error', durationMs, error: err };
|
|
80
|
+
}
|
|
81
|
+
const solveMs = performance.now() - solveStart;
|
|
82
|
+
// --- serialize (size-guarded) ------------------------------------------
|
|
83
|
+
const serializeStart = performance.now();
|
|
84
|
+
let serialized;
|
|
85
|
+
try {
|
|
86
|
+
serialized = JSON.stringify(result);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
// V8 caps a single string at ~512 MB; an oversized `file` output trips a
|
|
90
|
+
// RangeError here. Surface it as the same too-large outcome as the byte cap.
|
|
91
|
+
if (err instanceof RangeError)
|
|
92
|
+
return { kind: 'too_large' };
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
if (serialized.length > args.responseMaxBytes)
|
|
96
|
+
return { kind: 'too_large' };
|
|
97
|
+
const serializeMs = performance.now() - serializeStart;
|
|
98
|
+
// --- gzip ---------------------------------------------------------------
|
|
99
|
+
// Compress BEFORE the timing snapshot so its cost is a measured phase (`gzip`
|
|
100
|
+
// in Server-Timing, included in `total`). Buffered (not streamed) so
|
|
101
|
+
// Content-Length is known and a connection cut mid-transfer fails hard
|
|
102
|
+
// instead of truncating the JSON.
|
|
103
|
+
let compressed = null;
|
|
104
|
+
let gzipMs = 0;
|
|
105
|
+
if (/\bgzip\b/i.test(args.acceptEncoding) && serialized.length > 1024) {
|
|
106
|
+
const gzipStart = performance.now();
|
|
107
|
+
compressed = gzipSync(Buffer.from(serialized));
|
|
108
|
+
gzipMs = performance.now() - gzipStart;
|
|
109
|
+
}
|
|
110
|
+
const serverTotalMs = performance.now() - args.loadStartMs;
|
|
111
|
+
// --- Server-Timing envelope (the versioned wire contract) ---------------
|
|
112
|
+
const serverTiming = buildServerTiming({
|
|
113
|
+
defLoadMs: args.defLoadMs,
|
|
114
|
+
treeBuildMs,
|
|
115
|
+
solveMs,
|
|
116
|
+
serializeMs,
|
|
117
|
+
gzipMs,
|
|
118
|
+
serverTotalMs,
|
|
119
|
+
rhino: client.rhinoTiming.last,
|
|
120
|
+
settle: client.solveMeta.last,
|
|
121
|
+
prepMarks: args.prepMarks ?? []
|
|
122
|
+
});
|
|
123
|
+
const headers = {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'Server-Timing': serverTiming,
|
|
126
|
+
[COMPUTE_VERSION_HEADER]: String(COMPUTE_CONTRACT_VERSION),
|
|
127
|
+
Vary: 'Accept-Encoding'
|
|
128
|
+
};
|
|
129
|
+
const metrics = {
|
|
130
|
+
treeBuildMs,
|
|
131
|
+
solveMs,
|
|
132
|
+
serializeMs,
|
|
133
|
+
gzipMs,
|
|
134
|
+
serverTotalMs,
|
|
135
|
+
serializedBytes: serialized.length,
|
|
136
|
+
compressedBytes: compressed?.byteLength ?? null
|
|
137
|
+
};
|
|
138
|
+
let envelope;
|
|
139
|
+
if (compressed) {
|
|
140
|
+
// A downstream proxy's `encode gzip` skips already-encoded responses, so
|
|
141
|
+
// this never double-compresses. Vary is set on both branches.
|
|
142
|
+
headers['Content-Encoding'] = 'gzip';
|
|
143
|
+
headers['Content-Length'] = String(compressed.byteLength);
|
|
144
|
+
envelope = { body: new Uint8Array(compressed), encoding: 'gzip', headers, result, metrics };
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
headers['Content-Length'] = String(Buffer.byteLength(serialized));
|
|
148
|
+
envelope = { body: serialized, headers, result, metrics };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
kind: 'ok',
|
|
152
|
+
envelope,
|
|
153
|
+
solveMs,
|
|
154
|
+
errorCount: result.errors?.length ?? 0,
|
|
155
|
+
warningCount: result.warnings?.length ?? 0
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Assemble the `Server-Timing` header. The browser reads these to attribute its
|
|
160
|
+
* round-trip: `total` here == the server's headers-to-out wall time, so browser
|
|
161
|
+
* `ttfb − total` ≈ network+send latency. When the compute server reported its
|
|
162
|
+
* own decode/solve/encode (VektorNode fork), `rhino_*` is time ON the compute
|
|
163
|
+
* server and `compute_link` is everything between (network + queue wait). Prep
|
|
164
|
+
* sub-phases ride as `p_*`; cache verdicts as 0/1 `selva_cache`/`def_reupload`.
|
|
165
|
+
*/
|
|
166
|
+
function buildServerTiming(parts) {
|
|
167
|
+
let header = `load;dur=${parts.defLoadMs.toFixed(1)}, ` +
|
|
168
|
+
`tree;dur=${parts.treeBuildMs.toFixed(1)}, ` +
|
|
169
|
+
`solve;dur=${parts.solveMs.toFixed(1)}, ` +
|
|
170
|
+
`serialize;dur=${parts.serializeMs.toFixed(1)}, ` +
|
|
171
|
+
`gzip;dur=${parts.gzipMs.toFixed(1)}, ` +
|
|
172
|
+
`total;dur=${parts.serverTotalMs.toFixed(1)}`;
|
|
173
|
+
const rhino = parts.rhino;
|
|
174
|
+
if (rhino) {
|
|
175
|
+
const onRhinoMs = rhino.decode + rhino.solve + rhino.encode;
|
|
176
|
+
const computeLinkMs = Math.max(0, parts.solveMs - onRhinoMs);
|
|
177
|
+
header +=
|
|
178
|
+
`, rhino_decode;dur=${rhino.decode.toFixed(1)}` +
|
|
179
|
+
`, rhino_solve;dur=${rhino.solve.toFixed(1)}` +
|
|
180
|
+
`, rhino_encode;dur=${rhino.encode.toFixed(1)}` +
|
|
181
|
+
`, compute_link;dur=${computeLinkMs.toFixed(1)}`;
|
|
182
|
+
}
|
|
183
|
+
for (const [label, ms] of parts.prepMarks) {
|
|
184
|
+
header += `, p_${label};dur=${ms.toFixed(1)}`;
|
|
185
|
+
}
|
|
186
|
+
const settle = parts.settle;
|
|
187
|
+
if (settle) {
|
|
188
|
+
header += `, selva_cache;dur=${settle.fromCache ? 1 : 0}`;
|
|
189
|
+
if (settle.definitionReuploaded !== undefined) {
|
|
190
|
+
header += `, def_reupload;dur=${settle.definitionReuploaded ? 1 : 0}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return header;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=solve-pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"solve-pipeline.js","sourceRoot":"","sources":["../../src/compute/solve-pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type InputParam } from '@selvajs/compute';
|
|
2
|
+
import type { SchemaInput } from '@selvajs/schemas';
|
|
3
|
+
/**
|
|
4
|
+
* Adapt a persisted `SchemaInput` (plus the user's chosen value) into the package's
|
|
5
|
+
* raw `InputParamSchema`, then let `@selvajs/compute`'s own `processInput` produce the
|
|
6
|
+
* typed `InputParam`. The package handles every paramType (Number/Integer/Text/Boolean/
|
|
7
|
+
* ValueList/Geometry/File/Color), normalizes the lowercase UI paramType to its
|
|
8
|
+
* PascalCase form, and falls back gracefully on underspecified inputs — so we no longer
|
|
9
|
+
* maintain a parallel hand-rolled transform that silently coerced valueList/file/color
|
|
10
|
+
* to Text and drifted from the package (and from the parapet app, which already delegates
|
|
11
|
+
* here). `values`/`acceptedFormats` are absent on `SchemaInput` (they live on the layout
|
|
12
|
+
* item's config), so valueList/file inputs fall back to carrying the selected value with
|
|
13
|
+
* no option list — the same value that reaches Grasshopper.
|
|
14
|
+
*/
|
|
15
|
+
export declare function transformInputParameter(input: SchemaInput & {
|
|
16
|
+
minimum?: number;
|
|
17
|
+
maximum?: number;
|
|
18
|
+
stepSize?: number;
|
|
19
|
+
}, value: unknown): InputParam;
|
|
20
|
+
//# sourceMappingURL=transform-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform-input.d.ts","sourceRoot":"","sources":["../../src/compute/transform-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAuC,MAAM,kBAAkB,CAAC;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,WAAW,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAC9E,KAAK,EAAE,OAAO,GACZ,UAAU,CAiCZ"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { processInput } from '@selvajs/compute';
|
|
2
|
+
/**
|
|
3
|
+
* Adapt a persisted `SchemaInput` (plus the user's chosen value) into the package's
|
|
4
|
+
* raw `InputParamSchema`, then let `@selvajs/compute`'s own `processInput` produce the
|
|
5
|
+
* typed `InputParam`. The package handles every paramType (Number/Integer/Text/Boolean/
|
|
6
|
+
* ValueList/Geometry/File/Color), normalizes the lowercase UI paramType to its
|
|
7
|
+
* PascalCase form, and falls back gracefully on underspecified inputs — so we no longer
|
|
8
|
+
* maintain a parallel hand-rolled transform that silently coerced valueList/file/color
|
|
9
|
+
* to Text and drifted from the package (and from the parapet app, which already delegates
|
|
10
|
+
* here). `values`/`acceptedFormats` are absent on `SchemaInput` (they live on the layout
|
|
11
|
+
* item's config), so valueList/file inputs fall back to carrying the selected value with
|
|
12
|
+
* no option list — the same value that reaches Grasshopper.
|
|
13
|
+
*/
|
|
14
|
+
export function transformInputParameter(input, value) {
|
|
15
|
+
const raw = {
|
|
16
|
+
id: input.id,
|
|
17
|
+
name: input.nickname,
|
|
18
|
+
nickname: input.nickname || null,
|
|
19
|
+
description: input.description || '',
|
|
20
|
+
// Dynamic value lists ride the ValueList contract on the wire — the plugin's
|
|
21
|
+
// GetDynamicValueListParameter deliberately reuses TypeName "ValueList" so the
|
|
22
|
+
// compute fork's existing ValueList input case assigns it. processInput has no
|
|
23
|
+
// dynamicValueList handler; without this mapping it falls back to Geometry with
|
|
24
|
+
// a null default and TreeBuilder DROPS the input, so the user's selection never
|
|
25
|
+
// reaches the definition (the param then solves on its own stale fallback).
|
|
26
|
+
paramType: input.paramType === 'dynamicValueList' ? 'valueList' : input.paramType,
|
|
27
|
+
treeAccess: input.inputStructure === 'tree',
|
|
28
|
+
minimum: input.minimum ?? null,
|
|
29
|
+
maximum: input.maximum ?? null,
|
|
30
|
+
atLeast: 1,
|
|
31
|
+
atMost: 1,
|
|
32
|
+
stepSize: input.stepSize,
|
|
33
|
+
// The user's selection wins; otherwise the schema default.
|
|
34
|
+
default: value ?? input.default
|
|
35
|
+
};
|
|
36
|
+
const processed = processInput(raw);
|
|
37
|
+
// processInput's default-normalization only understands scalars and innerTree
|
|
38
|
+
// objects — a plain array default (multi-select widgets, e.g. a dynamic value
|
|
39
|
+
// list checklist) is dropped as "malformed", which would omit the input from
|
|
40
|
+
// the solve entirely. Restore it; TreeBuilder.appendFlat handles arrays natively.
|
|
41
|
+
const effective = value ?? input.default;
|
|
42
|
+
if (Array.isArray(effective) && processed.default == null) {
|
|
43
|
+
processed.default = effective;
|
|
44
|
+
}
|
|
45
|
+
return processed;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=transform-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform-input.js","sourceRoot":"","sources":["../../src/compute/transform-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGxF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CACtC,KAA8E,EAC9E,KAAc;IAEd,MAAM,GAAG,GAAqB;QAC7B,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,IAAI,EAAE,KAAK,CAAC,QAAQ;QACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;QAChC,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,EAAE;QACpC,6EAA6E;QAC7E,+EAA+E;QAC/E,+EAA+E;QAC/E,gFAAgF;QAChF,gFAAgF;QAChF,4EAA4E;QAC5E,SAAS,EAAE,KAAK,CAAC,SAAS,KAAK,kBAAkB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS;QACjF,UAAU,EAAE,KAAK,CAAC,cAAc,KAAK,MAAM;QAC3C,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;QAC9B,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;QAC9B,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,2DAA2D;QAC3D,OAAO,EAAE,KAAK,IAAI,KAAK,CAAC,OAAO;KAC/B,CAAC;IAEF,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACpC,8EAA8E;IAC9E,8EAA8E;IAC9E,6EAA6E;IAC7E,kFAAkF;IAClF,MAAM,SAAS,GAAG,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QAC1D,SAAkC,CAAC,OAAO,GAAG,SAAS,CAAC;IACzD,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=load-for-render.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-for-render.test.d.ts","sourceRoot":"","sources":["../../../src/definitions/__tests__/load-for-render.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import { UI_SCHEMA_VERSION } from '@selvajs/schemas';
|
|
9
|
+
import { createDefinitionLoader, DefinitionLoadError } from '../load-for-render.js';
|
|
10
|
+
const ctx = { userId: 'user-1' };
|
|
11
|
+
function uiSchema(version, inputs = []) {
|
|
12
|
+
return {
|
|
13
|
+
id: 'schema-1',
|
|
14
|
+
name: 'Test',
|
|
15
|
+
schemaVersion: version,
|
|
16
|
+
inputs,
|
|
17
|
+
outputs: [],
|
|
18
|
+
layout: { type: 'tabbed', tabs: [] }
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const record = {
|
|
22
|
+
guid: 'def-1',
|
|
23
|
+
projectId: 'proj-1',
|
|
24
|
+
computeServerId: null,
|
|
25
|
+
liveVersionId: 'v-live',
|
|
26
|
+
draftVersionId: 'v-draft'
|
|
27
|
+
};
|
|
28
|
+
function version(overrides = {}) {
|
|
29
|
+
return {
|
|
30
|
+
id: 'v-live',
|
|
31
|
+
definitionId: 'def-1',
|
|
32
|
+
versionNumber: 1,
|
|
33
|
+
fileExt: 'gh',
|
|
34
|
+
fileKey: 'definitions/def-1/versions/v1.gh',
|
|
35
|
+
uploadedBy: 'user-1',
|
|
36
|
+
uploadedAt: '2026-07-01T00:00:00Z',
|
|
37
|
+
...overrides
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const computeServer = { id: 'srv-1', serverUrl: 'http://c.example' };
|
|
41
|
+
function makeDeps(overrides = {}) {
|
|
42
|
+
const getIO = vi.fn(async () => ({ inputs: [{ id: 'a', default: 42 }], outputs: [] }));
|
|
43
|
+
const deps = {
|
|
44
|
+
storage: { get: vi.fn(async () => new Uint8Array([1, 2, 3])) },
|
|
45
|
+
definitions: {
|
|
46
|
+
getVersion: vi.fn(async () => version()),
|
|
47
|
+
setVersionSchema: vi.fn(async () => { })
|
|
48
|
+
},
|
|
49
|
+
projects: { getProject: vi.fn(async () => ({ id: 'proj-1', orgId: 'org-1' })) },
|
|
50
|
+
resolveServer: vi.fn(async () => computeServer),
|
|
51
|
+
getClient: vi.fn(async () => ({ client: { getIO, getRawIO: vi.fn() } })),
|
|
52
|
+
fetchSchema: vi.fn(async () => uiSchema(UI_SCHEMA_VERSION, [{ id: 'a' }])),
|
|
53
|
+
onWarn: vi.fn(),
|
|
54
|
+
...overrides
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
deps: deps,
|
|
58
|
+
mocks: deps,
|
|
59
|
+
getIO
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function flushMicrotasks() {
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
64
|
+
}
|
|
65
|
+
describe('schema cache staleness (ADR 0005)', () => {
|
|
66
|
+
it('uses the cached schema when its version matches, without re-extracting or persisting', async () => {
|
|
67
|
+
const cached = uiSchema(UI_SCHEMA_VERSION, [{ id: 'a' }]);
|
|
68
|
+
const { deps, mocks } = makeDeps();
|
|
69
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: cached }));
|
|
70
|
+
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
71
|
+
expect(mocks.fetchSchema).not.toHaveBeenCalled();
|
|
72
|
+
expect(mocks.definitions.setVersionSchema).not.toHaveBeenCalled();
|
|
73
|
+
// Compute defaults are merged into the cached schema's inputs.
|
|
74
|
+
expect(result.schema.inputs[0].default).toBe(42);
|
|
75
|
+
});
|
|
76
|
+
it('re-extracts and persists when the cached schema is at an older format version', async () => {
|
|
77
|
+
const { deps, mocks } = makeDeps();
|
|
78
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: uiSchema('2.10.0') }));
|
|
79
|
+
await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
80
|
+
await flushMicrotasks();
|
|
81
|
+
expect(mocks.fetchSchema).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(mocks.definitions.setVersionSchema).toHaveBeenCalledWith(ctx, 'v-live', expect.objectContaining({ schemaVersion: UI_SCHEMA_VERSION }));
|
|
83
|
+
});
|
|
84
|
+
it('re-extracts and persists when no schema is cached (pre-caching row)', async () => {
|
|
85
|
+
const { deps, mocks } = makeDeps();
|
|
86
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
87
|
+
await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
88
|
+
await flushMicrotasks();
|
|
89
|
+
expect(mocks.fetchSchema).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(mocks.definitions.setVersionSchema).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
it('does NOT persist a re-extracted schema that is still not at the current version', async () => {
|
|
93
|
+
// Compute plugin behind the app: re-extraction yields an older format.
|
|
94
|
+
const { deps, mocks } = makeDeps({
|
|
95
|
+
fetchSchema: vi.fn(async () => uiSchema('2.10.0'))
|
|
96
|
+
});
|
|
97
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
98
|
+
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
99
|
+
await flushMicrotasks();
|
|
100
|
+
expect(mocks.definitions.setVersionSchema).not.toHaveBeenCalled();
|
|
101
|
+
// The older schema still renders (older shapes only lack optional additions).
|
|
102
|
+
expect(result.schema.schemaVersion).toBe('2.10.0');
|
|
103
|
+
});
|
|
104
|
+
it('a failed persist warns but does not fail the render', async () => {
|
|
105
|
+
const { deps, mocks } = makeDeps();
|
|
106
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
107
|
+
mocks.definitions.setVersionSchema.mockRejectedValue(new Error('RLS: not allowed'));
|
|
108
|
+
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
109
|
+
await flushMicrotasks();
|
|
110
|
+
expect(result.schema.schemaVersion).toBe(UI_SCHEMA_VERSION);
|
|
111
|
+
expect(mocks.onWarn).toHaveBeenCalledWith(expect.stringContaining('cache refresh failed'));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('version resolution', () => {
|
|
115
|
+
it('resolves the draft pointer for the draft channel', async () => {
|
|
116
|
+
const { deps, mocks } = makeDeps();
|
|
117
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ id: 'v-draft' }));
|
|
118
|
+
await createDefinitionLoader(deps)(ctx, record, 'draft');
|
|
119
|
+
expect(mocks.definitions.getVersion).toHaveBeenCalledWith(ctx, 'v-draft');
|
|
120
|
+
});
|
|
121
|
+
it('an explicit versionId wins over the channel pointer', async () => {
|
|
122
|
+
const { deps, mocks } = makeDeps();
|
|
123
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ id: 'v-7' }));
|
|
124
|
+
await createDefinitionLoader(deps)(ctx, record, 'live', 'v-7');
|
|
125
|
+
expect(mocks.definitions.getVersion).toHaveBeenCalledWith(ctx, 'v-7');
|
|
126
|
+
});
|
|
127
|
+
it("rejects a version that belongs to another definition ('data')", async () => {
|
|
128
|
+
const { deps, mocks } = makeDeps();
|
|
129
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ definitionId: 'other-def' }));
|
|
130
|
+
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
131
|
+
name: 'DefinitionLoadError',
|
|
132
|
+
kind: 'data'
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('error classification', () => {
|
|
137
|
+
it("no pointer for the channel → 'data'", async () => {
|
|
138
|
+
const { deps } = makeDeps();
|
|
139
|
+
const bare = { ...record, liveVersionId: null };
|
|
140
|
+
await expect(createDefinitionLoader(deps)(ctx, bare, 'live')).rejects.toMatchObject({
|
|
141
|
+
kind: 'data'
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
it("missing blob → 'data'", async () => {
|
|
145
|
+
const { deps, mocks } = makeDeps();
|
|
146
|
+
mocks.storage.get.mockResolvedValue(null);
|
|
147
|
+
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
148
|
+
kind: 'data'
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
it("unresolvable compute server → 'missing-config'", async () => {
|
|
152
|
+
const { deps, mocks } = makeDeps();
|
|
153
|
+
mocks.resolveServer.mockRejectedValue(new Error('none visible'));
|
|
154
|
+
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
155
|
+
kind: 'missing-config'
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
it("client connect failure → 'connect'", async () => {
|
|
159
|
+
const { deps, mocks } = makeDeps();
|
|
160
|
+
mocks.getClient.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
161
|
+
await expect(createDefinitionLoader(deps)(ctx, record, 'live')).rejects.toMatchObject({
|
|
162
|
+
kind: 'connect'
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
it("IO/schema phase failure → 'schema'", async () => {
|
|
166
|
+
const { deps, mocks } = makeDeps({
|
|
167
|
+
fetchSchema: vi.fn(async () => {
|
|
168
|
+
throw new Error('no Schema output');
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: undefined }));
|
|
172
|
+
const err = await createDefinitionLoader(deps)(ctx, record, 'live').catch((e) => e);
|
|
173
|
+
expect(err).toBeInstanceOf(DefinitionLoadError);
|
|
174
|
+
expect(err.kind).toBe('schema');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('compute default merging', () => {
|
|
178
|
+
beforeEach(() => vi.clearAllMocks());
|
|
179
|
+
it('converts Color defaults to hex', async () => {
|
|
180
|
+
const { deps, mocks } = makeDeps({
|
|
181
|
+
getClient: vi.fn(async () => ({
|
|
182
|
+
client: {
|
|
183
|
+
getIO: vi.fn(async () => ({
|
|
184
|
+
inputs: [{ id: 'col', paramType: 'Color', default: '255,128,0' }],
|
|
185
|
+
outputs: []
|
|
186
|
+
})),
|
|
187
|
+
getRawIO: vi.fn()
|
|
188
|
+
}
|
|
189
|
+
}))
|
|
190
|
+
});
|
|
191
|
+
mocks.definitions.getVersion.mockResolvedValue(version({ schema: uiSchema(UI_SCHEMA_VERSION, [{ id: 'col' }]) }));
|
|
192
|
+
const result = await createDefinitionLoader(deps)(ctx, record, 'live');
|
|
193
|
+
expect(result.schema.inputs[0].default).toBe('#ff8000');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=load-for-render.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-for-render.test.js","sourceRoot":"","sources":["../../../src/definitions/__tests__/load-for-render.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=schema-extraction.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-extraction.test.d.ts","sourceRoot":"","sources":["../../../src/definitions/__tests__/schema-extraction.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { UI_SCHEMA_VERSION } from '@selvajs/schemas';
|
|
12
|
+
import { fetchSchemaFromCompute, assertSupportedSchemaVersion, SchemaExtractionError } from '../schema-extraction.js';
|
|
13
|
+
function schema(version) {
|
|
14
|
+
return {
|
|
15
|
+
id: 'schema-1',
|
|
16
|
+
name: 'Test',
|
|
17
|
+
...(version !== undefined && { schemaVersion: version }),
|
|
18
|
+
inputs: [],
|
|
19
|
+
outputs: [],
|
|
20
|
+
layout: { type: 'tabbed', tabs: [] }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function bump(version, part) {
|
|
24
|
+
const nums = version.split('.').map(Number);
|
|
25
|
+
nums[part] += 1;
|
|
26
|
+
return nums.join('.');
|
|
27
|
+
}
|
|
28
|
+
const server = {
|
|
29
|
+
id: 'srv-1',
|
|
30
|
+
serverUrl: 'http://compute.example.com',
|
|
31
|
+
apiKey: 'secret'
|
|
32
|
+
};
|
|
33
|
+
describe('assertSupportedSchemaVersion', () => {
|
|
34
|
+
it('passes the current version', () => {
|
|
35
|
+
expect(() => assertSupportedSchemaVersion(schema(UI_SCHEMA_VERSION))).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
it('passes older versions (older shapes only lack optional additions)', () => {
|
|
38
|
+
expect(() => assertSupportedSchemaVersion(schema('1.0.0'))).not.toThrow();
|
|
39
|
+
expect(() => assertSupportedSchemaVersion(schema('2.0.0'))).not.toThrow();
|
|
40
|
+
});
|
|
41
|
+
it('passes a schema without a version (pre-2.12.0 plugin)', () => {
|
|
42
|
+
expect(() => assertSupportedSchemaVersion(schema())).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
it.each([
|
|
45
|
+
['major', 0],
|
|
46
|
+
['minor', 1],
|
|
47
|
+
['patch', 2]
|
|
48
|
+
])('throws unsupported for a newer %s version', (_label, part) => {
|
|
49
|
+
const newer = bump(UI_SCHEMA_VERSION, part);
|
|
50
|
+
try {
|
|
51
|
+
assertSupportedSchemaVersion(schema(newer));
|
|
52
|
+
expect.unreachable('should have thrown');
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
expect(err).toBeInstanceOf(SchemaExtractionError);
|
|
56
|
+
expect(err.kind).toBe('unsupported');
|
|
57
|
+
expect(err.message).toContain(newer);
|
|
58
|
+
expect(err.message).toContain(UI_SCHEMA_VERSION);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
it('passes an unparseable version string (fail open — invalid is not "newer")', () => {
|
|
62
|
+
expect(() => assertSupportedSchemaVersion(schema('abc'))).not.toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('fetchSchemaFromCompute', () => {
|
|
66
|
+
let fetchMock;
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
fetchMock = vi.fn();
|
|
69
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.unstubAllGlobals();
|
|
73
|
+
});
|
|
74
|
+
function computeResponse(schemas) {
|
|
75
|
+
// Compute wraps results with PascalCase keys.
|
|
76
|
+
return new Response(JSON.stringify([{ FileName: 'definition.gh', Schemas: schemas }]), {
|
|
77
|
+
status: 200,
|
|
78
|
+
headers: { 'content-type': 'application/json' }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
it('extracts the first schema and sends the API key header', async () => {
|
|
82
|
+
fetchMock.mockResolvedValue(computeResponse([schema(UI_SCHEMA_VERSION)]));
|
|
83
|
+
const result = await fetchSchemaFromCompute(new Uint8Array([1]), server);
|
|
84
|
+
expect(result.id).toBe('schema-1');
|
|
85
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
86
|
+
expect(url).toBe('http://compute.example.com/grasshopper/schema');
|
|
87
|
+
expect(init.headers['RhinoComputeKey']).toBe('secret');
|
|
88
|
+
});
|
|
89
|
+
it('throws unreachable when the network request fails', async () => {
|
|
90
|
+
fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
91
|
+
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
92
|
+
name: 'SchemaExtractionError',
|
|
93
|
+
kind: 'unreachable'
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('throws unreachable on a non-OK status', async () => {
|
|
97
|
+
fetchMock.mockResolvedValue(new Response('nope', { status: 500 }));
|
|
98
|
+
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
99
|
+
kind: 'unreachable'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('throws invalid when the definition yields no schemas', async () => {
|
|
103
|
+
fetchMock.mockResolvedValue(computeResponse([]));
|
|
104
|
+
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
105
|
+
kind: 'invalid'
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it('applies the version gate to the extracted schema', async () => {
|
|
109
|
+
fetchMock.mockResolvedValue(computeResponse([schema(bump(UI_SCHEMA_VERSION, 1))]));
|
|
110
|
+
await expect(fetchSchemaFromCompute(new Uint8Array([1]), server)).rejects.toMatchObject({
|
|
111
|
+
kind: 'unsupported'
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=schema-extraction.test.js.map
|