@stardeck-customer-apps/testing 0.13.0 → 0.15.0
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/SKILL.md +34 -0
- package/dist/index.d.mts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +134 -5
- package/dist/index.mjs +134 -5
- package/dist/next/headers-shim.js +3 -0
- package/dist/next/headers-shim.mjs +3 -0
- package/dist/setup.js +82 -2
- package/dist/setup.mjs +82 -2
- package/package.json +9 -3
package/SKILL.md
CHANGED
|
@@ -295,6 +295,32 @@ expect(app.edge.latestDisplay()?.action).toBe("show");
|
|
|
295
295
|
- `allowNetwork` — permit non-Stardeck outbound fetches (off by default).
|
|
296
296
|
- `app.reset()` — wipe + re-apply schema + re-seed + clear inbox/sessions.
|
|
297
297
|
Call in `beforeEach`.
|
|
298
|
+
- `app.reset({ mode: "data" })` — available in testing SDK **0.14.0+**. Faster
|
|
299
|
+
opt-in for stable schemas: truncate ordinary tables in `public`, restart their
|
|
300
|
+
owned serial/identity sequences, re-run the data-only seed, and clear the same
|
|
301
|
+
simulator/user/session state as the default. It preserves tables and other
|
|
302
|
+
schema objects instead of rebuilding them. The default remains full schema
|
|
303
|
+
reset; `app.reset({ mode: "schema" })` explicitly selects that default.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// Only after verifying this suite keeps its schema fixed and uses a data-only seed:
|
|
307
|
+
beforeEach(() => app.reset({ mode: "data" }));
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Keep `app.reset()` for tests that change schema or depend on INSERTs in the
|
|
311
|
+
schema script. Also keep full reset for independent sequences, materialized
|
|
312
|
+
views, partitioned/foreign tables, temporary tables, and custom triggers or
|
|
313
|
+
cross-schema dependencies. Data mode does not restore those states or detect
|
|
314
|
+
eligibility; inspect both the tests and seed before opting in. All required
|
|
315
|
+
baseline rows must come from the data-only seed callback, not schema SQL.
|
|
316
|
+
Data mode refuses foreign keys requiring a cross-schema cascade. Database and
|
|
317
|
+
seed errors reject the reset; fix them before running the next test.
|
|
318
|
+
|
|
319
|
+
Do not use `it.concurrent` or `test.concurrent` with a shared test app: user,
|
|
320
|
+
database, and simulator state are shared within the file. Keep file isolation
|
|
321
|
+
enabled and let Vitest parallelize files. This option does not change workers
|
|
322
|
+
or the platform's test-run deadline.
|
|
323
|
+
|
|
298
324
|
- `app.asUser(partial?)` — sign in a test user; `getSession()` in app code
|
|
299
325
|
resolves to it. `app.signOut()` clears.
|
|
300
326
|
- `app.issueSession(partial?)` — mint access/refresh tokens for cookie-based
|
|
@@ -425,6 +451,14 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
425
451
|
display, test print, `getCapabilities()`) — captured in `app.edge`; seed
|
|
426
452
|
devices/peripherals for pairing pickers via `app.edge.seedDevices()` /
|
|
427
453
|
`seedPeripherals()`.
|
|
454
|
+
- `createStarlensClient().extract()` from the starlens SDK — there is no model in
|
|
455
|
+
tests: register what Starlens answers with `app.starlens.mock({ output, issues })`
|
|
456
|
+
(a successful extraction; each issue path comes back `null` in `output`, as in
|
|
457
|
+
production) or `app.starlens.reject("not a receipt")` (the 422 the SDK maps to
|
|
458
|
+
`UnsupportedDocumentError`). An unregistered call fails with a descriptive 502 so a
|
|
459
|
+
test never passes on an invented extraction. `app.starlens.calls` records each
|
|
460
|
+
request's version, input names/media types (never bytes), schema and
|
|
461
|
+
instructions — assert your `.describe()` aliases reached the wire there.
|
|
428
462
|
|
|
429
463
|
### Receipt layout and printer capabilities
|
|
430
464
|
|
package/dist/index.d.mts
CHANGED
|
@@ -462,6 +462,44 @@ interface TestEdge {
|
|
|
462
462
|
clear(): void;
|
|
463
463
|
get count(): number;
|
|
464
464
|
}
|
|
465
|
+
/** One Starlens request the app made, as the simulator saw it (names only, never bytes). */
|
|
466
|
+
interface CapturedStarlensCall {
|
|
467
|
+
version: string;
|
|
468
|
+
inputs: {
|
|
469
|
+
name: string;
|
|
470
|
+
mediaType: string | null;
|
|
471
|
+
source: "url" | "data";
|
|
472
|
+
}[];
|
|
473
|
+
schema: Record<string, unknown>;
|
|
474
|
+
instructions?: string;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* What the simulated Starlens answers next. `{ output, issues }` is returned as a
|
|
478
|
+
* successful extraction with every issue path nulled in `output`, exactly as the control
|
|
479
|
+
* plane does; `{ reject }` becomes the 422 `unsupported_document` refusal the SDK maps to
|
|
480
|
+
* `UnsupportedDocumentError`.
|
|
481
|
+
*/
|
|
482
|
+
type StarlensReply = {
|
|
483
|
+
output: unknown;
|
|
484
|
+
issues?: {
|
|
485
|
+
path: string;
|
|
486
|
+
reason: string;
|
|
487
|
+
}[];
|
|
488
|
+
} | {
|
|
489
|
+
reject: string;
|
|
490
|
+
};
|
|
491
|
+
interface TestStarlens {
|
|
492
|
+
/** Queue the reply for the next `extract()` call. */
|
|
493
|
+
mock(reply: StarlensReply): void;
|
|
494
|
+
/** Queue a refusal for the next `extract()` call. */
|
|
495
|
+
reject(reason: string): void;
|
|
496
|
+
/** Reply used whenever the queue is empty (null = fail with a descriptive 502). */
|
|
497
|
+
mockDefault(reply: StarlensReply | null): void;
|
|
498
|
+
get calls(): CapturedStarlensCall[];
|
|
499
|
+
latestCall(): CapturedStarlensCall | undefined;
|
|
500
|
+
clear(): void;
|
|
501
|
+
get count(): number;
|
|
502
|
+
}
|
|
465
503
|
interface TestAppOptions {
|
|
466
504
|
/** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
|
|
467
505
|
dataStores?: TestDataStoreInput[];
|
|
@@ -506,6 +544,8 @@ interface TestApp {
|
|
|
506
544
|
messages: TestMessages;
|
|
507
545
|
/** Captured edge print/display ops and peripheral bindings from edge-sdk. */
|
|
508
546
|
edge: TestEdge;
|
|
547
|
+
/** Scripted replies and captured calls for the Starlens SDK. */
|
|
548
|
+
starlens: TestStarlens;
|
|
509
549
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
510
550
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
511
551
|
/**
|
|
@@ -520,8 +560,15 @@ interface TestApp {
|
|
|
520
560
|
* control-plane verify endpoint resolves these tokens back to the user.
|
|
521
561
|
*/
|
|
522
562
|
issueSession(user?: Partial<TestUser>): SessionTokens;
|
|
523
|
-
/**
|
|
524
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Rebuild the schema (default), re-run the seed, and clear simulator/user state.
|
|
565
|
+
* `mode: "data"` instead truncates ordinary public tables and restarts owned
|
|
566
|
+
* sequences. Use only with a stable schema and data-only seed; it does not
|
|
567
|
+
* restore DDL changes, schema-script data, or independent sequences.
|
|
568
|
+
*/
|
|
569
|
+
reset(options?: {
|
|
570
|
+
mode: "schema" | "data";
|
|
571
|
+
}): Promise<void>;
|
|
525
572
|
/** Tear down the app and restore global fetch. */
|
|
526
573
|
close(): Promise<void>;
|
|
527
574
|
}
|
|
@@ -564,4 +611,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
564
611
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
565
612
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
566
613
|
|
|
567
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
614
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.d.ts
CHANGED
|
@@ -462,6 +462,44 @@ interface TestEdge {
|
|
|
462
462
|
clear(): void;
|
|
463
463
|
get count(): number;
|
|
464
464
|
}
|
|
465
|
+
/** One Starlens request the app made, as the simulator saw it (names only, never bytes). */
|
|
466
|
+
interface CapturedStarlensCall {
|
|
467
|
+
version: string;
|
|
468
|
+
inputs: {
|
|
469
|
+
name: string;
|
|
470
|
+
mediaType: string | null;
|
|
471
|
+
source: "url" | "data";
|
|
472
|
+
}[];
|
|
473
|
+
schema: Record<string, unknown>;
|
|
474
|
+
instructions?: string;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* What the simulated Starlens answers next. `{ output, issues }` is returned as a
|
|
478
|
+
* successful extraction with every issue path nulled in `output`, exactly as the control
|
|
479
|
+
* plane does; `{ reject }` becomes the 422 `unsupported_document` refusal the SDK maps to
|
|
480
|
+
* `UnsupportedDocumentError`.
|
|
481
|
+
*/
|
|
482
|
+
type StarlensReply = {
|
|
483
|
+
output: unknown;
|
|
484
|
+
issues?: {
|
|
485
|
+
path: string;
|
|
486
|
+
reason: string;
|
|
487
|
+
}[];
|
|
488
|
+
} | {
|
|
489
|
+
reject: string;
|
|
490
|
+
};
|
|
491
|
+
interface TestStarlens {
|
|
492
|
+
/** Queue the reply for the next `extract()` call. */
|
|
493
|
+
mock(reply: StarlensReply): void;
|
|
494
|
+
/** Queue a refusal for the next `extract()` call. */
|
|
495
|
+
reject(reason: string): void;
|
|
496
|
+
/** Reply used whenever the queue is empty (null = fail with a descriptive 502). */
|
|
497
|
+
mockDefault(reply: StarlensReply | null): void;
|
|
498
|
+
get calls(): CapturedStarlensCall[];
|
|
499
|
+
latestCall(): CapturedStarlensCall | undefined;
|
|
500
|
+
clear(): void;
|
|
501
|
+
get count(): number;
|
|
502
|
+
}
|
|
465
503
|
interface TestAppOptions {
|
|
466
504
|
/** Data stores exposed through the production-shaped STARDECK_DATA_STORES manifest. */
|
|
467
505
|
dataStores?: TestDataStoreInput[];
|
|
@@ -506,6 +544,8 @@ interface TestApp {
|
|
|
506
544
|
messages: TestMessages;
|
|
507
545
|
/** Captured edge print/display ops and peripheral bindings from edge-sdk. */
|
|
508
546
|
edge: TestEdge;
|
|
547
|
+
/** Scripted replies and captured calls for the Starlens SDK. */
|
|
548
|
+
starlens: TestStarlens;
|
|
509
549
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
510
550
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
511
551
|
/**
|
|
@@ -520,8 +560,15 @@ interface TestApp {
|
|
|
520
560
|
* control-plane verify endpoint resolves these tokens back to the user.
|
|
521
561
|
*/
|
|
522
562
|
issueSession(user?: Partial<TestUser>): SessionTokens;
|
|
523
|
-
/**
|
|
524
|
-
|
|
563
|
+
/**
|
|
564
|
+
* Rebuild the schema (default), re-run the seed, and clear simulator/user state.
|
|
565
|
+
* `mode: "data"` instead truncates ordinary public tables and restarts owned
|
|
566
|
+
* sequences. Use only with a stable schema and data-only seed; it does not
|
|
567
|
+
* restore DDL changes, schema-script data, or independent sequences.
|
|
568
|
+
*/
|
|
569
|
+
reset(options?: {
|
|
570
|
+
mode: "schema" | "data";
|
|
571
|
+
}): Promise<void>;
|
|
525
572
|
/** Tear down the app and restore global fetch. */
|
|
526
573
|
close(): Promise<void>;
|
|
527
574
|
}
|
|
@@ -564,4 +611,4 @@ declare const TEST_ENV_DEFAULTS: {
|
|
|
564
611
|
/** Default location of the DDL snapshot written by `generate-types`. */
|
|
565
612
|
declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
566
613
|
|
|
567
|
-
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
|
614
|
+
export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CapturedMessage, type CapturedPrint, type CapturedStarlensCall, type CapturedTestPrint, type CapturedUpload, DATA_STORE_TEST_HOST, DATA_STORE_TEST_URL, DEFAULT_SCHEMA_PATH, type DeviceInfo, type PaymentSimulatorFailure, type PaymentSimulatorOperation, type PeripheralInfo, STORAGE_TEST_HOST, STORAGE_TEST_URL, type SessionTokens, type SimBoltCharge, type SimBoltConnection, type SimBoltIntentRecord, type SimBoltIntentStatus, type StarlensReply, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, TestDataStoreInput, type TestDirectory, type TestEdge, type TestInbox, type TestMessages, type TestPayments, type TestStarlens, type TestStorage, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,19 @@ async function applySchema(db, schemaSql) {
|
|
|
73
73
|
async function dropAllTables(db) {
|
|
74
74
|
await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
|
|
75
75
|
}
|
|
76
|
+
async function listPublicTables(db) {
|
|
77
|
+
const { rows } = await db.query(`
|
|
78
|
+
SELECT format('%I.%I', n.nspname, c.relname) AS name
|
|
79
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
80
|
+
WHERE n.nspname = 'public' AND c.relkind = 'r'
|
|
81
|
+
ORDER BY c.relname
|
|
82
|
+
`);
|
|
83
|
+
return rows.map(({ name }) => name);
|
|
84
|
+
}
|
|
85
|
+
async function truncateAllTables(db) {
|
|
86
|
+
const tables = await listPublicTables(db);
|
|
87
|
+
if (tables.length) await db.exec(`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY`);
|
|
88
|
+
}
|
|
76
89
|
var identityParsers = null;
|
|
77
90
|
function getIdentityParsers() {
|
|
78
91
|
if (!identityParsers) {
|
|
@@ -131,6 +144,9 @@ var state = globalSingleton("state", () => ({
|
|
|
131
144
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
132
145
|
edgeCapabilities: null,
|
|
133
146
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
147
|
+
starlensCalls: [],
|
|
148
|
+
starlensReplies: [],
|
|
149
|
+
starlensDefaultReply: null,
|
|
134
150
|
allowNetwork: false
|
|
135
151
|
}));
|
|
136
152
|
function requireDb() {
|
|
@@ -2540,6 +2556,104 @@ function createEdge() {
|
|
|
2540
2556
|
};
|
|
2541
2557
|
}
|
|
2542
2558
|
|
|
2559
|
+
// src/simulator/starlens.ts
|
|
2560
|
+
function starlensFailure(error, status, code) {
|
|
2561
|
+
return json({ success: false, error, code }, status);
|
|
2562
|
+
}
|
|
2563
|
+
function nullFlagged(output, issues) {
|
|
2564
|
+
const copy = JSON.parse(JSON.stringify(output ?? null));
|
|
2565
|
+
for (const { path } of issues) {
|
|
2566
|
+
const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
2567
|
+
const last = keys.pop();
|
|
2568
|
+
const parent = keys.reduce(
|
|
2569
|
+
(o, k) => o != null && typeof o === "object" ? o[k] : void 0,
|
|
2570
|
+
copy
|
|
2571
|
+
);
|
|
2572
|
+
if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
|
|
2573
|
+
parent[last] = null;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return copy;
|
|
2577
|
+
}
|
|
2578
|
+
async function handleStarlensExtract(request, signedDeploymentId) {
|
|
2579
|
+
const body = await readJsonBody(request);
|
|
2580
|
+
if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
|
|
2581
|
+
return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
|
|
2582
|
+
}
|
|
2583
|
+
const inputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
2584
|
+
if (inputs.length === 0) {
|
|
2585
|
+
return starlensFailure(
|
|
2586
|
+
"Validation failed \u2014 inputs: at least one input",
|
|
2587
|
+
400,
|
|
2588
|
+
"VALIDATION_ERROR"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
const schema = body.schema;
|
|
2592
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
2593
|
+
return starlensFailure(
|
|
2594
|
+
"Validation failed \u2014 schema.properties is required",
|
|
2595
|
+
400,
|
|
2596
|
+
"VALIDATION_ERROR"
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
const call = {
|
|
2600
|
+
version: typeof body.version === "string" ? body.version : "latest",
|
|
2601
|
+
inputs: inputs.map((i) => ({
|
|
2602
|
+
name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
|
|
2603
|
+
mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
|
|
2604
|
+
source: typeof i.url === "string" ? "url" : "data"
|
|
2605
|
+
})),
|
|
2606
|
+
schema,
|
|
2607
|
+
instructions: typeof body.instructions === "string" ? body.instructions : void 0
|
|
2608
|
+
};
|
|
2609
|
+
state.starlensCalls.push(call);
|
|
2610
|
+
const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
|
|
2611
|
+
if (!reply) {
|
|
2612
|
+
return starlensFailure(
|
|
2613
|
+
"[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
|
|
2614
|
+
502,
|
|
2615
|
+
"model_failed"
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
if ("reject" in reply) {
|
|
2619
|
+
return starlensFailure(reply.reject, 422, "unsupported_document");
|
|
2620
|
+
}
|
|
2621
|
+
const issues = reply.issues ?? [];
|
|
2622
|
+
return success({
|
|
2623
|
+
version: call.version === "latest" ? "v1" : call.version,
|
|
2624
|
+
output: nullFlagged(reply.output, issues),
|
|
2625
|
+
issues,
|
|
2626
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
function createStarlens() {
|
|
2630
|
+
return {
|
|
2631
|
+
mock(reply) {
|
|
2632
|
+
state.starlensReplies.push(reply);
|
|
2633
|
+
},
|
|
2634
|
+
reject(reason) {
|
|
2635
|
+
state.starlensReplies.push({ reject: reason });
|
|
2636
|
+
},
|
|
2637
|
+
mockDefault(reply) {
|
|
2638
|
+
state.starlensDefaultReply = reply;
|
|
2639
|
+
},
|
|
2640
|
+
get calls() {
|
|
2641
|
+
return state.starlensCalls;
|
|
2642
|
+
},
|
|
2643
|
+
latestCall() {
|
|
2644
|
+
return state.starlensCalls[state.starlensCalls.length - 1];
|
|
2645
|
+
},
|
|
2646
|
+
clear() {
|
|
2647
|
+
state.starlensCalls = [];
|
|
2648
|
+
state.starlensReplies = [];
|
|
2649
|
+
state.starlensDefaultReply = null;
|
|
2650
|
+
},
|
|
2651
|
+
get count() {
|
|
2652
|
+
return state.starlensCalls.length;
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2543
2657
|
// src/simulator/router.ts
|
|
2544
2658
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
2545
2659
|
originalFetch: null
|
|
@@ -2561,7 +2675,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2561
2675
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
2562
2676
|
);
|
|
2563
2677
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2564
|
-
|
|
2678
|
+
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2679
|
+
return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2565
2680
|
}
|
|
2566
2681
|
function deploymentIdFromPath(url) {
|
|
2567
2682
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2591,6 +2706,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2591
2706
|
);
|
|
2592
2707
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2593
2708
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2709
|
+
let signedDeploymentId = null;
|
|
2594
2710
|
if (requiresDeploymentHmac(request, url)) {
|
|
2595
2711
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2596
2712
|
if (!authHeader) {
|
|
@@ -2602,6 +2718,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2602
2718
|
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
2603
2719
|
return failure("Invalid authentication", 401);
|
|
2604
2720
|
}
|
|
2721
|
+
signedDeploymentId = authPayload.deploymentId;
|
|
2605
2722
|
}
|
|
2606
2723
|
if (isStorageHost) {
|
|
2607
2724
|
return handleStorageRequest(request, url);
|
|
@@ -2622,6 +2739,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2622
2739
|
if (edgeMatch) {
|
|
2623
2740
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2624
2741
|
}
|
|
2742
|
+
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2743
|
+
return handleStarlensExtract(request, signedDeploymentId);
|
|
2744
|
+
}
|
|
2625
2745
|
if (dataStoreMatch) {
|
|
2626
2746
|
const subPath = dataStoreMatch[1] ?? "";
|
|
2627
2747
|
const db = requireDb();
|
|
@@ -2643,7 +2763,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2643
2763
|
}
|
|
2644
2764
|
}
|
|
2645
2765
|
return failure(
|
|
2646
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
2766
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
2647
2767
|
404
|
|
2648
2768
|
);
|
|
2649
2769
|
}
|
|
@@ -2811,6 +2931,7 @@ async function createTestApp(options = {}) {
|
|
|
2811
2931
|
const storage = createStorage();
|
|
2812
2932
|
const messages = createMessages();
|
|
2813
2933
|
const edge = createEdge();
|
|
2934
|
+
const starlens = createStarlens();
|
|
2814
2935
|
const app = {
|
|
2815
2936
|
db,
|
|
2816
2937
|
inbox,
|
|
@@ -2819,6 +2940,7 @@ async function createTestApp(options = {}) {
|
|
|
2819
2940
|
storage,
|
|
2820
2941
|
messages,
|
|
2821
2942
|
edge,
|
|
2943
|
+
starlens,
|
|
2822
2944
|
async query(sql, params = []) {
|
|
2823
2945
|
const result = await db.query(sql, params);
|
|
2824
2946
|
return result.rows;
|
|
@@ -2841,9 +2963,14 @@ async function createTestApp(options = {}) {
|
|
|
2841
2963
|
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
2842
2964
|
return tokens;
|
|
2843
2965
|
},
|
|
2844
|
-
async reset() {
|
|
2845
|
-
|
|
2846
|
-
|
|
2966
|
+
async reset(resetOptions) {
|
|
2967
|
+
if (resetOptions?.mode === "data") {
|
|
2968
|
+
await truncateAllTables(db);
|
|
2969
|
+
if (options.seed) await options.seed(db);
|
|
2970
|
+
} else {
|
|
2971
|
+
await dropAllTables(db);
|
|
2972
|
+
await initialize();
|
|
2973
|
+
}
|
|
2847
2974
|
state.currentUser = null;
|
|
2848
2975
|
state.sessions.clear();
|
|
2849
2976
|
state.refreshSessions.clear();
|
|
@@ -2855,6 +2982,7 @@ async function createTestApp(options = {}) {
|
|
|
2855
2982
|
storage.clear();
|
|
2856
2983
|
messages.clear();
|
|
2857
2984
|
edge.clear();
|
|
2985
|
+
starlens.clear();
|
|
2858
2986
|
},
|
|
2859
2987
|
async close() {
|
|
2860
2988
|
state.db = null;
|
|
@@ -2869,6 +2997,7 @@ async function createTestApp(options = {}) {
|
|
|
2869
2997
|
storage.clear();
|
|
2870
2998
|
messages.clear();
|
|
2871
2999
|
edge.clear();
|
|
3000
|
+
starlens.clear();
|
|
2872
3001
|
uninstallFetchRouter();
|
|
2873
3002
|
await db.close();
|
|
2874
3003
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -24,6 +24,19 @@ async function applySchema(db, schemaSql) {
|
|
|
24
24
|
async function dropAllTables(db) {
|
|
25
25
|
await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
|
|
26
26
|
}
|
|
27
|
+
async function listPublicTables(db) {
|
|
28
|
+
const { rows } = await db.query(`
|
|
29
|
+
SELECT format('%I.%I', n.nspname, c.relname) AS name
|
|
30
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
31
|
+
WHERE n.nspname = 'public' AND c.relkind = 'r'
|
|
32
|
+
ORDER BY c.relname
|
|
33
|
+
`);
|
|
34
|
+
return rows.map(({ name }) => name);
|
|
35
|
+
}
|
|
36
|
+
async function truncateAllTables(db) {
|
|
37
|
+
const tables = await listPublicTables(db);
|
|
38
|
+
if (tables.length) await db.exec(`TRUNCATE TABLE ${tables.join(", ")} RESTART IDENTITY`);
|
|
39
|
+
}
|
|
27
40
|
var identityParsers = null;
|
|
28
41
|
function getIdentityParsers() {
|
|
29
42
|
if (!identityParsers) {
|
|
@@ -82,6 +95,9 @@ var state = globalSingleton("state", () => ({
|
|
|
82
95
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
83
96
|
edgeCapabilities: null,
|
|
84
97
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
98
|
+
starlensCalls: [],
|
|
99
|
+
starlensReplies: [],
|
|
100
|
+
starlensDefaultReply: null,
|
|
85
101
|
allowNetwork: false
|
|
86
102
|
}));
|
|
87
103
|
function requireDb() {
|
|
@@ -2491,6 +2507,104 @@ function createEdge() {
|
|
|
2491
2507
|
};
|
|
2492
2508
|
}
|
|
2493
2509
|
|
|
2510
|
+
// src/simulator/starlens.ts
|
|
2511
|
+
function starlensFailure(error, status, code) {
|
|
2512
|
+
return json({ success: false, error, code }, status);
|
|
2513
|
+
}
|
|
2514
|
+
function nullFlagged(output, issues) {
|
|
2515
|
+
const copy = JSON.parse(JSON.stringify(output ?? null));
|
|
2516
|
+
for (const { path } of issues) {
|
|
2517
|
+
const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
2518
|
+
const last = keys.pop();
|
|
2519
|
+
const parent = keys.reduce(
|
|
2520
|
+
(o, k) => o != null && typeof o === "object" ? o[k] : void 0,
|
|
2521
|
+
copy
|
|
2522
|
+
);
|
|
2523
|
+
if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
|
|
2524
|
+
parent[last] = null;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return copy;
|
|
2528
|
+
}
|
|
2529
|
+
async function handleStarlensExtract(request, signedDeploymentId) {
|
|
2530
|
+
const body = await readJsonBody(request);
|
|
2531
|
+
if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
|
|
2532
|
+
return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
|
|
2533
|
+
}
|
|
2534
|
+
const inputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
2535
|
+
if (inputs.length === 0) {
|
|
2536
|
+
return starlensFailure(
|
|
2537
|
+
"Validation failed \u2014 inputs: at least one input",
|
|
2538
|
+
400,
|
|
2539
|
+
"VALIDATION_ERROR"
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
const schema = body.schema;
|
|
2543
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
2544
|
+
return starlensFailure(
|
|
2545
|
+
"Validation failed \u2014 schema.properties is required",
|
|
2546
|
+
400,
|
|
2547
|
+
"VALIDATION_ERROR"
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
const call = {
|
|
2551
|
+
version: typeof body.version === "string" ? body.version : "latest",
|
|
2552
|
+
inputs: inputs.map((i) => ({
|
|
2553
|
+
name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
|
|
2554
|
+
mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
|
|
2555
|
+
source: typeof i.url === "string" ? "url" : "data"
|
|
2556
|
+
})),
|
|
2557
|
+
schema,
|
|
2558
|
+
instructions: typeof body.instructions === "string" ? body.instructions : void 0
|
|
2559
|
+
};
|
|
2560
|
+
state.starlensCalls.push(call);
|
|
2561
|
+
const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
|
|
2562
|
+
if (!reply) {
|
|
2563
|
+
return starlensFailure(
|
|
2564
|
+
"[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
|
|
2565
|
+
502,
|
|
2566
|
+
"model_failed"
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
if ("reject" in reply) {
|
|
2570
|
+
return starlensFailure(reply.reject, 422, "unsupported_document");
|
|
2571
|
+
}
|
|
2572
|
+
const issues = reply.issues ?? [];
|
|
2573
|
+
return success({
|
|
2574
|
+
version: call.version === "latest" ? "v1" : call.version,
|
|
2575
|
+
output: nullFlagged(reply.output, issues),
|
|
2576
|
+
issues,
|
|
2577
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
function createStarlens() {
|
|
2581
|
+
return {
|
|
2582
|
+
mock(reply) {
|
|
2583
|
+
state.starlensReplies.push(reply);
|
|
2584
|
+
},
|
|
2585
|
+
reject(reason) {
|
|
2586
|
+
state.starlensReplies.push({ reject: reason });
|
|
2587
|
+
},
|
|
2588
|
+
mockDefault(reply) {
|
|
2589
|
+
state.starlensDefaultReply = reply;
|
|
2590
|
+
},
|
|
2591
|
+
get calls() {
|
|
2592
|
+
return state.starlensCalls;
|
|
2593
|
+
},
|
|
2594
|
+
latestCall() {
|
|
2595
|
+
return state.starlensCalls[state.starlensCalls.length - 1];
|
|
2596
|
+
},
|
|
2597
|
+
clear() {
|
|
2598
|
+
state.starlensCalls = [];
|
|
2599
|
+
state.starlensReplies = [];
|
|
2600
|
+
state.starlensDefaultReply = null;
|
|
2601
|
+
},
|
|
2602
|
+
get count() {
|
|
2603
|
+
return state.starlensCalls.length;
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2494
2608
|
// src/simulator/router.ts
|
|
2495
2609
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
2496
2610
|
originalFetch: null
|
|
@@ -2512,7 +2626,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2512
2626
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
2513
2627
|
);
|
|
2514
2628
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2515
|
-
|
|
2629
|
+
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2630
|
+
return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2516
2631
|
}
|
|
2517
2632
|
function deploymentIdFromPath(url) {
|
|
2518
2633
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2542,6 +2657,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2542
2657
|
);
|
|
2543
2658
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2544
2659
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2660
|
+
let signedDeploymentId = null;
|
|
2545
2661
|
if (requiresDeploymentHmac(request, url)) {
|
|
2546
2662
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2547
2663
|
if (!authHeader) {
|
|
@@ -2553,6 +2669,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2553
2669
|
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
2554
2670
|
return failure("Invalid authentication", 401);
|
|
2555
2671
|
}
|
|
2672
|
+
signedDeploymentId = authPayload.deploymentId;
|
|
2556
2673
|
}
|
|
2557
2674
|
if (isStorageHost) {
|
|
2558
2675
|
return handleStorageRequest(request, url);
|
|
@@ -2573,6 +2690,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2573
2690
|
if (edgeMatch) {
|
|
2574
2691
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2575
2692
|
}
|
|
2693
|
+
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2694
|
+
return handleStarlensExtract(request, signedDeploymentId);
|
|
2695
|
+
}
|
|
2576
2696
|
if (dataStoreMatch) {
|
|
2577
2697
|
const subPath = dataStoreMatch[1] ?? "";
|
|
2578
2698
|
const db = requireDb();
|
|
@@ -2594,7 +2714,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2594
2714
|
}
|
|
2595
2715
|
}
|
|
2596
2716
|
return failure(
|
|
2597
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
2717
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
2598
2718
|
404
|
|
2599
2719
|
);
|
|
2600
2720
|
}
|
|
@@ -2762,6 +2882,7 @@ async function createTestApp(options = {}) {
|
|
|
2762
2882
|
const storage = createStorage();
|
|
2763
2883
|
const messages = createMessages();
|
|
2764
2884
|
const edge = createEdge();
|
|
2885
|
+
const starlens = createStarlens();
|
|
2765
2886
|
const app = {
|
|
2766
2887
|
db,
|
|
2767
2888
|
inbox,
|
|
@@ -2770,6 +2891,7 @@ async function createTestApp(options = {}) {
|
|
|
2770
2891
|
storage,
|
|
2771
2892
|
messages,
|
|
2772
2893
|
edge,
|
|
2894
|
+
starlens,
|
|
2773
2895
|
async query(sql, params = []) {
|
|
2774
2896
|
const result = await db.query(sql, params);
|
|
2775
2897
|
return result.rows;
|
|
@@ -2792,9 +2914,14 @@ async function createTestApp(options = {}) {
|
|
|
2792
2914
|
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
2793
2915
|
return tokens;
|
|
2794
2916
|
},
|
|
2795
|
-
async reset() {
|
|
2796
|
-
|
|
2797
|
-
|
|
2917
|
+
async reset(resetOptions) {
|
|
2918
|
+
if (resetOptions?.mode === "data") {
|
|
2919
|
+
await truncateAllTables(db);
|
|
2920
|
+
if (options.seed) await options.seed(db);
|
|
2921
|
+
} else {
|
|
2922
|
+
await dropAllTables(db);
|
|
2923
|
+
await initialize();
|
|
2924
|
+
}
|
|
2798
2925
|
state.currentUser = null;
|
|
2799
2926
|
state.sessions.clear();
|
|
2800
2927
|
state.refreshSessions.clear();
|
|
@@ -2806,6 +2933,7 @@ async function createTestApp(options = {}) {
|
|
|
2806
2933
|
storage.clear();
|
|
2807
2934
|
messages.clear();
|
|
2808
2935
|
edge.clear();
|
|
2936
|
+
starlens.clear();
|
|
2809
2937
|
},
|
|
2810
2938
|
async close() {
|
|
2811
2939
|
state.db = null;
|
|
@@ -2820,6 +2948,7 @@ async function createTestApp(options = {}) {
|
|
|
2820
2948
|
storage.clear();
|
|
2821
2949
|
messages.clear();
|
|
2822
2950
|
edge.clear();
|
|
2951
|
+
starlens.clear();
|
|
2823
2952
|
uninstallFetchRouter();
|
|
2824
2953
|
await db.close();
|
|
2825
2954
|
}
|
|
@@ -74,6 +74,9 @@ var state = globalSingleton("state", () => ({
|
|
|
74
74
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
75
75
|
edgeCapabilities: null,
|
|
76
76
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
77
|
+
starlensCalls: [],
|
|
78
|
+
starlensReplies: [],
|
|
79
|
+
starlensDefaultReply: null,
|
|
77
80
|
allowNetwork: false
|
|
78
81
|
}));
|
|
79
82
|
|
|
@@ -47,6 +47,9 @@ var state = globalSingleton("state", () => ({
|
|
|
47
47
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
48
48
|
edgeCapabilities: null,
|
|
49
49
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
50
|
+
starlensCalls: [],
|
|
51
|
+
starlensReplies: [],
|
|
52
|
+
starlensDefaultReply: null,
|
|
50
53
|
allowNetwork: false
|
|
51
54
|
}));
|
|
52
55
|
|
package/dist/setup.js
CHANGED
|
@@ -68,6 +68,9 @@ var state = globalSingleton("state", () => ({
|
|
|
68
68
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
69
69
|
edgeCapabilities: null,
|
|
70
70
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
71
|
+
starlensCalls: [],
|
|
72
|
+
starlensReplies: [],
|
|
73
|
+
starlensDefaultReply: null,
|
|
71
74
|
allowNetwork: false
|
|
72
75
|
}));
|
|
73
76
|
function requireDb() {
|
|
@@ -2000,6 +2003,77 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
2000
2003
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
2001
2004
|
}
|
|
2002
2005
|
|
|
2006
|
+
// src/simulator/starlens.ts
|
|
2007
|
+
function starlensFailure(error, status, code) {
|
|
2008
|
+
return json({ success: false, error, code }, status);
|
|
2009
|
+
}
|
|
2010
|
+
function nullFlagged(output, issues) {
|
|
2011
|
+
const copy = JSON.parse(JSON.stringify(output ?? null));
|
|
2012
|
+
for (const { path } of issues) {
|
|
2013
|
+
const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
2014
|
+
const last = keys.pop();
|
|
2015
|
+
const parent = keys.reduce(
|
|
2016
|
+
(o, k) => o != null && typeof o === "object" ? o[k] : void 0,
|
|
2017
|
+
copy
|
|
2018
|
+
);
|
|
2019
|
+
if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
|
|
2020
|
+
parent[last] = null;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return copy;
|
|
2024
|
+
}
|
|
2025
|
+
async function handleStarlensExtract(request, signedDeploymentId) {
|
|
2026
|
+
const body = await readJsonBody(request);
|
|
2027
|
+
if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
|
|
2028
|
+
return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
|
|
2029
|
+
}
|
|
2030
|
+
const inputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
2031
|
+
if (inputs.length === 0) {
|
|
2032
|
+
return starlensFailure(
|
|
2033
|
+
"Validation failed \u2014 inputs: at least one input",
|
|
2034
|
+
400,
|
|
2035
|
+
"VALIDATION_ERROR"
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
const schema = body.schema;
|
|
2039
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
2040
|
+
return starlensFailure(
|
|
2041
|
+
"Validation failed \u2014 schema.properties is required",
|
|
2042
|
+
400,
|
|
2043
|
+
"VALIDATION_ERROR"
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const call = {
|
|
2047
|
+
version: typeof body.version === "string" ? body.version : "latest",
|
|
2048
|
+
inputs: inputs.map((i) => ({
|
|
2049
|
+
name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
|
|
2050
|
+
mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
|
|
2051
|
+
source: typeof i.url === "string" ? "url" : "data"
|
|
2052
|
+
})),
|
|
2053
|
+
schema,
|
|
2054
|
+
instructions: typeof body.instructions === "string" ? body.instructions : void 0
|
|
2055
|
+
};
|
|
2056
|
+
state.starlensCalls.push(call);
|
|
2057
|
+
const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
|
|
2058
|
+
if (!reply) {
|
|
2059
|
+
return starlensFailure(
|
|
2060
|
+
"[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
|
|
2061
|
+
502,
|
|
2062
|
+
"model_failed"
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
if ("reject" in reply) {
|
|
2066
|
+
return starlensFailure(reply.reject, 422, "unsupported_document");
|
|
2067
|
+
}
|
|
2068
|
+
const issues = reply.issues ?? [];
|
|
2069
|
+
return success({
|
|
2070
|
+
version: call.version === "latest" ? "v1" : call.version,
|
|
2071
|
+
output: nullFlagged(reply.output, issues),
|
|
2072
|
+
issues,
|
|
2073
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2003
2077
|
// src/simulator/router.ts
|
|
2004
2078
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
2005
2079
|
originalFetch: null
|
|
@@ -2021,7 +2095,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
2021
2095
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
2022
2096
|
);
|
|
2023
2097
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2024
|
-
|
|
2098
|
+
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2099
|
+
return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2025
2100
|
}
|
|
2026
2101
|
function deploymentIdFromPath(url) {
|
|
2027
2102
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2051,6 +2126,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2051
2126
|
);
|
|
2052
2127
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2053
2128
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2129
|
+
let signedDeploymentId = null;
|
|
2054
2130
|
if (requiresDeploymentHmac(request, url)) {
|
|
2055
2131
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2056
2132
|
if (!authHeader) {
|
|
@@ -2062,6 +2138,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2062
2138
|
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
2063
2139
|
return failure("Invalid authentication", 401);
|
|
2064
2140
|
}
|
|
2141
|
+
signedDeploymentId = authPayload.deploymentId;
|
|
2065
2142
|
}
|
|
2066
2143
|
if (isStorageHost) {
|
|
2067
2144
|
return handleStorageRequest(request, url);
|
|
@@ -2082,6 +2159,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2082
2159
|
if (edgeMatch) {
|
|
2083
2160
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2084
2161
|
}
|
|
2162
|
+
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2163
|
+
return handleStarlensExtract(request, signedDeploymentId);
|
|
2164
|
+
}
|
|
2085
2165
|
if (dataStoreMatch) {
|
|
2086
2166
|
const subPath = dataStoreMatch[1] ?? "";
|
|
2087
2167
|
const db = requireDb();
|
|
@@ -2103,7 +2183,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2103
2183
|
}
|
|
2104
2184
|
}
|
|
2105
2185
|
return failure(
|
|
2106
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
2186
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
2107
2187
|
404
|
|
2108
2188
|
);
|
|
2109
2189
|
}
|
package/dist/setup.mjs
CHANGED
|
@@ -44,6 +44,9 @@ var state = globalSingleton("state", () => ({
|
|
|
44
44
|
edgeBindings: /* @__PURE__ */ new Map(),
|
|
45
45
|
edgeCapabilities: null,
|
|
46
46
|
edgePeripheralCapabilities: /* @__PURE__ */ new Map(),
|
|
47
|
+
starlensCalls: [],
|
|
48
|
+
starlensReplies: [],
|
|
49
|
+
starlensDefaultReply: null,
|
|
47
50
|
allowNetwork: false
|
|
48
51
|
}));
|
|
49
52
|
function requireDb() {
|
|
@@ -1976,6 +1979,77 @@ async function handleEdgeRequest(request, subPath) {
|
|
|
1976
1979
|
return edgeFailure(`No edge simulator for ${method} .../edge${subPath}`, 404);
|
|
1977
1980
|
}
|
|
1978
1981
|
|
|
1982
|
+
// src/simulator/starlens.ts
|
|
1983
|
+
function starlensFailure(error, status, code) {
|
|
1984
|
+
return json({ success: false, error, code }, status);
|
|
1985
|
+
}
|
|
1986
|
+
function nullFlagged(output, issues) {
|
|
1987
|
+
const copy = JSON.parse(JSON.stringify(output ?? null));
|
|
1988
|
+
for (const { path } of issues) {
|
|
1989
|
+
const keys = path.replace(/^output\./, "").replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
1990
|
+
const last = keys.pop();
|
|
1991
|
+
const parent = keys.reduce(
|
|
1992
|
+
(o, k) => o != null && typeof o === "object" ? o[k] : void 0,
|
|
1993
|
+
copy
|
|
1994
|
+
);
|
|
1995
|
+
if (parent != null && typeof parent === "object" && Object.prototype.hasOwnProperty.call(parent, last)) {
|
|
1996
|
+
parent[last] = null;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
return copy;
|
|
2000
|
+
}
|
|
2001
|
+
async function handleStarlensExtract(request, signedDeploymentId) {
|
|
2002
|
+
const body = await readJsonBody(request);
|
|
2003
|
+
if (signedDeploymentId !== null && body.deploymentId !== signedDeploymentId) {
|
|
2004
|
+
return starlensFailure("Deployment ID mismatch", 401, "AUTHENTICATION_ERROR");
|
|
2005
|
+
}
|
|
2006
|
+
const inputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
2007
|
+
if (inputs.length === 0) {
|
|
2008
|
+
return starlensFailure(
|
|
2009
|
+
"Validation failed \u2014 inputs: at least one input",
|
|
2010
|
+
400,
|
|
2011
|
+
"VALIDATION_ERROR"
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
const schema = body.schema;
|
|
2015
|
+
if (!schema || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
2016
|
+
return starlensFailure(
|
|
2017
|
+
"Validation failed \u2014 schema.properties is required",
|
|
2018
|
+
400,
|
|
2019
|
+
"VALIDATION_ERROR"
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
const call = {
|
|
2023
|
+
version: typeof body.version === "string" ? body.version : "latest",
|
|
2024
|
+
inputs: inputs.map((i) => ({
|
|
2025
|
+
name: typeof i.filename === "string" ? i.filename : typeof i.url === "string" ? i.url : "upload",
|
|
2026
|
+
mediaType: typeof i.mediaType === "string" ? i.mediaType : null,
|
|
2027
|
+
source: typeof i.url === "string" ? "url" : "data"
|
|
2028
|
+
})),
|
|
2029
|
+
schema,
|
|
2030
|
+
instructions: typeof body.instructions === "string" ? body.instructions : void 0
|
|
2031
|
+
};
|
|
2032
|
+
state.starlensCalls.push(call);
|
|
2033
|
+
const reply = state.starlensReplies.shift() ?? state.starlensDefaultReply;
|
|
2034
|
+
if (!reply) {
|
|
2035
|
+
return starlensFailure(
|
|
2036
|
+
"[stardeck-testing] No Starlens reply registered. Call app.starlens.mock({ output, issues }) or app.starlens.reject(reason) before the request.",
|
|
2037
|
+
502,
|
|
2038
|
+
"model_failed"
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
if ("reject" in reply) {
|
|
2042
|
+
return starlensFailure(reply.reject, 422, "unsupported_document");
|
|
2043
|
+
}
|
|
2044
|
+
const issues = reply.issues ?? [];
|
|
2045
|
+
return success({
|
|
2046
|
+
version: call.version === "latest" ? "v1" : call.version,
|
|
2047
|
+
output: nullFlagged(reply.output, issues),
|
|
2048
|
+
issues,
|
|
2049
|
+
usage: { inputTokens: 0, outputTokens: 0, cost: 0 }
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1979
2053
|
// src/simulator/router.ts
|
|
1980
2054
|
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
1981
2055
|
originalFetch: null
|
|
@@ -1997,7 +2071,8 @@ function requiresDeploymentHmac(request, url) {
|
|
|
1997
2071
|
/^\/api\/deployments\/[^/]+\/integrations\/(slack|line|facebook)(\/.*)?$/
|
|
1998
2072
|
);
|
|
1999
2073
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2000
|
-
|
|
2074
|
+
const isStarlens = url.pathname === "/api/starlens/extract";
|
|
2075
|
+
return !!(isStarlens || dataStoreMatch || isEmail || identitiesMatch || storeMatch || messagingMatch || edgeMatch);
|
|
2001
2076
|
}
|
|
2002
2077
|
function deploymentIdFromPath(url) {
|
|
2003
2078
|
const match = url.pathname.match(/\/api\/deployments\/([^/]+)(?:\/|$)/);
|
|
@@ -2027,6 +2102,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2027
2102
|
);
|
|
2028
2103
|
const edgeMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/edge(\/.*)?$/);
|
|
2029
2104
|
const isStorageHost = url.hostname === STORAGE_TEST_HOST;
|
|
2105
|
+
let signedDeploymentId = null;
|
|
2030
2106
|
if (requiresDeploymentHmac(request, url)) {
|
|
2031
2107
|
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
2032
2108
|
if (!authHeader) {
|
|
@@ -2038,6 +2114,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2038
2114
|
if (!authPayload || pathDeploymentId !== null && authPayload.deploymentId !== pathDeploymentId) {
|
|
2039
2115
|
return failure("Invalid authentication", 401);
|
|
2040
2116
|
}
|
|
2117
|
+
signedDeploymentId = authPayload.deploymentId;
|
|
2041
2118
|
}
|
|
2042
2119
|
if (isStorageHost) {
|
|
2043
2120
|
return handleStorageRequest(request, url);
|
|
@@ -2058,6 +2135,9 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2058
2135
|
if (edgeMatch) {
|
|
2059
2136
|
return handleEdgeRequest(request, edgeMatch[1] ?? "");
|
|
2060
2137
|
}
|
|
2138
|
+
if (url.pathname === "/api/starlens/extract" && request.method === "POST") {
|
|
2139
|
+
return handleStarlensExtract(request, signedDeploymentId);
|
|
2140
|
+
}
|
|
2061
2141
|
if (dataStoreMatch) {
|
|
2062
2142
|
const subPath = dataStoreMatch[1] ?? "";
|
|
2063
2143
|
const db = requireDb();
|
|
@@ -2079,7 +2159,7 @@ async function handleSimulatedRequest(request, url) {
|
|
|
2079
2159
|
}
|
|
2080
2160
|
}
|
|
2081
2161
|
return failure(
|
|
2082
|
-
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, auth verify/refresh, Neon /sql.`,
|
|
2162
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, identities CRUD, payments store/checkout, storage upload/files, messaging send, edge print/display/bindings, starlens extract, auth verify/refresh, Neon /sql.`,
|
|
2083
2163
|
404
|
|
2084
2164
|
);
|
|
2085
2165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"@stardeck-customer-apps/payments-sdk": ">=0.1.0",
|
|
71
71
|
"@stardeck-customer-apps/storage-sdk": ">=0.1.0",
|
|
72
72
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
73
|
-
"vitest": ">=2.0.0"
|
|
73
|
+
"vitest": ">=2.0.0",
|
|
74
|
+
"@stardeck-customer-apps/starlens": ">=0.1.0"
|
|
74
75
|
},
|
|
75
76
|
"peerDependenciesMeta": {
|
|
76
77
|
"@stardeck-customer-apps/integrations-sdk": {
|
|
@@ -87,6 +88,9 @@
|
|
|
87
88
|
},
|
|
88
89
|
"next": {
|
|
89
90
|
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@stardeck-customer-apps/starlens": {
|
|
93
|
+
"optional": true
|
|
90
94
|
}
|
|
91
95
|
},
|
|
92
96
|
"devDependencies": {
|
|
@@ -106,6 +110,8 @@
|
|
|
106
110
|
"tsup": "^8.0.0",
|
|
107
111
|
"typescript": "^5.0.0",
|
|
108
112
|
"typescript-eslint": "^8.0.0",
|
|
109
|
-
"vitest": "^3.2.4"
|
|
113
|
+
"vitest": "^3.2.4",
|
|
114
|
+
"@stardeck-customer-apps/starlens": "*",
|
|
115
|
+
"zod": "^4.3.5"
|
|
110
116
|
}
|
|
111
117
|
}
|