@ripplo/testing 0.4.7 → 0.5.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/dist/lockfile.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import "./chunk-4MGIQFAJ.js";
2
2
 
3
3
  // src/lockfile.ts
4
+ import { createHash } from "crypto";
4
5
  import fs from "fs/promises";
5
6
  import path from "path";
6
7
 
@@ -500,6 +501,7 @@ var observerSchema = z9.object({
500
501
  // src/lockfile.ts
501
502
  import { z as z10 } from "zod";
502
503
  var LOCKFILE_RELATIVE_PATH = ".ripplo/ripplo.lock";
504
+ var FIXTURES_RELATIVE_PATH = ".ripplo/fixtures";
503
505
  var MAX_TESTS = 5e3;
504
506
  var requiresKeysSchema = z10.record(z10.string().max(200), z10.string().max(200));
505
507
  var compiledTestSchema = z10.object({
@@ -512,14 +514,32 @@ var compiledTestSchema = z10.object({
512
514
  sourcePath: z10.string().max(500).optional(),
513
515
  spec: workflowSpecSchema
514
516
  });
515
- var lockfileBodySchema = z10.object({
517
+ var fixtureEntrySchema = z10.object({
518
+ sha256: z10.string().regex(/^[0-9a-f]{64}$/u),
519
+ size: z10.number().int().nonnegative()
520
+ });
521
+ var fixturesMapSchema = z10.record(z10.string().min(1).max(500), fixtureEntrySchema);
522
+ var lockfileBodyV1Schema = z10.object({
523
+ observers: z10.record(z10.string().max(200), observerSchema),
524
+ preconditions: z10.record(z10.string().max(200), preconditionSchema),
525
+ tests: z10.array(compiledTestSchema).max(MAX_TESTS)
526
+ });
527
+ var lockfileBodyV2Schema = z10.object({
528
+ fixtures: fixturesMapSchema,
516
529
  observers: z10.record(z10.string().max(200), observerSchema),
517
530
  preconditions: z10.record(z10.string().max(200), preconditionSchema),
518
531
  tests: z10.array(compiledTestSchema).max(MAX_TESTS)
519
532
  });
520
- var lockfileCodec = defineCodec("ripplo-lockfile").initial(lockfileBodySchema).build();
533
+ var lockfileCodec = defineCodec("ripplo-lockfile").initial(lockfileBodyV1Schema).upgrade(
534
+ lockfileBodyV2Schema,
535
+ (prev) => ({
536
+ ...prev,
537
+ fixtures: {}
538
+ })
539
+ ).build();
521
540
  function compileResultToLockfile(result) {
522
541
  return {
542
+ fixtures: { ...result.fixtures },
523
543
  observers: result.observers,
524
544
  preconditions: result.preconditions,
525
545
  tests: result.tests.filter((test) => test.implemented).map((test) => ({
@@ -547,12 +567,76 @@ async function readLockfile({ cwd }) {
547
567
  }
548
568
  return decodeJson(lockfileCodec, raw);
549
569
  }
570
+ var MAX_FIXTURE_BYTES = 10 * 1024 * 1024;
571
+ var MAX_TOTAL_FIXTURE_BYTES = 50 * 1024 * 1024;
550
572
  async function writeLockfile({ cwd, result }) {
551
- const lockfile = compileResultToLockfile(result);
573
+ const hydrated = await hashFixturesIntoCompileResult({ cwd, result });
574
+ const lockfile = compileResultToLockfile(hydrated);
552
575
  const lockfilePath = path.join(cwd, LOCKFILE_RELATIVE_PATH);
553
576
  await fs.mkdir(path.dirname(lockfilePath), { recursive: true });
554
577
  await fs.writeFile(lockfilePath, serializeLockfile(lockfile), "utf8");
555
578
  }
579
+ async function hashFixturesIntoCompileResult({
580
+ cwd,
581
+ result
582
+ }) {
583
+ const referenced = collectFixtureReferences(result);
584
+ if (referenced.size === 0) {
585
+ return { ...result, fixtures: {} };
586
+ }
587
+ const fixturesRoot = path.join(cwd, FIXTURES_RELATIVE_PATH);
588
+ const sortedNames = [...referenced].toSorted((a, b) => a.localeCompare(b));
589
+ const hashed = await Promise.all(
590
+ sortedNames.map(async (name) => {
591
+ const entry = await hashOneFixture({ fixturesRoot, name });
592
+ if (entry.size > MAX_FIXTURE_BYTES) {
593
+ throw new Error(
594
+ `Fixture "${name}" is ${String(entry.size)} bytes; exceeds per-file limit of ${String(MAX_FIXTURE_BYTES)} bytes`
595
+ );
596
+ }
597
+ return [name, entry];
598
+ })
599
+ );
600
+ const total = hashed.reduce((sum, [, entry]) => sum + entry.size, 0);
601
+ if (total > MAX_TOTAL_FIXTURE_BYTES) {
602
+ throw new Error(
603
+ `Total fixtures size exceeds limit of ${String(MAX_TOTAL_FIXTURE_BYTES)} bytes`
604
+ );
605
+ }
606
+ return { ...result, fixtures: Object.fromEntries(hashed) };
607
+ }
608
+ async function hashOneFixture({ fixturesRoot, name }) {
609
+ if (name.includes("..") || path.isAbsolute(name)) {
610
+ throw new Error(`Invalid fixture name "${name}": must be a path under .ripplo/fixtures/`);
611
+ }
612
+ const abs = path.join(fixturesRoot, name);
613
+ const stat = await fs.lstat(abs).catch((error) => {
614
+ if (isNodeError(error) && error.code === "ENOENT") {
615
+ throw new Error(`Fixture "${name}" not found at ${abs}`);
616
+ }
617
+ throw error;
618
+ });
619
+ if (stat.isSymbolicLink()) {
620
+ throw new Error(`Fixture "${name}" is a symlink; symlinks are not allowed`);
621
+ }
622
+ if (!stat.isFile()) {
623
+ throw new Error(`Fixture "${name}" is not a regular file`);
624
+ }
625
+ const bytes = await fs.readFile(abs);
626
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
627
+ return { sha256, size: bytes.byteLength };
628
+ }
629
+ function collectFixtureReferences(result) {
630
+ const names = /* @__PURE__ */ new Set();
631
+ result.tests.forEach((test) => {
632
+ Object.values(test.spec.nodes).forEach((node) => {
633
+ if (node.type === "upload") {
634
+ node.files.forEach((name) => names.add(name));
635
+ }
636
+ });
637
+ });
638
+ return names;
639
+ }
556
640
  function compareCompileToLockfile({
557
641
  compiled,
558
642
  existing
@@ -589,9 +673,11 @@ function isPlainObject(value) {
589
673
  return typeof value === "object" && value !== null && !Array.isArray(value);
590
674
  }
591
675
  export {
676
+ FIXTURES_RELATIVE_PATH,
592
677
  LOCKFILE_RELATIVE_PATH,
593
678
  compareCompileToLockfile,
594
679
  compileResultToLockfile,
680
+ hashFixturesIntoCompileResult,
595
681
  lockfileCodec,
596
682
  readLockfile,
597
683
  serializeLockfile,
package/dist/nestjs.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DynamicModule } from '@nestjs/common';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/nestjs.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  batchRequestSchema,
3
- buildSetCookieHeader,
4
3
  observerRequestSchema,
5
4
  readAdapterWebhookSecret,
6
- serializeCookie,
7
5
  teardownRequestSchema,
6
+ toBatchRunResults,
7
+ toTeardownResults,
8
8
  verifyWebhookSignature
9
- } from "./chunk-V6LMXKGL.js";
9
+ } from "./chunk-XO36IU66.js";
10
10
  import {
11
11
  __decorateClass,
12
12
  __decorateParam
@@ -45,25 +45,15 @@ function createController(path) {
45
45
  }
46
46
  const parsed = batchRequestSchema.safeParse(req.body);
47
47
  if (!parsed.success) {
48
- res.status(400).json({ error: "Invalid request body", success: false });
48
+ res.status(400).json({ error: "Invalid request body" });
49
49
  return;
50
50
  }
51
51
  const host = req.get("host") ?? "";
52
52
  const proto = req.get("x-forwarded-proto") ?? req.protocol;
53
53
  const appUrl = `${proto}://${host}`;
54
- const result = await this.opts.engine.executePreconditions(parsed.data.preconditions, {
55
- appUrl
56
- });
57
- result.cookies.forEach((cookie) => {
58
- res.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
59
- });
60
- res.status(200).json({
61
- data: result.data,
62
- error: result.error,
63
- executed: result.executed,
64
- runId: result.runId,
65
- success: result.success
66
- });
54
+ const items = parsed.data.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
55
+ const results = await this.opts.engine.executePreconditions(items, { appUrl });
56
+ res.status(200).json({ results: toBatchRunResults(results) });
67
57
  }
68
58
  async executeObserver(req, res) {
69
59
  if (!guard(req, res, this.opts)) {
@@ -86,11 +76,16 @@ function createController(path) {
86
76
  }
87
77
  const parsed = teardownRequestSchema.safeParse(req.body);
88
78
  if (!parsed.success) {
89
- res.status(400).json({ error: "Invalid request body", success: false });
79
+ res.status(400).json({ error: "Invalid request body" });
90
80
  return;
91
81
  }
92
- await this.opts.engine.teardown(parsed.data.preconditions, parsed.data.data);
93
- res.status(200).json({ success: true });
82
+ const items = parsed.data.batch.map((b) => ({
83
+ data: b.data,
84
+ names: b.preconditions,
85
+ runId: b.runId
86
+ }));
87
+ const results = await this.opts.engine.teardown(items);
88
+ res.status(200).json({ results: toTeardownResults(results) });
94
89
  }
95
90
  };
96
91
  __decorateClass([
package/dist/nextjs.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
2
- import './builder-BMjy83Iy.js';
3
- import './types-16SB7zjP.js';
1
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
2
+ import './builder-DiVz3t1D.js';
3
+ import './types-BzZrl65Z.js';
4
4
  import './step-De52hTLd.js';
5
5
  import '@ripplo/spec';
6
6
 
package/dist/nextjs.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  batchRequestSchema,
3
- buildSetCookieHeader,
4
3
  observerRequestSchema,
5
4
  readAdapterWebhookSecret,
6
- serializeCookie,
7
5
  teardownRequestSchema,
6
+ toBatchRunResults,
7
+ toTeardownResults,
8
8
  verifyWebhookSignature
9
- } from "./chunk-V6LMXKGL.js";
9
+ } from "./chunk-XO36IU66.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/nextjs.ts
@@ -59,38 +59,31 @@ async function handleExecutePreconditions({
59
59
  const json = tryParseJson(body);
60
60
  const parsed = json == null ? null : batchRequestSchema.safeParse(json);
61
61
  if (parsed == null || !parsed.success) {
62
- return jsonResponse({ error: "Invalid request body", success: false }, 400);
62
+ return jsonResponse({ error: "Invalid request body" }, 400);
63
63
  }
64
64
  const host = req.headers.get("host");
65
65
  if (host == null || host.length === 0) {
66
- return jsonResponse({ error: "Missing host header", success: false }, 400);
66
+ return jsonResponse({ error: "Missing host header" }, 400);
67
67
  }
68
68
  const proto = req.headers.get("x-forwarded-proto") ?? "http";
69
69
  const appUrl = `${proto}://${host}`;
70
- const result = await engine.executePreconditions(parsed.data.preconditions, { appUrl });
71
- const headers = new Headers({ "content-type": "application/json" });
72
- result.cookies.forEach((cookie) => {
73
- headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
74
- });
75
- return new Response(
76
- JSON.stringify({
77
- data: result.data,
78
- error: result.error,
79
- executed: result.executed,
80
- runId: result.runId,
81
- success: result.success
82
- }),
83
- { headers, status: 200 }
84
- );
70
+ const items = parsed.data.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
71
+ const results = await engine.executePreconditions(items, { appUrl });
72
+ return jsonResponse({ results: toBatchRunResults(results) }, 200);
85
73
  }
86
74
  async function handleTeardown({ body, engine }) {
87
75
  const json = tryParseJson(body);
88
76
  const parsed = json == null ? null : teardownRequestSchema.safeParse(json);
89
77
  if (parsed == null || !parsed.success) {
90
- return jsonResponse({ error: "Invalid request body", success: false }, 400);
78
+ return jsonResponse({ error: "Invalid request body" }, 400);
91
79
  }
92
- await engine.teardown(parsed.data.preconditions, parsed.data.data);
93
- return jsonResponse({ success: true }, 200);
80
+ const items = parsed.data.batch.map((b) => ({
81
+ data: b.data,
82
+ names: b.preconditions,
83
+ runId: b.runId
84
+ }));
85
+ const results = await engine.teardown(items);
86
+ return jsonResponse({ results: toTeardownResults(results) }, 200);
94
87
  }
95
88
  async function verifyAndReadBody(req, webhookSecret) {
96
89
  if (webhookSecret.length === 0) {
@@ -40,6 +40,13 @@ interface Precondition<TData extends Record<string, Primitive> = Record<string,
40
40
  }
41
41
  type PreconditionData<T extends Precondition> = T[typeof PRECONDITION_DATA];
42
42
  type PreconditionDeps<T extends Precondition> = T[typeof PRECONDITION_DEPS];
43
+ interface PreconditionDefinitionSetupItem {
44
+ readonly ctx: SetupContext;
45
+ readonly deps: Record<string, Record<string, Primitive>>;
46
+ }
47
+ interface PreconditionDefinitionTeardownItem {
48
+ readonly ctx: TeardownContext<Record<string, Primitive>>;
49
+ }
43
50
  interface PreconditionDefinition {
44
51
  readonly dependsOn: ReadonlyArray<string>;
45
52
  readonly depMapping: ReadonlyArray<readonly [string, string]>;
@@ -47,8 +54,8 @@ interface PreconditionDefinition {
47
54
  readonly implemented: boolean;
48
55
  readonly name: string;
49
56
  readonly returns: ReadonlyArray<string>;
50
- readonly teardown: ((ctx: TeardownContext<Record<string, Primitive>>) => Promise<void>) | undefined;
51
- readonly setup: (ctx: SetupContext, deps: Record<string, Record<string, Primitive>>) => Promise<Record<string, Primitive>>;
57
+ readonly teardown: ((items: ReadonlyArray<PreconditionDefinitionTeardownItem>) => Promise<void>) | undefined;
58
+ readonly setup: (items: ReadonlyArray<PreconditionDefinitionSetupItem>) => Promise<ReadonlyArray<Record<string, Primitive>>>;
52
59
  }
53
60
  type VarsFn<T> = (vars: Record<string, Record<string, Primitive>>) => T;
54
61
  interface TestDefinition {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ripplo/testing",
3
3
  "description": "TypeScript DSL for defining and running Ripplo e2e workflow tests",
4
- "version": "0.4.7",
4
+ "version": "0.5.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -99,8 +99,8 @@
99
99
  "tsup": "^8.5.1",
100
100
  "typescript": "catalog:",
101
101
  "vitest": "^4.1.4",
102
- "@ripplo/spec": "^0.0.0",
103
- "@ripplo/eslint-config": "0.0.0"
102
+ "@ripplo/eslint-config": "0.0.0",
103
+ "@ripplo/spec": "^0.0.0"
104
104
  },
105
105
  "peerDependencies": {
106
106
  "@nestjs/common": "^10.0.0 || ^11.0.0",