@stardeck-customer-apps/testing 0.24.0 → 0.25.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 CHANGED
@@ -565,7 +565,10 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
565
565
  in `app.storage` against the simulated `STORAGE_URL` host.
566
566
  - `client.slack` / `client.line` / `client.facebook` send endpoints — captured
567
567
  in `app.messages`.
568
- - `createEdgeClient()` (print, pair/unpair, list devices/peripherals, show/clear
568
+ - `createEdgeClient()` (print, `enqueueReceipt()`/`getPrintJob()` durable receipts: idempotent per key,
569
+ conflicts and refused options mirrored, `app.edge.durablePrints`, and
570
+ `app.edge.settleNextDurableJob({ status: "needs_attention", reason, outcome })` to drive the
571
+ uncertain/not-printed UI; pair/unpair, list devices/peripherals, show/clear
569
572
  display, test print, `getCapabilities()`) — captured in `app.edge`; seed
570
573
  devices/peripherals for pairing pickers via `app.edge.seedDevices()` /
571
574
  `seedPeripherals()`.
package/dist/index.d.mts CHANGED
@@ -542,6 +542,27 @@ interface CapturedPrint {
542
542
  logo?: boolean;
543
543
  copies: number;
544
544
  }
545
+ /** One `edge.enqueueReceipt()` the app made, as the simulator accepted it. */
546
+ interface CapturedDurablePrint {
547
+ jobId: string;
548
+ idempotencyKey: string;
549
+ alias?: string;
550
+ deviceId?: string;
551
+ peripheralId?: string;
552
+ receipt: ReceiptPayload;
553
+ logo?: boolean;
554
+ }
555
+ /** How a simulated durable job settles. Mirrors the platform's status contract. */
556
+ type SimulatedDurableOutcome = {
557
+ status: "sent_to_printer";
558
+ } | {
559
+ status: "queued";
560
+ } | {
561
+ status: "needs_attention";
562
+ reason: string;
563
+ outcome: "not_printed" | "uncertain";
564
+ message?: string;
565
+ };
545
566
  interface CapturedDisplay {
546
567
  action: "show" | "clear";
547
568
  alias?: string;
@@ -557,6 +578,15 @@ interface TestEdge {
557
578
  get prints(): CapturedPrint[];
558
579
  get displays(): CapturedDisplay[];
559
580
  get testPrints(): CapturedTestPrint[];
581
+ /** Durable receipts accepted by `edge.enqueueReceipt()` (one per distinct idempotency key). */
582
+ get durablePrints(): CapturedDurablePrint[];
583
+ /**
584
+ * Decide how the NEXT accepted durable job settles when the app reads it with
585
+ * `edge.getPrintJob()`. Without this every job reads `sent_to_printer`. Queue
586
+ * several to model a run of jobs; use `needs_attention` with `uncertain` to
587
+ * test the "check the printer before reprinting" path.
588
+ */
589
+ settleNextDurableJob(outcome: SimulatedDurableOutcome): void;
560
590
  latestPrint(): CapturedPrint | undefined;
561
591
  latestDisplay(): CapturedDisplay | undefined;
562
592
  get bindings(): BindingInfo[];
@@ -742,4 +772,4 @@ declare const TEST_ENV_DEFAULTS: {
742
772
  /** Default location of the DDL snapshot written by `generate-types`. */
743
773
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
744
774
 
745
- 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 };
775
+ export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedDurablePrint, 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 SimulatedDurableOutcome, 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
@@ -542,6 +542,27 @@ interface CapturedPrint {
542
542
  logo?: boolean;
543
543
  copies: number;
544
544
  }
545
+ /** One `edge.enqueueReceipt()` the app made, as the simulator accepted it. */
546
+ interface CapturedDurablePrint {
547
+ jobId: string;
548
+ idempotencyKey: string;
549
+ alias?: string;
550
+ deviceId?: string;
551
+ peripheralId?: string;
552
+ receipt: ReceiptPayload;
553
+ logo?: boolean;
554
+ }
555
+ /** How a simulated durable job settles. Mirrors the platform's status contract. */
556
+ type SimulatedDurableOutcome = {
557
+ status: "sent_to_printer";
558
+ } | {
559
+ status: "queued";
560
+ } | {
561
+ status: "needs_attention";
562
+ reason: string;
563
+ outcome: "not_printed" | "uncertain";
564
+ message?: string;
565
+ };
545
566
  interface CapturedDisplay {
546
567
  action: "show" | "clear";
547
568
  alias?: string;
@@ -557,6 +578,15 @@ interface TestEdge {
557
578
  get prints(): CapturedPrint[];
558
579
  get displays(): CapturedDisplay[];
559
580
  get testPrints(): CapturedTestPrint[];
581
+ /** Durable receipts accepted by `edge.enqueueReceipt()` (one per distinct idempotency key). */
582
+ get durablePrints(): CapturedDurablePrint[];
583
+ /**
584
+ * Decide how the NEXT accepted durable job settles when the app reads it with
585
+ * `edge.getPrintJob()`. Without this every job reads `sent_to_printer`. Queue
586
+ * several to model a run of jobs; use `needs_attention` with `uncertain` to
587
+ * test the "check the printer before reprinting" path.
588
+ */
589
+ settleNextDurableJob(outcome: SimulatedDurableOutcome): void;
560
590
  latestPrint(): CapturedPrint | undefined;
561
591
  latestDisplay(): CapturedDisplay | undefined;
562
592
  get bindings(): BindingInfo[];
@@ -742,4 +772,4 @@ declare const TEST_ENV_DEFAULTS: {
742
772
  /** Default location of the DDL snapshot written by `generate-types`. */
743
773
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
744
774
 
745
- 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 };
775
+ export { type BindingInfo, CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedCheckout, type CapturedDisplay, type CapturedDurablePrint, 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 SimulatedDurableOutcome, 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
@@ -365,6 +365,8 @@ var state = globalSingleton("state", () => ({
365
365
  edgeDisplays: [],
366
366
  edgeTestPrints: [],
367
367
  edgePrintCounter: 0,
368
+ edgeDurableJobs: /* @__PURE__ */ new Map(),
369
+ edgeDurableOutcomes: [],
368
370
  edgeDevices: [],
369
371
  edgePeripherals: [],
370
372
  edgeBindings: /* @__PURE__ */ new Map(),
@@ -3382,6 +3384,93 @@ async function handlePrint(request) {
3382
3384
  status: "completed"
3383
3385
  });
3384
3386
  }
3387
+ var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
3388
+ async function handleEnqueue(request) {
3389
+ const body = await readJsonBody(request);
3390
+ const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
3391
+ if (!IDEMPOTENCY_KEY.test(key)) {
3392
+ return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
3393
+ }
3394
+ if (body.openDrawer === true) {
3395
+ return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
3396
+ }
3397
+ if (body.copies !== void 0 && body.copies !== 1) {
3398
+ return edgeFailure(
3399
+ "Durable receipts print exactly one copy; submit a second job for another copy"
3400
+ );
3401
+ }
3402
+ const receipt = body.receipt ?? {};
3403
+ if (receipt.rawEscPos !== void 0) {
3404
+ return edgeFailure("rawEscPos is not supported for durable receipts");
3405
+ }
3406
+ const requestHash = JSON.stringify([
3407
+ body.alias,
3408
+ body.deviceId,
3409
+ body.peripheralId,
3410
+ receipt,
3411
+ body.logo
3412
+ ]);
3413
+ const existing = [...state.edgeDurableJobs.values()].find(
3414
+ (j) => j.captured.idempotencyKey === key
3415
+ );
3416
+ if (existing) {
3417
+ if (existing.requestHash !== requestHash) {
3418
+ return edgeFailure(
3419
+ "This idempotencyKey was already used for a different receipt, target or options.",
3420
+ 409,
3421
+ "IDEMPOTENCY_KEY_CONFLICT"
3422
+ );
3423
+ }
3424
+ return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
3425
+ }
3426
+ const jobId = nextJobId();
3427
+ const captured = {
3428
+ jobId,
3429
+ idempotencyKey: key,
3430
+ alias: body.alias ? String(body.alias) : void 0,
3431
+ deviceId: body.deviceId ? String(body.deviceId) : void 0,
3432
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
3433
+ receipt,
3434
+ logo: body.logo === true ? true : body.logo === false ? false : void 0
3435
+ };
3436
+ const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
3437
+ state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
3438
+ return json({ success: true, data: durableView(jobId, "queued") }, 202);
3439
+ }
3440
+ function durableView(jobId, as) {
3441
+ const job = state.edgeDurableJobs.get(jobId);
3442
+ const createdAt = now2();
3443
+ const base = {
3444
+ jobId,
3445
+ attempts: as === "queued" ? 0 : 1,
3446
+ createdAt,
3447
+ expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
3448
+ target: {
3449
+ deviceId: job.captured.deviceId ?? "sim-device",
3450
+ peripheralId: job.captured.peripheralId ?? "sim-peripheral"
3451
+ }
3452
+ };
3453
+ const outcome = as === "queued" ? { status: "queued" } : job.outcome;
3454
+ if (outcome.status === "needs_attention") {
3455
+ return {
3456
+ ...base,
3457
+ status: "needs_attention",
3458
+ attention: {
3459
+ reason: outcome.reason,
3460
+ outcome: outcome.outcome,
3461
+ message: outcome.message ?? "Delivery stopped."
3462
+ }
3463
+ };
3464
+ }
3465
+ if (outcome.status === "sent_to_printer") {
3466
+ return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
3467
+ }
3468
+ return { ...base, status: "queued" };
3469
+ }
3470
+ function handleGetDurable(jobId) {
3471
+ if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
3472
+ return success(durableView(jobId));
3473
+ }
3385
3474
  async function handleShowDisplay(request) {
3386
3475
  const body = await readJsonBody(request);
3387
3476
  const captured = {
@@ -3431,6 +3520,13 @@ async function handleEdgeRequest(request, subPath) {
3431
3520
  if (subPath === "/print" && method === "POST") {
3432
3521
  return handlePrint(request);
3433
3522
  }
3523
+ if (subPath === "/print-jobs" && method === "POST") {
3524
+ return handleEnqueue(request);
3525
+ }
3526
+ const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
3527
+ if (durableMatch && method === "GET") {
3528
+ return handleGetDurable(decodeURIComponent(durableMatch[1]));
3529
+ }
3434
3530
  if (subPath === "/capabilities" && method === "GET") {
3435
3531
  return handleCapabilities(request);
3436
3532
  }
@@ -3474,6 +3570,12 @@ function createEdge() {
3474
3570
  get testPrints() {
3475
3571
  return [...state.edgeTestPrints];
3476
3572
  },
3573
+ get durablePrints() {
3574
+ return [...state.edgeDurableJobs.values()].map((j) => j.captured);
3575
+ },
3576
+ settleNextDurableJob(outcome) {
3577
+ state.edgeDurableOutcomes.push(outcome);
3578
+ },
3477
3579
  latestPrint() {
3478
3580
  return state.edgePrints[state.edgePrints.length - 1];
3479
3581
  },
@@ -3510,12 +3612,14 @@ function createEdge() {
3510
3612
  state.edgeDisplays = [];
3511
3613
  state.edgeTestPrints = [];
3512
3614
  state.edgePrintCounter = 0;
3615
+ state.edgeDurableJobs.clear();
3616
+ state.edgeDurableOutcomes = [];
3513
3617
  state.edgeDevices = [];
3514
3618
  state.edgePeripherals = [];
3515
3619
  state.edgeBindings.clear();
3516
3620
  },
3517
3621
  get count() {
3518
- return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
3622
+ return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length + state.edgeDurableJobs.size;
3519
3623
  }
3520
3624
  };
3521
3625
  }
package/dist/index.mjs CHANGED
@@ -326,6 +326,8 @@ var state = globalSingleton("state", () => ({
326
326
  edgeDisplays: [],
327
327
  edgeTestPrints: [],
328
328
  edgePrintCounter: 0,
329
+ edgeDurableJobs: /* @__PURE__ */ new Map(),
330
+ edgeDurableOutcomes: [],
329
331
  edgeDevices: [],
330
332
  edgePeripherals: [],
331
333
  edgeBindings: /* @__PURE__ */ new Map(),
@@ -3343,6 +3345,93 @@ async function handlePrint(request) {
3343
3345
  status: "completed"
3344
3346
  });
3345
3347
  }
3348
+ var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
3349
+ async function handleEnqueue(request) {
3350
+ const body = await readJsonBody(request);
3351
+ const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
3352
+ if (!IDEMPOTENCY_KEY.test(key)) {
3353
+ return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
3354
+ }
3355
+ if (body.openDrawer === true) {
3356
+ return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
3357
+ }
3358
+ if (body.copies !== void 0 && body.copies !== 1) {
3359
+ return edgeFailure(
3360
+ "Durable receipts print exactly one copy; submit a second job for another copy"
3361
+ );
3362
+ }
3363
+ const receipt = body.receipt ?? {};
3364
+ if (receipt.rawEscPos !== void 0) {
3365
+ return edgeFailure("rawEscPos is not supported for durable receipts");
3366
+ }
3367
+ const requestHash = JSON.stringify([
3368
+ body.alias,
3369
+ body.deviceId,
3370
+ body.peripheralId,
3371
+ receipt,
3372
+ body.logo
3373
+ ]);
3374
+ const existing = [...state.edgeDurableJobs.values()].find(
3375
+ (j) => j.captured.idempotencyKey === key
3376
+ );
3377
+ if (existing) {
3378
+ if (existing.requestHash !== requestHash) {
3379
+ return edgeFailure(
3380
+ "This idempotencyKey was already used for a different receipt, target or options.",
3381
+ 409,
3382
+ "IDEMPOTENCY_KEY_CONFLICT"
3383
+ );
3384
+ }
3385
+ return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
3386
+ }
3387
+ const jobId = nextJobId();
3388
+ const captured = {
3389
+ jobId,
3390
+ idempotencyKey: key,
3391
+ alias: body.alias ? String(body.alias) : void 0,
3392
+ deviceId: body.deviceId ? String(body.deviceId) : void 0,
3393
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
3394
+ receipt,
3395
+ logo: body.logo === true ? true : body.logo === false ? false : void 0
3396
+ };
3397
+ const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
3398
+ state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
3399
+ return json({ success: true, data: durableView(jobId, "queued") }, 202);
3400
+ }
3401
+ function durableView(jobId, as) {
3402
+ const job = state.edgeDurableJobs.get(jobId);
3403
+ const createdAt = now2();
3404
+ const base = {
3405
+ jobId,
3406
+ attempts: as === "queued" ? 0 : 1,
3407
+ createdAt,
3408
+ expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
3409
+ target: {
3410
+ deviceId: job.captured.deviceId ?? "sim-device",
3411
+ peripheralId: job.captured.peripheralId ?? "sim-peripheral"
3412
+ }
3413
+ };
3414
+ const outcome = as === "queued" ? { status: "queued" } : job.outcome;
3415
+ if (outcome.status === "needs_attention") {
3416
+ return {
3417
+ ...base,
3418
+ status: "needs_attention",
3419
+ attention: {
3420
+ reason: outcome.reason,
3421
+ outcome: outcome.outcome,
3422
+ message: outcome.message ?? "Delivery stopped."
3423
+ }
3424
+ };
3425
+ }
3426
+ if (outcome.status === "sent_to_printer") {
3427
+ return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
3428
+ }
3429
+ return { ...base, status: "queued" };
3430
+ }
3431
+ function handleGetDurable(jobId) {
3432
+ if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
3433
+ return success(durableView(jobId));
3434
+ }
3346
3435
  async function handleShowDisplay(request) {
3347
3436
  const body = await readJsonBody(request);
3348
3437
  const captured = {
@@ -3392,6 +3481,13 @@ async function handleEdgeRequest(request, subPath) {
3392
3481
  if (subPath === "/print" && method === "POST") {
3393
3482
  return handlePrint(request);
3394
3483
  }
3484
+ if (subPath === "/print-jobs" && method === "POST") {
3485
+ return handleEnqueue(request);
3486
+ }
3487
+ const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
3488
+ if (durableMatch && method === "GET") {
3489
+ return handleGetDurable(decodeURIComponent(durableMatch[1]));
3490
+ }
3395
3491
  if (subPath === "/capabilities" && method === "GET") {
3396
3492
  return handleCapabilities(request);
3397
3493
  }
@@ -3435,6 +3531,12 @@ function createEdge() {
3435
3531
  get testPrints() {
3436
3532
  return [...state.edgeTestPrints];
3437
3533
  },
3534
+ get durablePrints() {
3535
+ return [...state.edgeDurableJobs.values()].map((j) => j.captured);
3536
+ },
3537
+ settleNextDurableJob(outcome) {
3538
+ state.edgeDurableOutcomes.push(outcome);
3539
+ },
3438
3540
  latestPrint() {
3439
3541
  return state.edgePrints[state.edgePrints.length - 1];
3440
3542
  },
@@ -3471,12 +3573,14 @@ function createEdge() {
3471
3573
  state.edgeDisplays = [];
3472
3574
  state.edgeTestPrints = [];
3473
3575
  state.edgePrintCounter = 0;
3576
+ state.edgeDurableJobs.clear();
3577
+ state.edgeDurableOutcomes = [];
3474
3578
  state.edgeDevices = [];
3475
3579
  state.edgePeripherals = [];
3476
3580
  state.edgeBindings.clear();
3477
3581
  },
3478
3582
  get count() {
3479
- return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length;
3583
+ return state.edgePrints.length + state.edgeDisplays.length + state.edgeTestPrints.length + state.edgeDurableJobs.size;
3480
3584
  }
3481
3585
  };
3482
3586
  }
@@ -87,6 +87,8 @@ var state = globalSingleton("state", () => ({
87
87
  edgeDisplays: [],
88
88
  edgeTestPrints: [],
89
89
  edgePrintCounter: 0,
90
+ edgeDurableJobs: /* @__PURE__ */ new Map(),
91
+ edgeDurableOutcomes: [],
90
92
  edgeDevices: [],
91
93
  edgePeripherals: [],
92
94
  edgeBindings: /* @__PURE__ */ new Map(),
@@ -60,6 +60,8 @@ var state = globalSingleton("state", () => ({
60
60
  edgeDisplays: [],
61
61
  edgeTestPrints: [],
62
62
  edgePrintCounter: 0,
63
+ edgeDurableJobs: /* @__PURE__ */ new Map(),
64
+ edgeDurableOutcomes: [],
63
65
  edgeDevices: [],
64
66
  edgePeripherals: [],
65
67
  edgeBindings: /* @__PURE__ */ new Map(),
package/dist/setup.js CHANGED
@@ -92,6 +92,8 @@ var state = globalSingleton("state", () => ({
92
92
  edgeDisplays: [],
93
93
  edgeTestPrints: [],
94
94
  edgePrintCounter: 0,
95
+ edgeDurableJobs: /* @__PURE__ */ new Map(),
96
+ edgeDurableOutcomes: [],
95
97
  edgeDevices: [],
96
98
  edgePeripherals: [],
97
99
  edgeBindings: /* @__PURE__ */ new Map(),
@@ -2527,6 +2529,93 @@ async function handlePrint(request) {
2527
2529
  status: "completed"
2528
2530
  });
2529
2531
  }
2532
+ var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
2533
+ async function handleEnqueue(request) {
2534
+ const body = await readJsonBody(request);
2535
+ const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
2536
+ if (!IDEMPOTENCY_KEY.test(key)) {
2537
+ return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
2538
+ }
2539
+ if (body.openDrawer === true) {
2540
+ return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
2541
+ }
2542
+ if (body.copies !== void 0 && body.copies !== 1) {
2543
+ return edgeFailure(
2544
+ "Durable receipts print exactly one copy; submit a second job for another copy"
2545
+ );
2546
+ }
2547
+ const receipt = body.receipt ?? {};
2548
+ if (receipt.rawEscPos !== void 0) {
2549
+ return edgeFailure("rawEscPos is not supported for durable receipts");
2550
+ }
2551
+ const requestHash = JSON.stringify([
2552
+ body.alias,
2553
+ body.deviceId,
2554
+ body.peripheralId,
2555
+ receipt,
2556
+ body.logo
2557
+ ]);
2558
+ const existing = [...state.edgeDurableJobs.values()].find(
2559
+ (j) => j.captured.idempotencyKey === key
2560
+ );
2561
+ if (existing) {
2562
+ if (existing.requestHash !== requestHash) {
2563
+ return edgeFailure(
2564
+ "This idempotencyKey was already used for a different receipt, target or options.",
2565
+ 409,
2566
+ "IDEMPOTENCY_KEY_CONFLICT"
2567
+ );
2568
+ }
2569
+ return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
2570
+ }
2571
+ const jobId = nextJobId();
2572
+ const captured = {
2573
+ jobId,
2574
+ idempotencyKey: key,
2575
+ alias: body.alias ? String(body.alias) : void 0,
2576
+ deviceId: body.deviceId ? String(body.deviceId) : void 0,
2577
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
2578
+ receipt,
2579
+ logo: body.logo === true ? true : body.logo === false ? false : void 0
2580
+ };
2581
+ const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
2582
+ state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
2583
+ return json({ success: true, data: durableView(jobId, "queued") }, 202);
2584
+ }
2585
+ function durableView(jobId, as) {
2586
+ const job = state.edgeDurableJobs.get(jobId);
2587
+ const createdAt = now2();
2588
+ const base = {
2589
+ jobId,
2590
+ attempts: as === "queued" ? 0 : 1,
2591
+ createdAt,
2592
+ expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
2593
+ target: {
2594
+ deviceId: job.captured.deviceId ?? "sim-device",
2595
+ peripheralId: job.captured.peripheralId ?? "sim-peripheral"
2596
+ }
2597
+ };
2598
+ const outcome = as === "queued" ? { status: "queued" } : job.outcome;
2599
+ if (outcome.status === "needs_attention") {
2600
+ return {
2601
+ ...base,
2602
+ status: "needs_attention",
2603
+ attention: {
2604
+ reason: outcome.reason,
2605
+ outcome: outcome.outcome,
2606
+ message: outcome.message ?? "Delivery stopped."
2607
+ }
2608
+ };
2609
+ }
2610
+ if (outcome.status === "sent_to_printer") {
2611
+ return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
2612
+ }
2613
+ return { ...base, status: "queued" };
2614
+ }
2615
+ function handleGetDurable(jobId) {
2616
+ if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
2617
+ return success(durableView(jobId));
2618
+ }
2530
2619
  async function handleShowDisplay(request) {
2531
2620
  const body = await readJsonBody(request);
2532
2621
  const captured = {
@@ -2576,6 +2665,13 @@ async function handleEdgeRequest(request, subPath) {
2576
2665
  if (subPath === "/print" && method === "POST") {
2577
2666
  return handlePrint(request);
2578
2667
  }
2668
+ if (subPath === "/print-jobs" && method === "POST") {
2669
+ return handleEnqueue(request);
2670
+ }
2671
+ const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
2672
+ if (durableMatch && method === "GET") {
2673
+ return handleGetDurable(decodeURIComponent(durableMatch[1]));
2674
+ }
2579
2675
  if (subPath === "/capabilities" && method === "GET") {
2580
2676
  return handleCapabilities(request);
2581
2677
  }
package/dist/setup.mjs CHANGED
@@ -68,6 +68,8 @@ var state = globalSingleton("state", () => ({
68
68
  edgeDisplays: [],
69
69
  edgeTestPrints: [],
70
70
  edgePrintCounter: 0,
71
+ edgeDurableJobs: /* @__PURE__ */ new Map(),
72
+ edgeDurableOutcomes: [],
71
73
  edgeDevices: [],
72
74
  edgePeripherals: [],
73
75
  edgeBindings: /* @__PURE__ */ new Map(),
@@ -2503,6 +2505,93 @@ async function handlePrint(request) {
2503
2505
  status: "completed"
2504
2506
  });
2505
2507
  }
2508
+ var IDEMPOTENCY_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
2509
+ async function handleEnqueue(request) {
2510
+ const body = await readJsonBody(request);
2511
+ const key = typeof body.idempotencyKey === "string" ? body.idempotencyKey : "";
2512
+ if (!IDEMPOTENCY_KEY.test(key)) {
2513
+ return edgeFailure("idempotencyKey must be 1-128 characters of A-Z a-z 0-9 _ . : -");
2514
+ }
2515
+ if (body.openDrawer === true) {
2516
+ return edgeFailure("Durable receipts cannot open the cash drawer; a retry would kick it again");
2517
+ }
2518
+ if (body.copies !== void 0 && body.copies !== 1) {
2519
+ return edgeFailure(
2520
+ "Durable receipts print exactly one copy; submit a second job for another copy"
2521
+ );
2522
+ }
2523
+ const receipt = body.receipt ?? {};
2524
+ if (receipt.rawEscPos !== void 0) {
2525
+ return edgeFailure("rawEscPos is not supported for durable receipts");
2526
+ }
2527
+ const requestHash = JSON.stringify([
2528
+ body.alias,
2529
+ body.deviceId,
2530
+ body.peripheralId,
2531
+ receipt,
2532
+ body.logo
2533
+ ]);
2534
+ const existing = [...state.edgeDurableJobs.values()].find(
2535
+ (j) => j.captured.idempotencyKey === key
2536
+ );
2537
+ if (existing) {
2538
+ if (existing.requestHash !== requestHash) {
2539
+ return edgeFailure(
2540
+ "This idempotencyKey was already used for a different receipt, target or options.",
2541
+ 409,
2542
+ "IDEMPOTENCY_KEY_CONFLICT"
2543
+ );
2544
+ }
2545
+ return json({ success: true, data: durableView(existing.captured.jobId, "queued") }, 200);
2546
+ }
2547
+ const jobId = nextJobId();
2548
+ const captured = {
2549
+ jobId,
2550
+ idempotencyKey: key,
2551
+ alias: body.alias ? String(body.alias) : void 0,
2552
+ deviceId: body.deviceId ? String(body.deviceId) : void 0,
2553
+ peripheralId: body.peripheralId ? String(body.peripheralId) : void 0,
2554
+ receipt,
2555
+ logo: body.logo === true ? true : body.logo === false ? false : void 0
2556
+ };
2557
+ const outcome = state.edgeDurableOutcomes.shift() ?? { status: "sent_to_printer" };
2558
+ state.edgeDurableJobs.set(jobId, { requestHash, captured, outcome });
2559
+ return json({ success: true, data: durableView(jobId, "queued") }, 202);
2560
+ }
2561
+ function durableView(jobId, as) {
2562
+ const job = state.edgeDurableJobs.get(jobId);
2563
+ const createdAt = now2();
2564
+ const base = {
2565
+ jobId,
2566
+ attempts: as === "queued" ? 0 : 1,
2567
+ createdAt,
2568
+ expiresAt: new Date(Date.parse(createdAt) + 10 * 6e4).toISOString(),
2569
+ target: {
2570
+ deviceId: job.captured.deviceId ?? "sim-device",
2571
+ peripheralId: job.captured.peripheralId ?? "sim-peripheral"
2572
+ }
2573
+ };
2574
+ const outcome = as === "queued" ? { status: "queued" } : job.outcome;
2575
+ if (outcome.status === "needs_attention") {
2576
+ return {
2577
+ ...base,
2578
+ status: "needs_attention",
2579
+ attention: {
2580
+ reason: outcome.reason,
2581
+ outcome: outcome.outcome,
2582
+ message: outcome.message ?? "Delivery stopped."
2583
+ }
2584
+ };
2585
+ }
2586
+ if (outcome.status === "sent_to_printer") {
2587
+ return { ...base, status: "sent_to_printer", sentToPrinterAt: createdAt };
2588
+ }
2589
+ return { ...base, status: "queued" };
2590
+ }
2591
+ function handleGetDurable(jobId) {
2592
+ if (!state.edgeDurableJobs.has(jobId)) return edgeFailure("Print job not found", 404);
2593
+ return success(durableView(jobId));
2594
+ }
2506
2595
  async function handleShowDisplay(request) {
2507
2596
  const body = await readJsonBody(request);
2508
2597
  const captured = {
@@ -2552,6 +2641,13 @@ async function handleEdgeRequest(request, subPath) {
2552
2641
  if (subPath === "/print" && method === "POST") {
2553
2642
  return handlePrint(request);
2554
2643
  }
2644
+ if (subPath === "/print-jobs" && method === "POST") {
2645
+ return handleEnqueue(request);
2646
+ }
2647
+ const durableMatch = subPath.match(/^\/print-jobs\/([^/]+)$/);
2648
+ if (durableMatch && method === "GET") {
2649
+ return handleGetDurable(decodeURIComponent(durableMatch[1]));
2650
+ }
2555
2651
  if (subPath === "/capabilities" && method === "GET") {
2556
2652
  return handleCapabilities(request);
2557
2653
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.24.0",
3
+ "version": "0.25.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",