@selvajs/server 0.2.0-beta.2 → 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 +12 -0
- package/dist/compute/client-cache.d.ts.map +1 -1
- package/dist/compute/client-cache.js +18 -3
- package/dist/compute/client-cache.js.map +1 -1
- package/dist/compute/definition-byte-cache.d.ts +85 -0
- package/dist/compute/definition-byte-cache.d.ts.map +1 -0
- package/dist/compute/definition-byte-cache.js +110 -0
- package/dist/compute/definition-byte-cache.js.map +1 -0
- package/dist/compute/index.d.ts +7 -2
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +6 -1
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/limits.d.ts +52 -0
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +33 -1
- package/dist/compute/limits.js.map +1 -1
- package/dist/compute/memory-solve-cache.d.ts +58 -0
- package/dist/compute/memory-solve-cache.d.ts.map +1 -0
- package/dist/compute/memory-solve-cache.js +0 -0
- package/dist/compute/memory-solve-cache.js.map +1 -0
- 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-envelope.d.ts +53 -0
- package/dist/compute/solve-cache-envelope.d.ts.map +1 -0
- package/dist/compute/solve-cache-envelope.js +66 -0
- package/dist/compute/solve-cache-envelope.js.map +1 -0
- package/dist/compute/solve-cache-key.d.ts +65 -0
- package/dist/compute/solve-cache-key.d.ts.map +1 -0
- package/dist/compute/solve-cache-key.js +50 -0
- package/dist/compute/solve-cache-key.js.map +1 -0
- package/dist/compute/solve-cache-single-flight.d.ts +42 -0
- package/dist/compute/solve-cache-single-flight.d.ts.map +1 -0
- package/dist/compute/solve-cache-single-flight.js +50 -0
- package/dist/compute/solve-cache-single-flight.js.map +1 -0
- package/dist/compute/solve-pipeline.d.ts +71 -8
- package/dist/compute/solve-pipeline.d.ts.map +1 -1
- package/dist/compute/solve-pipeline.js +180 -8
- package/dist/compute/solve-pipeline.js.map +1 -1
- package/dist/definitions/definition-service.d.ts +1 -1
- package/dist/definitions/definition-service.d.ts.map +1 -1
- package/dist/definitions/definition-service.js +8 -4
- package/dist/definitions/definition-service.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 -200
- package/dist/compute/__tests__/client-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-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 -292
- 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__/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 -224
- 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-pipeline.test.ts +0 -341
- package/src/compute/__tests__/transform-input.test.ts +0 -162
- package/src/compute/client-cache.ts +0 -300
- package/src/compute/index.ts +0 -48
- package/src/compute/limits.ts +0 -159
- 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-pipeline.ts +0 -339
- package/src/compute/transform-input.ts +0 -66
- 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 -243
- 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,341 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `runSolvePipeline` — the transport-agnostic solve core (K3).
|
|
3
|
-
*
|
|
4
|
-
* `@selvajs/compute` is mocked so the pipeline runs without a live compute
|
|
5
|
-
* server: `TreeBuilder.fromInputParams` returns a marker tree and the scheduler
|
|
6
|
-
* is a stub whose `solve` we script per test (resolve a response, throw an
|
|
7
|
-
* AbortError, etc.). That isolates the pipeline's own behavior — outcome
|
|
8
|
-
* classification, size cap, gzip, and the Server-Timing / version envelope.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
-
import { gunzipSync } from 'node:zlib';
|
|
13
|
-
import type { GrasshopperClient, InputParam, SolveScheduler } from '@selvajs/compute';
|
|
14
|
-
|
|
15
|
-
// --- Mock @selvajs/compute -------------------------------------------------
|
|
16
|
-
vi.mock('@selvajs/compute', () => {
|
|
17
|
-
return {
|
|
18
|
-
TreeBuilder: {
|
|
19
|
-
fromInputParams: (params: unknown[]) => ({ __tree: true, count: params.length })
|
|
20
|
-
},
|
|
21
|
-
// transform-input.ts (imported by the pipeline) calls processInput; return
|
|
22
|
-
// the raw shape as an InputParam so filtering/mapping runs end-to-end.
|
|
23
|
-
processInput: (raw: InputParam) => raw
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
runSolvePipeline,
|
|
29
|
-
COMPUTE_CONTRACT_VERSION,
|
|
30
|
-
COMPUTE_VERSION_HEADER,
|
|
31
|
-
type SolvePipelineArgs,
|
|
32
|
-
type PipelineInput,
|
|
33
|
-
type CachedClient
|
|
34
|
-
} from '../index.js';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A scripted solve stub. The pipeline only JSON-serializes and reads
|
|
38
|
-
* `errors`/`warnings`, so tests return arbitrary fake payloads — the return is a
|
|
39
|
-
* loose record rather than the full `GrasshopperComputeResponse`.
|
|
40
|
-
*/
|
|
41
|
-
type SolveStub = (
|
|
42
|
-
def: string | Uint8Array,
|
|
43
|
-
tree: unknown,
|
|
44
|
-
opts: { signal?: AbortSignal }
|
|
45
|
-
) => Promise<Record<string, unknown>>;
|
|
46
|
-
|
|
47
|
-
/** A CachedClient stub whose scheduler.solve is scripted per test. */
|
|
48
|
-
function fakeClient(solve: SolveStub): CachedClient {
|
|
49
|
-
return {
|
|
50
|
-
client: {} as GrasshopperClient,
|
|
51
|
-
scheduler: { solve } as unknown as SolveScheduler,
|
|
52
|
-
rhinoTiming: { last: null, seq: 0 },
|
|
53
|
-
solveMeta: { last: null, seq: 0 }
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function baseArgs(over: Partial<SolvePipelineArgs> = {}): SolvePipelineArgs {
|
|
58
|
-
return {
|
|
59
|
-
definitionSource: new Uint8Array([1, 2, 3]),
|
|
60
|
-
inputs: [{ id: 'a', paramType: 'number', nickname: 'A' } as PipelineInput],
|
|
61
|
-
values: { a: 5 },
|
|
62
|
-
client: fakeClient(async () => ({ values: [], errors: [], warnings: [] })),
|
|
63
|
-
responseMaxBytes: 10 * 1024 * 1024,
|
|
64
|
-
maxSolveDurationMs: 100_000,
|
|
65
|
-
acceptEncoding: '',
|
|
66
|
-
signal: new AbortController().signal,
|
|
67
|
-
loadStartMs: performance.now(),
|
|
68
|
-
defLoadMs: 1.5,
|
|
69
|
-
prepMarks: [['body', 0.2]],
|
|
70
|
-
...over
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
describe('runSolvePipeline — success', () => {
|
|
75
|
-
it('returns an ok envelope with a JSON body and the version header', async () => {
|
|
76
|
-
const result = { values: [{ id: 'out' }], errors: [], warnings: ['w'] };
|
|
77
|
-
const outcome = await runSolvePipeline(baseArgs({ client: fakeClient(async () => result) }));
|
|
78
|
-
expect(outcome.kind).toBe('ok');
|
|
79
|
-
if (outcome.kind !== 'ok') return;
|
|
80
|
-
expect(outcome.errorCount).toBe(0);
|
|
81
|
-
expect(outcome.warningCount).toBe(1);
|
|
82
|
-
expect(outcome.envelope.encoding).toBeUndefined();
|
|
83
|
-
expect(JSON.parse(outcome.envelope.body as string)).toEqual(result);
|
|
84
|
-
expect(outcome.envelope.headers[COMPUTE_VERSION_HEADER]).toBe(String(COMPUTE_CONTRACT_VERSION));
|
|
85
|
-
expect(outcome.envelope.headers['Content-Type']).toBe('application/json');
|
|
86
|
-
expect(outcome.envelope.headers['Content-Length']).toBe(
|
|
87
|
-
String(Buffer.byteLength(outcome.envelope.body as string))
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('only sends inputs with a paramType to the tree builder', async () => {
|
|
92
|
-
let seenTree: unknown;
|
|
93
|
-
const outcome = await runSolvePipeline(
|
|
94
|
-
baseArgs({
|
|
95
|
-
inputs: [
|
|
96
|
-
{ id: 'a', paramType: 'number', nickname: 'A' } as PipelineInput,
|
|
97
|
-
{ id: 'b', nickname: 'B' } as PipelineInput // no paramType → filtered out
|
|
98
|
-
],
|
|
99
|
-
client: fakeClient(async (_def, tree) => {
|
|
100
|
-
seenTree = tree;
|
|
101
|
-
return { errors: [], warnings: [] };
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
);
|
|
105
|
-
expect(outcome.kind).toBe('ok');
|
|
106
|
-
expect(seenTree).toEqual({ __tree: true, count: 1 });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('forwards the abort signal to the scheduler', async () => {
|
|
110
|
-
const controller = new AbortController();
|
|
111
|
-
let seenSignal: AbortSignal | undefined;
|
|
112
|
-
await runSolvePipeline(
|
|
113
|
-
baseArgs({
|
|
114
|
-
signal: controller.signal,
|
|
115
|
-
client: fakeClient(async (_d, _t, opts) => {
|
|
116
|
-
seenSignal = opts.signal;
|
|
117
|
-
return { errors: [], warnings: [] };
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
);
|
|
121
|
-
expect(seenSignal).toBe(controller.signal);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('runSolvePipeline — gzip', () => {
|
|
126
|
-
it('compresses when Accept-Encoding allows and the body is over 1 KB', async () => {
|
|
127
|
-
// A payload comfortably over the 1 KB gzip threshold.
|
|
128
|
-
const big = { values: Array.from({ length: 500 }, (_, i) => ({ i, s: 'xxxxxxxxxx' })) };
|
|
129
|
-
const outcome = await runSolvePipeline(
|
|
130
|
-
baseArgs({ acceptEncoding: 'gzip, br', client: fakeClient(async () => big) })
|
|
131
|
-
);
|
|
132
|
-
expect(outcome.kind).toBe('ok');
|
|
133
|
-
if (outcome.kind !== 'ok') return;
|
|
134
|
-
expect(outcome.envelope.encoding).toBe('gzip');
|
|
135
|
-
expect(outcome.envelope.headers['Content-Encoding']).toBe('gzip');
|
|
136
|
-
expect(outcome.envelope.headers['Vary']).toBe('Accept-Encoding');
|
|
137
|
-
// The gzip body round-trips back to the original JSON.
|
|
138
|
-
const inflated = gunzipSync(Buffer.from(outcome.envelope.body as Uint8Array)).toString();
|
|
139
|
-
expect(JSON.parse(inflated)).toEqual(big);
|
|
140
|
-
expect(outcome.envelope.metrics.compressedBytes).toBeGreaterThan(0);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('does NOT compress when Accept-Encoding lacks gzip', async () => {
|
|
144
|
-
const big = { values: Array.from({ length: 500 }, (_, i) => ({ i, s: 'xxxxxxxxxx' })) };
|
|
145
|
-
const outcome = await runSolvePipeline(
|
|
146
|
-
baseArgs({ acceptEncoding: 'br', client: fakeClient(async () => big) })
|
|
147
|
-
);
|
|
148
|
-
expect(outcome.kind).toBe('ok');
|
|
149
|
-
if (outcome.kind !== 'ok') return;
|
|
150
|
-
expect(outcome.envelope.encoding).toBeUndefined();
|
|
151
|
-
expect(outcome.envelope.headers['Content-Encoding']).toBeUndefined();
|
|
152
|
-
expect(outcome.envelope.metrics.compressedBytes).toBeNull();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('does NOT compress a tiny body even with gzip advertised', async () => {
|
|
156
|
-
const outcome = await runSolvePipeline(
|
|
157
|
-
baseArgs({ acceptEncoding: 'gzip', client: fakeClient(async () => ({ ok: 1 })) })
|
|
158
|
-
);
|
|
159
|
-
expect(outcome.kind).toBe('ok');
|
|
160
|
-
if (outcome.kind !== 'ok') return;
|
|
161
|
-
expect(outcome.envelope.encoding).toBeUndefined();
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('runSolvePipeline — failure outcomes', () => {
|
|
166
|
-
it('classifies the scheduler deadline (AbortError, signal NOT aborted) as timeout', async () => {
|
|
167
|
-
const outcome = await runSolvePipeline(
|
|
168
|
-
baseArgs({
|
|
169
|
-
maxSolveDurationMs: 42_000,
|
|
170
|
-
client: fakeClient(async () => {
|
|
171
|
-
const e = new Error('aborted');
|
|
172
|
-
e.name = 'AbortError';
|
|
173
|
-
throw e;
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
);
|
|
177
|
-
expect(outcome.kind).toBe('timeout');
|
|
178
|
-
if (outcome.kind !== 'timeout') return;
|
|
179
|
-
expect(outcome.message).toContain('42s');
|
|
180
|
-
expect(outcome.durationMs).toBeGreaterThanOrEqual(0);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('classifies AbortError with the request signal aborted as client_abort', async () => {
|
|
184
|
-
const controller = new AbortController();
|
|
185
|
-
controller.abort();
|
|
186
|
-
const outcome = await runSolvePipeline(
|
|
187
|
-
baseArgs({
|
|
188
|
-
signal: controller.signal,
|
|
189
|
-
client: fakeClient(async () => {
|
|
190
|
-
const e = new Error('aborted');
|
|
191
|
-
e.name = 'AbortError';
|
|
192
|
-
throw e;
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
);
|
|
196
|
-
expect(outcome.kind).toBe('client_abort');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('classifies any other throw as compute_error carrying the original error', async () => {
|
|
200
|
-
const boom = new TypeError('fetch failed');
|
|
201
|
-
const outcome = await runSolvePipeline(
|
|
202
|
-
baseArgs({
|
|
203
|
-
client: fakeClient(async () => {
|
|
204
|
-
throw boom;
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
);
|
|
208
|
-
expect(outcome.kind).toBe('compute_error');
|
|
209
|
-
if (outcome.kind !== 'compute_error') return;
|
|
210
|
-
expect(outcome.error).toBe(boom);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('returns too_large when the serialized result exceeds responseMaxBytes', async () => {
|
|
214
|
-
const outcome = await runSolvePipeline(
|
|
215
|
-
baseArgs({
|
|
216
|
-
responseMaxBytes: 10, // tiny cap
|
|
217
|
-
client: fakeClient(async () => ({ values: 'a bigger than ten bytes payload' }))
|
|
218
|
-
})
|
|
219
|
-
);
|
|
220
|
-
expect(outcome.kind).toBe('too_large');
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe('runSolvePipeline — Server-Timing', () => {
|
|
225
|
-
it('emits the core phases and echoes prep marks as p_*', async () => {
|
|
226
|
-
const outcome = await runSolvePipeline(
|
|
227
|
-
baseArgs({
|
|
228
|
-
defLoadMs: 12.3,
|
|
229
|
-
prepMarks: [
|
|
230
|
-
['body', 0.5],
|
|
231
|
-
['client', 3.1]
|
|
232
|
-
],
|
|
233
|
-
client: fakeClient(async () => ({ errors: [], warnings: [] }))
|
|
234
|
-
})
|
|
235
|
-
);
|
|
236
|
-
expect(outcome.kind).toBe('ok');
|
|
237
|
-
if (outcome.kind !== 'ok') return;
|
|
238
|
-
const st = outcome.envelope.headers['Server-Timing'];
|
|
239
|
-
expect(st).toContain('load;dur=12.3');
|
|
240
|
-
expect(st).toContain('tree;dur=');
|
|
241
|
-
expect(st).toContain('solve;dur=');
|
|
242
|
-
expect(st).toContain('serialize;dur=');
|
|
243
|
-
expect(st).toContain('total;dur=');
|
|
244
|
-
expect(st).toContain('p_body;dur=0.5');
|
|
245
|
-
expect(st).toContain('p_client;dur=3.1');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('splits solve into rhino_* + compute_link when the client reported server timing', async () => {
|
|
249
|
-
// The real callbacks fire DURING the solve: onServerTiming per compute
|
|
250
|
-
// call, onSettle per settle — each bumps its seq. Mimic both so the
|
|
251
|
-
// pipeline's attribution guard (exactly one write since the snapshot,
|
|
252
|
-
// settle not fromCache) sees this request's own telemetry.
|
|
253
|
-
const client: CachedClient = fakeClient(async () => {
|
|
254
|
-
client.rhinoTiming.seq += 1;
|
|
255
|
-
client.rhinoTiming.last = { decode: 5, solve: 20, encode: 3 };
|
|
256
|
-
client.solveMeta.seq += 1;
|
|
257
|
-
client.solveMeta.last = { fromCache: false, definitionReuploaded: false };
|
|
258
|
-
return { errors: [], warnings: [] };
|
|
259
|
-
});
|
|
260
|
-
const outcome = await runSolvePipeline(baseArgs({ client }));
|
|
261
|
-
expect(outcome.kind).toBe('ok');
|
|
262
|
-
if (outcome.kind !== 'ok') return;
|
|
263
|
-
const st = outcome.envelope.headers['Server-Timing'];
|
|
264
|
-
expect(st).toContain('rhino_decode;dur=5.0');
|
|
265
|
-
expect(st).toContain('rhino_solve;dur=20.0');
|
|
266
|
-
expect(st).toContain('rhino_encode;dur=3.0');
|
|
267
|
-
expect(st).toContain('compute_link;dur=');
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('surfaces selva_cache + def_reupload verdicts as 0/1 flags', async () => {
|
|
271
|
-
const client: CachedClient = fakeClient(async () => {
|
|
272
|
-
client.solveMeta.seq += 1;
|
|
273
|
-
client.solveMeta.last = { fromCache: true, definitionReuploaded: false };
|
|
274
|
-
return { errors: [], warnings: [] };
|
|
275
|
-
});
|
|
276
|
-
const outcome = await runSolvePipeline(baseArgs({ client }));
|
|
277
|
-
expect(outcome.kind).toBe('ok');
|
|
278
|
-
if (outcome.kind !== 'ok') return;
|
|
279
|
-
const st = outcome.envelope.headers['Server-Timing'];
|
|
280
|
-
expect(st).toContain('selva_cache;dur=1');
|
|
281
|
-
expect(st).toContain('def_reupload;dur=0');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('does not attribute stale telemetry a previous request left on the warm client', async () => {
|
|
285
|
-
// The holders are shared per-server. Data written before this solve's
|
|
286
|
-
// snapshot (seq unchanged during the solve) must not leak into this
|
|
287
|
-
// request's Server-Timing.
|
|
288
|
-
const client = fakeClient(async () => ({ errors: [], warnings: [] }));
|
|
289
|
-
client.rhinoTiming = { last: { decode: 99, solve: 99, encode: 99 }, seq: 3 };
|
|
290
|
-
client.solveMeta = { last: { fromCache: true }, seq: 5 };
|
|
291
|
-
const outcome = await runSolvePipeline(baseArgs({ client }));
|
|
292
|
-
expect(outcome.kind).toBe('ok');
|
|
293
|
-
if (outcome.kind !== 'ok') return;
|
|
294
|
-
expect(outcome.envelope.headers['Server-Timing']).not.toContain('rhino_decode');
|
|
295
|
-
expect(outcome.envelope.headers['Server-Timing']).not.toContain('selva_cache');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('drops telemetry when a concurrent request also settled in the window (audit B6)', async () => {
|
|
299
|
-
// With maxConcurrentSolves > 1 two solves can interleave. More than one
|
|
300
|
-
// settle since this request's snapshot makes the shared slots ambiguous —
|
|
301
|
-
// the pipeline must omit the segments rather than report another
|
|
302
|
-
// request's cache verdict / rhino timing as ours.
|
|
303
|
-
const client: CachedClient = fakeClient(async () => {
|
|
304
|
-
// Our own solve's callbacks…
|
|
305
|
-
client.rhinoTiming.seq += 1;
|
|
306
|
-
client.rhinoTiming.last = { decode: 5, solve: 20, encode: 3 };
|
|
307
|
-
client.solveMeta.seq += 1;
|
|
308
|
-
client.solveMeta.last = { fromCache: false, definitionReuploaded: false };
|
|
309
|
-
// …then a concurrent request settles before we read.
|
|
310
|
-
client.rhinoTiming.seq += 1;
|
|
311
|
-
client.rhinoTiming.last = { decode: 1, solve: 1, encode: 1 };
|
|
312
|
-
client.solveMeta.seq += 1;
|
|
313
|
-
client.solveMeta.last = { fromCache: true };
|
|
314
|
-
return { errors: [], warnings: [] };
|
|
315
|
-
});
|
|
316
|
-
const outcome = await runSolvePipeline(baseArgs({ client }));
|
|
317
|
-
expect(outcome.kind).toBe('ok');
|
|
318
|
-
if (outcome.kind !== 'ok') return;
|
|
319
|
-
const st = outcome.envelope.headers['Server-Timing'];
|
|
320
|
-
expect(st).not.toContain('selva_cache');
|
|
321
|
-
expect(st).not.toContain('rhino_decode');
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('never attributes rhino timing to a Selva-cache hit (no compute call of ours)', async () => {
|
|
325
|
-
// Our request was served from the in-process cache while a concurrent
|
|
326
|
-
// request's compute call produced the only rhino timing in the window.
|
|
327
|
-
const client: CachedClient = fakeClient(async () => {
|
|
328
|
-
client.rhinoTiming.seq += 1; // the OTHER request's compute call
|
|
329
|
-
client.rhinoTiming.last = { decode: 7, solve: 70, encode: 7 };
|
|
330
|
-
client.solveMeta.seq += 1; // our own settle — a cache hit
|
|
331
|
-
client.solveMeta.last = { fromCache: true };
|
|
332
|
-
return { errors: [], warnings: [] };
|
|
333
|
-
});
|
|
334
|
-
const outcome = await runSolvePipeline(baseArgs({ client }));
|
|
335
|
-
expect(outcome.kind).toBe('ok');
|
|
336
|
-
if (outcome.kind !== 'ok') return;
|
|
337
|
-
const st = outcome.envelope.headers['Server-Timing'];
|
|
338
|
-
expect(st).toContain('selva_cache;dur=1');
|
|
339
|
-
expect(st).not.toContain('rhino_decode');
|
|
340
|
-
});
|
|
341
|
-
});
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `transformInputParameter` — the adapter that turns a persisted `SchemaInput`
|
|
3
|
-
* (plus the user's chosen value) into the `@selvajs/compute` `InputParam` that
|
|
4
|
-
* `TreeBuilder.fromInputParams` serializes for the solve.
|
|
5
|
-
*
|
|
6
|
-
* The adapter delegates to the package's `processInput`, so these assert the
|
|
7
|
-
* package-aligned behavior: correct `paramType` for every type (not the old
|
|
8
|
-
* coerce-everything-to-Text), and `default` carrying the user's value. Where a value/
|
|
9
|
-
* default is absent, `processInput` leaves `default` undefined and `fromInputParams`
|
|
10
|
-
* omits the input entirely — letting Grasshopper use its own internal default rather
|
|
11
|
-
* than forcing `''`/`false`.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect } from 'vitest';
|
|
15
|
-
import { transformInputParameter } from '../transform-input.js';
|
|
16
|
-
import type { SchemaInput } from '@selvajs/schemas';
|
|
17
|
-
|
|
18
|
-
type Input = SchemaInput & { minimum?: number; maximum?: number; stepSize?: number };
|
|
19
|
-
|
|
20
|
-
function input(partial: Partial<Input>): Input {
|
|
21
|
-
return {
|
|
22
|
-
id: 'id-1',
|
|
23
|
-
nickname: 'Param',
|
|
24
|
-
paramType: 'text',
|
|
25
|
-
...partial
|
|
26
|
-
} as Input;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe('transformInputParameter — identity fields', () => {
|
|
30
|
-
it('carries id, name, nickname, description through', () => {
|
|
31
|
-
const out = transformInputParameter(
|
|
32
|
-
input({ id: 'guid-x', nickname: 'Width', description: 'the width' }),
|
|
33
|
-
5
|
|
34
|
-
);
|
|
35
|
-
expect(out.id).toBe('guid-x');
|
|
36
|
-
expect(out.name).toBe('Width');
|
|
37
|
-
expect(out.nickname).toBe('Width');
|
|
38
|
-
expect(out.description).toBe('the width');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('defaults a blank nickname to null and an absent description to empty string', () => {
|
|
42
|
-
const out = transformInputParameter(input({ nickname: '', description: undefined }), 'v');
|
|
43
|
-
expect(out.nickname).toBeNull();
|
|
44
|
-
expect(out.description).toBe('');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('transformInputParameter — number / integer', () => {
|
|
49
|
-
it('maps number with the user value winning over the schema default', () => {
|
|
50
|
-
const out = transformInputParameter(
|
|
51
|
-
input({ paramType: 'number', default: 1, minimum: 0, maximum: 10, stepSize: 0.5 }),
|
|
52
|
-
7
|
|
53
|
-
);
|
|
54
|
-
expect(out).toMatchObject({ paramType: 'Number', default: 7, minimum: 0, maximum: 10 });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('falls back to the schema default when no value is supplied', () => {
|
|
58
|
-
const out = transformInputParameter(input({ paramType: 'number', default: 3 }), undefined);
|
|
59
|
-
expect(out.default).toBe(3);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('honors a schema-authored stepSize verbatim for integers (compute ≥3.1.0-beta.5)', () => {
|
|
63
|
-
// The package used to force integer stepSize to 1; since beta.5 a
|
|
64
|
-
// server-authored stepSize wins and the 1-heuristic is only the fallback.
|
|
65
|
-
const out = transformInputParameter(
|
|
66
|
-
input({ paramType: 'integer', stepSize: 0.5, default: 2 }),
|
|
67
|
-
4
|
|
68
|
-
);
|
|
69
|
-
expect(out).toMatchObject({ paramType: 'Integer', stepSize: 0.5, default: 4 });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('falls back to stepSize 1 for integers without an authored stepSize', () => {
|
|
73
|
-
const out = transformInputParameter(input({ paramType: 'integer', default: 2 }), 4);
|
|
74
|
-
expect(out).toMatchObject({ paramType: 'Integer', stepSize: 1, default: 4 });
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('transformInputParameter — text', () => {
|
|
79
|
-
it('maps text and keeps the user value', () => {
|
|
80
|
-
const out = transformInputParameter(input({ paramType: 'text', default: 'd' }), 'hello');
|
|
81
|
-
expect(out).toMatchObject({ paramType: 'Text', default: 'hello' });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('falls back to the schema default when no value is supplied', () => {
|
|
85
|
-
expect(
|
|
86
|
-
transformInputParameter(input({ paramType: 'text', default: 'd' }), undefined).default
|
|
87
|
-
).toBe('d');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('leaves default undefined when neither value nor schema default exist (input is then omitted)', () => {
|
|
91
|
-
// processInput does not force ''. fromInputParams filters undefined defaults, so the
|
|
92
|
-
// param is simply not sent and Grasshopper uses its own internal default.
|
|
93
|
-
expect(
|
|
94
|
-
transformInputParameter(input({ paramType: 'text', default: undefined }), undefined).default
|
|
95
|
-
).toBeUndefined();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('transformInputParameter — boolean', () => {
|
|
100
|
-
it('maps boolean and preserves an explicit false (the bug-prone case)', () => {
|
|
101
|
-
const out = transformInputParameter(input({ paramType: 'boolean', default: true }), false);
|
|
102
|
-
expect(out).toMatchObject({ paramType: 'Boolean', default: false });
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('falls back to the schema default when no value is supplied', () => {
|
|
106
|
-
expect(
|
|
107
|
-
transformInputParameter(input({ paramType: 'boolean', default: true }), undefined).default
|
|
108
|
-
).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('leaves default undefined when neither value nor schema default exist (input is then omitted)', () => {
|
|
112
|
-
expect(
|
|
113
|
-
transformInputParameter(input({ paramType: 'boolean', default: undefined }), undefined)
|
|
114
|
-
.default
|
|
115
|
-
).toBeUndefined();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('transformInputParameter — value-list / file / color / generic', () => {
|
|
120
|
-
// The regression that motivated the swap: these used to be silently coerced to Text.
|
|
121
|
-
// processInput now types them correctly. (The options map for ValueList isn't carried
|
|
122
|
-
// on SchemaInput, so `values` is empty and the selected value rides on `default` —
|
|
123
|
-
// which is exactly the string a contextual ValueList param consumes.)
|
|
124
|
-
it('maps valueList to ValueList, carrying the selected value', () => {
|
|
125
|
-
const out = transformInputParameter(input({ paramType: 'valueList', default: '0' }), '1');
|
|
126
|
-
expect(out).toMatchObject({ paramType: 'ValueList', default: '1' });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Regression: processInput has no dynamicValueList handler, so without the explicit
|
|
130
|
-
// mapping it fell to the Geometry fallback with a null default — and fromInputParams
|
|
131
|
-
// then DROPPED the input, so the user's selection never reached the definition.
|
|
132
|
-
// The plugin's GetDynamicValueListParameter reuses the ValueList wire contract
|
|
133
|
-
// (TypeName "ValueList"), so ValueList is the correct target type.
|
|
134
|
-
it('maps dynamicValueList to ValueList, carrying the selected value', () => {
|
|
135
|
-
const out = transformInputParameter(
|
|
136
|
-
input({ paramType: 'dynamicValueList', default: '0' }),
|
|
137
|
-
'3.5'
|
|
138
|
-
);
|
|
139
|
-
expect(out).toMatchObject({ paramType: 'ValueList', default: '3.5' });
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('maps dynamicValueList checklist (array value) to ValueList, carrying all selections', () => {
|
|
143
|
-
const out = transformInputParameter(input({ paramType: 'dynamicValueList' }), ['a', 'b']);
|
|
144
|
-
expect(out.paramType).toBe('ValueList');
|
|
145
|
-
expect(out.default).toEqual(['a', 'b']);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('maps file to File', () => {
|
|
149
|
-
const out = transformInputParameter(input({ paramType: 'file' }), { fileName: 'a.3dm' });
|
|
150
|
-
expect(out.paramType).toBe('File');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('maps color to Color, carrying the value', () => {
|
|
154
|
-
const out = transformInputParameter(input({ paramType: 'color' }), '255,0,0');
|
|
155
|
-
expect(out).toMatchObject({ paramType: 'Color', default: '255,0,0' });
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('maps generic to the package fallback type (Geometry), not Text', () => {
|
|
159
|
-
const out = transformInputParameter(input({ paramType: 'generic', default: 'd' }), undefined);
|
|
160
|
-
expect(out.paramType).toBe('Geometry');
|
|
161
|
-
});
|
|
162
|
-
});
|