@sealpixel/plugin-testing 1.0.0-rc.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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @sealpixel/plugin-testing
2
+
3
+ `checkPluginParity` pins render hashes in a goldens file; `fuzzPlugin` checks reducer totality, idempotent rendering and `upTo` consistency. Used by `create-sealpixel-plugin` scaffolds.
@@ -0,0 +1,45 @@
1
+ import { type AnyPlugin, type AssetResolver, type Operation, type RenderOptions } from '@sealpixel/core';
2
+ import fc from 'fast-check';
3
+ export interface PluginParityCase {
4
+ id: string;
5
+ /** Source image bytes (PNG, JPEG or WebP). */
6
+ image: Uint8Array;
7
+ /** Ops for the plugin under test (and any first-party ops). */
8
+ ops: Operation[];
9
+ output?: RenderOptions['output'];
10
+ }
11
+ export interface PluginParityOptions {
12
+ plugin: AnyPlugin | AnyPlugin[];
13
+ cases: PluginParityCase[];
14
+ /** Path of the JSON file holding `{ [id]: hash }`. Created on first run. */
15
+ goldens: string;
16
+ /** Additional plugins available to the cases. Defaults to the first-party set. */
17
+ basePlugins?: readonly AnyPlugin[];
18
+ assets?: AssetResolver;
19
+ /** Set to true (or `SEALPIXEL_UPDATE_GOLDENS=1`) to rewrite the goldens file. */
20
+ update?: boolean;
21
+ }
22
+ export interface PluginParityResult {
23
+ id: string;
24
+ hash: string;
25
+ expected: string | null;
26
+ ok: boolean;
27
+ }
28
+ /** Renders every case and compares with the goldens file. Returns all results; throws when one differs and `update` is off. */
29
+ export declare function checkPluginParity(options: PluginParityOptions): Promise<PluginParityResult[]>;
30
+ export interface FuzzPluginOptions {
31
+ plugin: AnyPlugin;
32
+ /** Generates valid ops for the plugin. */
33
+ ops: fc.Arbitrary<Operation>;
34
+ image: Uint8Array;
35
+ basePlugins?: readonly AnyPlugin[];
36
+ assets?: AssetResolver;
37
+ runs?: number;
38
+ maxOps?: number;
39
+ }
40
+ /**
41
+ * Properties: parse round-trip, reducer totality, render never throws, render
42
+ * is idempotent, `upTo` equals the sliced log.
43
+ */
44
+ export declare function fuzzPlugin(options: FuzzPluginOptions): Promise<void>;
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,aAAa,EAIlB,KAAK,SAAS,EACd,KAAK,aAAa,EAEnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,KAAK,EAAE,UAAU,CAAC;IAClB,+DAA+D;IAC/D,GAAG,EAAE,SAAS,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;IAChC,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,WAAW,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACnC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,iFAAiF;IACjF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,EAAE,EAAE,OAAO,CAAC;CACb;AAED,+HAA+H;AAC/H,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAsC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,SAAS,CAAC;IAClB,0CAA0C;IAC1C,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACnC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC1E"}
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Helpers for plugin authors. `describePluginParity` renders cases through
3
+ * `render()` and pins their hashes in a goldens file, so a kernel change is a
4
+ * visible diff and Node/browser/server outputs can be cross-checked.
5
+ * `fuzzPlugin` checks the reducer is total and rendering never throws.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { createEditLog, createRegistry, render, } from '@sealpixel/core';
9
+ import { defaultPlugins } from '@sealpixel/plugins';
10
+ import { loadSealpixelWasm } from '@sealpixel/wasm';
11
+ import fc from 'fast-check';
12
+ /** Renders every case and compares with the goldens file. Returns all results; throws when one differs and `update` is off. */
13
+ export async function checkPluginParity(options) {
14
+ const plugins = Array.isArray(options.plugin) ? options.plugin : [options.plugin];
15
+ const registry = createRegistry([...(options.basePlugins ?? defaultPlugins), ...plugins]);
16
+ const wasm = await loadSealpixelWasm();
17
+ const update = options.update ?? process.env['SEALPIXEL_UPDATE_GOLDENS'] !== undefined;
18
+ const stored = existsSync(options.goldens)
19
+ ? JSON.parse(readFileSync(options.goldens, 'utf8'))
20
+ : {};
21
+ const results = [];
22
+ for (const c of options.cases) {
23
+ const decoded = wasm.decode(c.image);
24
+ const log = {
25
+ ...createEditLog({ id: c.id, width: decoded.width, height: decoded.height }, registry),
26
+ ops: c.ops,
27
+ };
28
+ const out = await render(c.image, log, {
29
+ output: c.output ?? { format: 'image/png' },
30
+ registry,
31
+ wasm,
32
+ ...(options.assets ? { assets: options.assets } : {}),
33
+ });
34
+ const expected = stored[c.id] ?? null;
35
+ results.push({ id: c.id, hash: out.hash, expected, ok: expected === out.hash });
36
+ if (update)
37
+ stored[c.id] = out.hash;
38
+ }
39
+ if (update) {
40
+ const next = `${JSON.stringify(stored, null, 2)}\n`;
41
+ if (!existsSync(options.goldens) || readFileSync(options.goldens, 'utf8') !== next)
42
+ writeFileSync(options.goldens, next);
43
+ return results.map((r) => ({ ...r, expected: r.hash, ok: true }));
44
+ }
45
+ const failures = results.filter((r) => !r.ok);
46
+ if (failures.length > 0) {
47
+ throw new Error(`plugin parity: ${failures.length} case(s) differ:\n${failures.map((f) => ` ${f.id}: expected ${f.expected ?? '(missing, run with SEALPIXEL_UPDATE_GOLDENS=1)'} got ${f.hash}`).join('\n')}`);
48
+ }
49
+ return results;
50
+ }
51
+ /**
52
+ * Properties: parse round-trip, reducer totality, render never throws, render
53
+ * is idempotent, `upTo` equals the sliced log.
54
+ */
55
+ export async function fuzzPlugin(options) {
56
+ const registry = createRegistry([...(options.basePlugins ?? defaultPlugins), options.plugin]);
57
+ const wasm = await loadSealpixelWasm();
58
+ const decoded = wasm.decode(options.image);
59
+ const source = { id: 'fuzz', width: decoded.width, height: decoded.height };
60
+ const logs = fc
61
+ .uniqueArray(options.ops, { maxLength: options.maxOps ?? 6, selector: (op) => op.id })
62
+ .map((ops) => ({ ...createEditLog(source, registry), ops }));
63
+ const renderOptions = {
64
+ output: { format: 'image/png' },
65
+ registry,
66
+ wasm,
67
+ ...(options.assets ? { assets: options.assets } : {}),
68
+ };
69
+ await fc.assert(fc.asyncProperty(logs, async (log) => {
70
+ const parsed = registry.parseEditLog(JSON.parse(JSON.stringify(log)));
71
+ if (parsed.ops.length !== log.ops.length)
72
+ throw new Error('round trip changed the op count');
73
+ const a = await render(options.image, log, renderOptions);
74
+ const b = await render(options.image, log, renderOptions);
75
+ if (a.hash !== b.hash)
76
+ throw new Error('render is not idempotent');
77
+ const i = Math.floor(log.ops.length / 2);
78
+ const sliced = await render(options.image, { ...log, ops: log.ops.slice(0, i) }, renderOptions);
79
+ const upTo = await render(options.image, log, { ...renderOptions, upTo: i });
80
+ if (sliced.hash !== upTo.hash)
81
+ throw new Error('upTo differs from the sliced log');
82
+ }), { numRuns: options.runs ?? Number(process.env['FUZZ_RUNS'] ?? 25) });
83
+ }
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@sealpixel/plugin-testing",
3
+ "version": "1.0.0-rc.0",
4
+ "description": "Test helpers for Sealpixel plugin authors: pinned render goldens (checkPluginParity) and property-based fuzzing (fuzzPlugin).",
5
+ "license": "BUSL-1.1",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/9feet/sealpixel.git",
9
+ "directory": "packages/plugin-testing"
10
+ },
11
+ "homepage": "https://docs.sealpixel.dev",
12
+ "bugs": "https://github.com/9feet/sealpixel/issues",
13
+ "type": "module",
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit"
29
+ },
30
+ "dependencies": {
31
+ "@sealpixel/core": "workspace:*",
32
+ "@sealpixel/plugins": "workspace:*",
33
+ "@sealpixel/wasm": "workspace:*",
34
+ "fast-check": "^4.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@sealpixel/config": "workspace:*",
38
+ "@types/node": "^24.0.0",
39
+ "typescript": "^5.9.0"
40
+ }
41
+ }