@joystyk/testing 0.0.0-stage → 0.2.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +191 -2
  5. package/dist/builders.d.ts +59 -0
  6. package/dist/builders.js +129 -0
  7. package/dist/builders.js.map +1 -0
  8. package/dist/clock.d.ts +30 -0
  9. package/dist/clock.js +58 -0
  10. package/dist/clock.js.map +1 -0
  11. package/dist/deep.d.ts +25 -0
  12. package/dist/deep.js +105 -0
  13. package/dist/deep.js.map +1 -0
  14. package/dist/engine-conformance.d.ts +166 -0
  15. package/dist/engine-conformance.js +995 -0
  16. package/dist/engine-conformance.js.map +1 -0
  17. package/dist/fixtures/dice-race.d.ts +41 -0
  18. package/dist/fixtures/dice-race.js +153 -0
  19. package/dist/fixtures/dice-race.js.map +1 -0
  20. package/dist/fixtures/draw-play.d.ts +54 -0
  21. package/dist/fixtures/draw-play.js +188 -0
  22. package/dist/fixtures/draw-play.js.map +1 -0
  23. package/dist/fixtures/drift.d.ts +34 -0
  24. package/dist/fixtures/drift.js +109 -0
  25. package/dist/fixtures/drift.js.map +1 -0
  26. package/dist/fixtures/guards.d.ts +11 -0
  27. package/dist/fixtures/guards.js +16 -0
  28. package/dist/fixtures/guards.js.map +1 -0
  29. package/dist/fixtures/hint-puzzle.d.ts +47 -0
  30. package/dist/fixtures/hint-puzzle.js +129 -0
  31. package/dist/fixtures/hint-puzzle.js.map +1 -0
  32. package/dist/fixtures/index.d.ts +6 -0
  33. package/dist/fixtures/index.js +7 -0
  34. package/dist/fixtures/nim.d.ts +34 -0
  35. package/dist/fixtures/nim.js +141 -0
  36. package/dist/fixtures/nim.js.map +1 -0
  37. package/dist/fixtures/sealed-bids.d.ts +46 -0
  38. package/dist/fixtures/sealed-bids.js +141 -0
  39. package/dist/fixtures/sealed-bids.js.map +1 -0
  40. package/dist/index.d.ts +13 -0
  41. package/dist/index.js +14 -0
  42. package/dist/replay.d.ts +58 -0
  43. package/dist/replay.js +110 -0
  44. package/dist/replay.js.map +1 -0
  45. package/dist/report.d.ts +68 -0
  46. package/dist/report.js +104 -0
  47. package/dist/report.js.map +1 -0
  48. package/dist/rng.d.ts +18 -0
  49. package/dist/rng.js +25 -0
  50. package/dist/rng.js.map +1 -0
  51. package/dist/schema.d.ts +13 -0
  52. package/dist/schema.js +21 -0
  53. package/dist/schema.js.map +1 -0
  54. package/dist/session-ports.d.ts +16 -0
  55. package/dist/session-ports.js +32 -0
  56. package/dist/session-ports.js.map +1 -0
  57. package/dist/simulation-conformance.d.ts +71 -0
  58. package/dist/simulation-conformance.js +312 -0
  59. package/dist/simulation-conformance.js.map +1 -0
  60. package/dist/storage.d.ts +29 -0
  61. package/dist/storage.js +63 -0
  62. package/dist/storage.js.map +1 -0
  63. package/dist/vitest.d.ts +32 -0
  64. package/dist/vitest.js +45 -0
  65. package/dist/vitest.js.map +1 -0
  66. package/dist/waiting.d.ts +58 -0
  67. package/dist/waiting.js +97 -0
  68. package/dist/waiting.js.map +1 -0
  69. package/package.json +55 -3
@@ -0,0 +1,68 @@
1
+ import type { Replay } from '@joystyk/contracts';
2
+ /** One reproducible failure. */
3
+ export type Failure = {
4
+ readonly message: string;
5
+ /** Which generated game failed: rerun just it with `only: { setup, game }`. */
6
+ readonly setup?: number;
7
+ readonly game?: number;
8
+ /** The game's seed string; the engine's generator is `testRng(seed, 'engine')`. */
9
+ readonly seed?: string;
10
+ /** The number of inputs applied before the failure (0: right after `init`). */
11
+ readonly step?: number;
12
+ /** The replay of the accepted inputs up to the failure; `replayEngine` reproduces it. */
13
+ readonly replay?: Replay;
14
+ /** The input being checked when the failure happened, if any. */
15
+ readonly input?: unknown;
16
+ };
17
+ export type CheckStatus = 'passed' | 'failed' | 'skipped';
18
+ /** The result of one check across every generated game. */
19
+ export type CheckResult = {
20
+ readonly id: string;
21
+ readonly title: string;
22
+ readonly status: CheckStatus;
23
+ /** Why the check was skipped, or a note on what it covered. */
24
+ readonly note?: string;
25
+ /** How many times the check ran (positions, transitions, games …). */
26
+ readonly runs: number;
27
+ /** The first few failures; `failureCount` has the total. */
28
+ readonly failures: readonly Failure[];
29
+ readonly failureCount: number;
30
+ };
31
+ /** Everything a suite found. */
32
+ export type ConformanceReport = {
33
+ readonly subject: string;
34
+ readonly ok: boolean;
35
+ readonly checks: readonly CheckResult[];
36
+ /** Games generated and inputs applied, for a sense of coverage. */
37
+ readonly games: number;
38
+ readonly steps: number;
39
+ };
40
+ /** The failed checks' ids. */
41
+ export declare const failedChecks: (report: ConformanceReport) => string[];
42
+ /** One check by id; throws if the suite has no such check. */
43
+ export declare const checkById: (report: ConformanceReport, id: string) => CheckResult;
44
+ /** A failure as text a person can act on: what broke, the seed, and the input sequence. */
45
+ export declare const formatFailure: (failure: Failure) => string;
46
+ /** A check as text: its title, status and failures. */
47
+ export declare const formatCheck: (check: CheckResult) => string;
48
+ /** The whole report as text. */
49
+ export declare const formatReport: (report: ConformanceReport) => string;
50
+ /** Collects failures per check while a suite runs. */
51
+ export declare class Recorder {
52
+ #private;
53
+ constructor(definitions: readonly {
54
+ readonly id: string;
55
+ readonly title: string;
56
+ }[], limit: number);
57
+ /** Counts one run of a check. */
58
+ ran(id: string, times?: number): void;
59
+ /** Records a failure of a check. */
60
+ fail(id: string, failure: Failure): void;
61
+ /** How many failures a check has so far. */
62
+ failures(id: string): number;
63
+ /** Marks a check as skipped, with the reason. */
64
+ skip(id: string, reason: string): void;
65
+ /** Adds a note to a check's result. */
66
+ note(id: string, note: string): void;
67
+ report(subject: string, games: number, steps: number): ConformanceReport;
68
+ }
package/dist/report.js ADDED
@@ -0,0 +1,104 @@
1
+ //#region src/report.ts
2
+ /** The failed checks' ids. */
3
+ var failedChecks = (report) => report.checks.filter((c) => c.status === "failed").map((c) => c.id);
4
+ /** One check by id; throws if the suite has no such check. */
5
+ var checkById = (report, id) => {
6
+ const check = report.checks.find((c) => c.id === id);
7
+ if (check === void 0) throw new Error(`No check "${id}" in the report for ${report.subject}.`);
8
+ return check;
9
+ };
10
+ /** A failure as text a person can act on: what broke, the seed, and the input sequence. */
11
+ var formatFailure = (failure) => {
12
+ const lines = [failure.message];
13
+ const where = [];
14
+ if (failure.setup !== void 0) where.push(`setup ${failure.setup}`);
15
+ if (failure.game !== void 0) where.push(`game ${failure.game}`);
16
+ if (failure.seed !== void 0) where.push(`seed "${failure.seed}"`);
17
+ if (failure.step !== void 0) where.push(`after ${failure.step} input${failure.step === 1 ? "" : "s"}`);
18
+ if (where.length > 0) lines.push(` at: ${where.join(", ")}`);
19
+ if (failure.setup !== void 0 && failure.game !== void 0) lines.push(` rerun only this game: add \`only: { setup: ${failure.setup}, game: ${failure.game} }\` to the options`);
20
+ if (failure.input !== void 0) lines.push(` input: ${JSON.stringify(failure.input)}`);
21
+ if (failure.replay !== void 0) lines.push(` replay: ${JSON.stringify(failure.replay)}`);
22
+ return lines.join("\n");
23
+ };
24
+ /** A check as text: its title, status and failures. */
25
+ var formatCheck = (check) => {
26
+ const head = `[${check.id}] ${check.title}: ${check.status}${check.note === void 0 ? "" : ` (${check.note})`}`;
27
+ if (check.status !== "failed") return head;
28
+ const more = check.failureCount - check.failures.length;
29
+ return [
30
+ `${head}, ${check.failureCount} failure${check.failureCount === 1 ? "" : "s"}`,
31
+ ...check.failures.map((f, i) => `\n#${i + 1} ${formatFailure(f)}`),
32
+ ...more > 0 ? [`\n… and ${more} more`] : []
33
+ ].join("\n");
34
+ };
35
+ /** The whole report as text. */
36
+ var formatReport = (report) => [`${report.subject}: ${report.ok ? "conforms" : "FAILS"} (${report.games} games, ${report.steps} inputs)`, ...report.checks.map(formatCheck)].join("\n");
37
+ /** Collects failures per check while a suite runs. */
38
+ var Recorder = class {
39
+ #limit;
40
+ #checks = /* @__PURE__ */ new Map();
41
+ constructor(definitions, limit) {
42
+ this.#limit = limit;
43
+ for (const { id, title } of definitions) this.#checks.set(id, {
44
+ title,
45
+ runs: 0,
46
+ failures: [],
47
+ count: 0
48
+ });
49
+ }
50
+ #get(id) {
51
+ const check = this.#checks.get(id);
52
+ if (check === void 0) throw new Error(`Unknown check "${id}".`);
53
+ return check;
54
+ }
55
+ /** Counts one run of a check. */
56
+ ran(id, times = 1) {
57
+ this.#get(id).runs += times;
58
+ }
59
+ /** Records a failure of a check. */
60
+ fail(id, failure) {
61
+ const check = this.#get(id);
62
+ check.count += 1;
63
+ if (check.failures.length < this.#limit) check.failures.push(failure);
64
+ }
65
+ /** How many failures a check has so far. */
66
+ failures(id) {
67
+ return this.#get(id).count;
68
+ }
69
+ /** Marks a check as skipped, with the reason. */
70
+ skip(id, reason) {
71
+ this.#get(id).skip = reason;
72
+ }
73
+ /** Adds a note to a check's result. */
74
+ note(id, note) {
75
+ this.#get(id).note = note;
76
+ }
77
+ report(subject, games, steps) {
78
+ const checks = [...this.#checks].map(([id, c]) => {
79
+ const skip = c.skip ?? (c.runs === 0 ? "nothing to check in the generated games" : void 0);
80
+ const status = c.count > 0 ? "failed" : skip !== void 0 ? "skipped" : "passed";
81
+ const note = status === "skipped" ? skip : c.note;
82
+ return {
83
+ id,
84
+ title: c.title,
85
+ status,
86
+ ...note === void 0 ? {} : { note },
87
+ runs: c.runs,
88
+ failures: c.failures,
89
+ failureCount: c.count
90
+ };
91
+ });
92
+ return {
93
+ subject,
94
+ ok: checks.every((c) => c.status !== "failed"),
95
+ checks,
96
+ games,
97
+ steps
98
+ };
99
+ }
100
+ };
101
+ //#endregion
102
+ export { Recorder, checkById, failedChecks, formatCheck, formatFailure, formatReport };
103
+
104
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","names":[],"sources":["../src/report.ts"],"sourcesContent":["// Conformance reports: what was checked, what failed, and how to reproduce each failure.\n\nimport type { Replay } from '@joystyk/contracts';\n\n/** One reproducible failure. */\nexport type Failure = {\n readonly message: string;\n /** Which generated game failed: rerun just it with `only: { setup, game }`. */\n readonly setup?: number;\n readonly game?: number;\n /** The game's seed string; the engine's generator is `testRng(seed, 'engine')`. */\n readonly seed?: string;\n /** The number of inputs applied before the failure (0: right after `init`). */\n readonly step?: number;\n /** The replay of the accepted inputs up to the failure; `replayEngine` reproduces it. */\n readonly replay?: Replay;\n /** The input being checked when the failure happened, if any. */\n readonly input?: unknown;\n};\n\nexport type CheckStatus = 'passed' | 'failed' | 'skipped';\n\n/** The result of one check across every generated game. */\nexport type CheckResult = {\n readonly id: string;\n readonly title: string;\n readonly status: CheckStatus;\n /** Why the check was skipped, or a note on what it covered. */\n readonly note?: string;\n /** How many times the check ran (positions, transitions, games …). */\n readonly runs: number;\n /** The first few failures; `failureCount` has the total. */\n readonly failures: readonly Failure[];\n readonly failureCount: number;\n};\n\n/** Everything a suite found. */\nexport type ConformanceReport = {\n readonly subject: string;\n readonly ok: boolean;\n readonly checks: readonly CheckResult[];\n /** Games generated and inputs applied, for a sense of coverage. */\n readonly games: number;\n readonly steps: number;\n};\n\n/** The failed checks' ids. */\nexport const failedChecks = (report: ConformanceReport): string[] =>\n report.checks.filter((c) => c.status === 'failed').map((c) => c.id);\n\n/** One check by id; throws if the suite has no such check. */\nexport const checkById = (report: ConformanceReport, id: string): CheckResult => {\n const check = report.checks.find((c) => c.id === id);\n if (check === undefined) throw new Error(`No check \"${id}\" in the report for ${report.subject}.`);\n return check;\n};\n\n/** A failure as text a person can act on: what broke, the seed, and the input sequence. */\nexport const formatFailure = (failure: Failure): string => {\n const lines = [failure.message];\n const where: string[] = [];\n if (failure.setup !== undefined) where.push(`setup ${failure.setup}`);\n if (failure.game !== undefined) where.push(`game ${failure.game}`);\n if (failure.seed !== undefined) where.push(`seed \"${failure.seed}\"`);\n if (failure.step !== undefined) where.push(`after ${failure.step} input${failure.step === 1 ? '' : 's'}`);\n if (where.length > 0) lines.push(` at: ${where.join(', ')}`);\n if (failure.setup !== undefined && failure.game !== undefined) {\n lines.push(\n ` rerun only this game: add \\`only: { setup: ${failure.setup}, game: ${failure.game} }\\` to the options`,\n );\n }\n if (failure.input !== undefined) lines.push(` input: ${JSON.stringify(failure.input)}`);\n if (failure.replay !== undefined) lines.push(` replay: ${JSON.stringify(failure.replay)}`);\n return lines.join('\\n');\n};\n\n/** A check as text: its title, status and failures. */\nexport const formatCheck = (check: CheckResult): string => {\n const head = `[${check.id}] ${check.title}: ${check.status}${check.note === undefined ? '' : ` (${check.note})`}`;\n if (check.status !== 'failed') return head;\n const more = check.failureCount - check.failures.length;\n return [\n `${head}, ${check.failureCount} failure${check.failureCount === 1 ? '' : 's'}`,\n ...check.failures.map((f, i) => `\\n#${i + 1} ${formatFailure(f)}`),\n ...(more > 0 ? [`\\n… and ${more} more`] : []),\n ].join('\\n');\n};\n\n/** The whole report as text. */\nexport const formatReport = (report: ConformanceReport): string =>\n [\n `${report.subject}: ${report.ok ? 'conforms' : 'FAILS'} (${report.games} games, ${report.steps} inputs)`,\n ...report.checks.map(formatCheck),\n ].join('\\n');\n\n/** Collects failures per check while a suite runs. */\nexport class Recorder {\n readonly #limit: number;\n readonly #checks = new Map<\n string,\n { title: string; runs: number; failures: Failure[]; count: number; skip?: string; note?: string }\n >();\n\n constructor(definitions: readonly { readonly id: string; readonly title: string }[], limit: number) {\n this.#limit = limit;\n for (const { id, title } of definitions) this.#checks.set(id, { title, runs: 0, failures: [], count: 0 });\n }\n\n #get(id: string) {\n const check = this.#checks.get(id);\n if (check === undefined) throw new Error(`Unknown check \"${id}\".`);\n return check;\n }\n\n /** Counts one run of a check. */\n ran(id: string, times = 1): void {\n this.#get(id).runs += times;\n }\n\n /** Records a failure of a check. */\n fail(id: string, failure: Failure): void {\n const check = this.#get(id);\n check.count += 1;\n if (check.failures.length < this.#limit) check.failures.push(failure);\n }\n\n /** How many failures a check has so far. */\n failures(id: string): number {\n return this.#get(id).count;\n }\n\n /** Marks a check as skipped, with the reason. */\n skip(id: string, reason: string): void {\n this.#get(id).skip = reason;\n }\n\n /** Adds a note to a check's result. */\n note(id: string, note: string): void {\n this.#get(id).note = note;\n }\n\n report(subject: string, games: number, steps: number): ConformanceReport {\n const checks: CheckResult[] = [...this.#checks].map(([id, c]) => {\n const skip = c.skip ?? (c.runs === 0 ? 'nothing to check in the generated games' : undefined);\n const status: CheckStatus = c.count > 0 ? 'failed' : skip !== undefined ? 'skipped' : 'passed';\n const note = status === 'skipped' ? skip : c.note;\n return {\n id,\n title: c.title,\n status,\n ...(note === undefined ? {} : { note }),\n runs: c.runs,\n failures: c.failures,\n failureCount: c.count,\n };\n });\n return { subject, ok: checks.every((c) => c.status !== 'failed'), checks, games, steps };\n }\n}\n"],"mappings":"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"}
package/dist/rng.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { Rng, RngState } from '@joystyk/contracts';
2
+ /**
3
+ * The seed state for a test. `testSeed('nim', 3)` hashes `"joystyk-test/nim/3"`. The same
4
+ * parts give the same state on every machine and in every run.
5
+ */
6
+ export declare const testSeed: (...parts: readonly (string | number)[]) => RngState;
7
+ /**
8
+ * A seeded generator for a test, from the kit's SFC32. Pass the same parts to get the same
9
+ * stream again, for example to reproduce a failure the suites report.
10
+ */
11
+ export declare const testRng: (...parts: readonly (string | number)[]) => Rng;
12
+ /** `count` distinct, reproducible seed states derived from `base`. */
13
+ export declare const testSeeds: (base: string, count: number) => RngState[];
14
+ /**
15
+ * An independent copy of `rng` at its current position. Drawing from the copy leaves `rng`
16
+ * untouched, which is how the suites try an input without consuming randomness.
17
+ */
18
+ export declare const cloneRng: (rng: Rng) => Rng;
package/dist/rng.js ADDED
@@ -0,0 +1,25 @@
1
+ import { restoreRng, seedState } from "@joystyk/kit";
2
+ //#region src/rng.ts
3
+ /** Every test seed is namespaced, so a test seed never equals a production deal or daily seed. */
4
+ var PREFIX = "joystyk-test";
5
+ /**
6
+ * The seed state for a test. `testSeed('nim', 3)` hashes `"joystyk-test/nim/3"`. The same
7
+ * parts give the same state on every machine and in every run.
8
+ */
9
+ var testSeed = (...parts) => seedState([PREFIX, ...parts.map(String)].join("/"));
10
+ /**
11
+ * A seeded generator for a test, from the kit's SFC32. Pass the same parts to get the same
12
+ * stream again, for example to reproduce a failure the suites report.
13
+ */
14
+ var testRng = (...parts) => restoreRng(testSeed(...parts));
15
+ /** `count` distinct, reproducible seed states derived from `base`. */
16
+ var testSeeds = (base, count) => Array.from({ length: count }, (_, i) => testSeed(base, i));
17
+ /**
18
+ * An independent copy of `rng` at its current position. Drawing from the copy leaves `rng`
19
+ * untouched, which is how the suites try an input without consuming randomness.
20
+ */
21
+ var cloneRng = (rng) => restoreRng(rng.save());
22
+ //#endregion
23
+ export { cloneRng, testRng, testSeed, testSeeds };
24
+
25
+ //# sourceMappingURL=rng.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.js","names":[],"sources":["../src/rng.ts"],"sourcesContent":["// Deterministic random sources for tests, built on the kit's seeded generator.\n\nimport type { Rng, RngState } from '@joystyk/contracts';\nimport { restoreRng, seedState } from '@joystyk/kit';\n\n/** Every test seed is namespaced, so a test seed never equals a production deal or daily seed. */\nconst PREFIX = 'joystyk-test';\n\n/**\n * The seed state for a test. `testSeed('nim', 3)` hashes `\"joystyk-test/nim/3\"`. The same\n * parts give the same state on every machine and in every run.\n */\nexport const testSeed = (...parts: readonly (string | number)[]): RngState =>\n seedState([PREFIX, ...parts.map(String)].join('/'));\n\n/**\n * A seeded generator for a test, from the kit's SFC32. Pass the same parts to get the same\n * stream again, for example to reproduce a failure the suites report.\n */\nexport const testRng = (...parts: readonly (string | number)[]): Rng => restoreRng(testSeed(...parts));\n\n/** `count` distinct, reproducible seed states derived from `base`. */\nexport const testSeeds = (base: string, count: number): RngState[] =>\n Array.from({ length: count }, (_, i) => testSeed(base, i));\n\n/**\n * An independent copy of `rng` at its current position. Drawing from the copy leaves `rng`\n * untouched, which is how the suites try an input without consuming randomness.\n */\nexport const cloneRng = (rng: Rng): Rng => restoreRng(rng.save());\n"],"mappings":";;;AAMA,IAAM,SAAS;;;;;AAMf,IAAa,YAAY,GAAG,UAC1B,UAAU,CAAC,QAAQ,GAAG,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;;;;;AAMpD,IAAa,WAAW,GAAG,UAA6C,WAAW,SAAS,GAAG,KAAK,CAAC;;AAGrG,IAAa,aAAa,MAAc,UACtC,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,SAAS,MAAM,CAAC,CAAC;;;;;AAM3D,IAAa,YAAY,QAAkB,WAAW,IAAI,KAAK,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { StandardSchemaV1, Validator } from '@joystyk/contracts';
2
+ /** The outcome of validating one value. */
3
+ export type Validation<T> = {
4
+ readonly ok: true;
5
+ readonly value: T;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly issues: readonly StandardSchemaV1.Issue[];
9
+ };
10
+ /** Validates `value` with a Standard Schema, sync or async. */
11
+ export declare const validate: <T>(schema: Validator<T>, value: unknown) => Promise<Validation<T>>;
12
+ /** Issues as one line: "message at path; message". */
13
+ export declare const formatIssues: (issues: readonly StandardSchemaV1.Issue[]) => string;
package/dist/schema.js ADDED
@@ -0,0 +1,21 @@
1
+ //#region src/schema.ts
2
+ /** Validates `value` with a Standard Schema, sync or async. */
3
+ var validate = async (schema, value) => {
4
+ const result = await schema["~standard"].validate(value);
5
+ return result.issues === void 0 ? {
6
+ ok: true,
7
+ value: result.value
8
+ } : {
9
+ ok: false,
10
+ issues: result.issues
11
+ };
12
+ };
13
+ /** Issues as one line: "message at path; message". */
14
+ var formatIssues = (issues) => issues.slice(0, 3).map((issue) => {
15
+ const path = (issue.path ?? []).map((p) => String(typeof p === "object" ? p.key : p)).join(".");
16
+ return path === "" ? issue.message : `${issue.message} at ${path}`;
17
+ }).join("; ") + (issues.length > 3 ? ` (+${issues.length - 3} more)` : "");
18
+ //#endregion
19
+ export { formatIssues, validate };
20
+
21
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","names":[],"sources":["../src/schema.ts"],"sourcesContent":["// Running Standard Schemas from tests.\n\nimport type { StandardSchemaV1, Validator } from '@joystyk/contracts';\n\n/** The outcome of validating one value. */\nexport type Validation<T> =\n { readonly ok: true; readonly value: T } | { readonly ok: false; readonly issues: readonly StandardSchemaV1.Issue[] };\n\n/** Validates `value` with a Standard Schema, sync or async. */\nexport const validate = async <T>(schema: Validator<T>, value: unknown): Promise<Validation<T>> => {\n const result = await schema['~standard'].validate(value);\n return result.issues === undefined ? { ok: true, value: result.value } : { ok: false, issues: result.issues };\n};\n\n/** Issues as one line: \"message at path; message\". */\nexport const formatIssues = (issues: readonly StandardSchemaV1.Issue[]): string =>\n issues\n .slice(0, 3)\n .map((issue) => {\n const path = (issue.path ?? []).map((p) => String(typeof p === 'object' ? p.key : p)).join('.');\n return path === '' ? issue.message : `${issue.message} at ${path}`;\n })\n .join('; ') + (issues.length > 3 ? ` (+${issues.length - 3} more)` : '');\n"],"mappings":";;AASA,IAAa,WAAW,OAAU,QAAsB,UAA2C;CACjG,MAAM,SAAS,MAAM,OAAO,YAAY,CAAC,SAAS,KAAK;CACvD,OAAO,OAAO,WAAW,KAAA,IAAY;EAAE,IAAI;EAAM,OAAO,OAAO;CAAM,IAAI;EAAE,IAAI;EAAO,QAAQ,OAAO;CAAO;AAC9G;;AAGA,IAAa,gBAAgB,WAC3B,OACG,MAAM,GAAG,CAAC,CAAC,CACX,KAAK,UAAU;CACd,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAA,CAAG,KAAK,MAAM,OAAO,OAAO,MAAM,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;CAC9F,OAAO,SAAS,KAAK,MAAM,UAAU,GAAG,MAAM,QAAQ,MAAM;AAC9D,CAAC,CAAC,CACD,KAAK,IAAI,KAAK,OAAO,SAAS,IAAI,MAAM,OAAO,SAAS,EAAE,UAAU"}
@@ -0,0 +1,16 @@
1
+ import type { SaveEnvelope } from '@joystyk/contracts';
2
+ import type { Clock, SaveStore } from '@joystyk/session';
3
+ import type { FakeClock } from './clock.js';
4
+ /** The runner's `Clock` port, driven by a fake clock: time moves only on `advance`. */
5
+ export declare const runnerClock: (fake: FakeClock) => Clock;
6
+ /** A runner `SaveStore` in memory, with what a test needs to inspect it. */
7
+ export interface MemorySaveStore extends SaveStore {
8
+ /** What the slot holds: a JSON copy of the last write since the last `clear`, or null. */
9
+ slot(): SaveEnvelope | null;
10
+ /** How many writes landed. */
11
+ writes(): number;
12
+ /** How many times the runner cleared the slot. */
13
+ clears(): number;
14
+ }
15
+ /** An in-memory save slot, which the runner writes and clears as the launcher's does. */
16
+ export declare const createMemorySaveStore: () => MemorySaveStore;
@@ -0,0 +1,32 @@
1
+ //#region src/session-ports.ts
2
+ /** The runner's `Clock` port, driven by a fake clock: time moves only on `advance`. */
3
+ var runnerClock = (fake) => ({
4
+ now: () => fake.now(),
5
+ after: (ms, callback) => {
6
+ const timer = fake.setTimeout(callback, ms);
7
+ return () => timer.cancel();
8
+ }
9
+ });
10
+ /** An in-memory save slot, which the runner writes and clears as the launcher's does. */
11
+ var createMemorySaveStore = () => {
12
+ let slot = null;
13
+ let writes = 0;
14
+ let clears = 0;
15
+ return {
16
+ write: async (save) => {
17
+ slot = JSON.parse(JSON.stringify(save));
18
+ writes += 1;
19
+ },
20
+ clear: async () => {
21
+ slot = null;
22
+ clears += 1;
23
+ },
24
+ slot: () => slot,
25
+ writes: () => writes,
26
+ clears: () => clears
27
+ };
28
+ };
29
+ //#endregion
30
+ export { createMemorySaveStore, runnerClock };
31
+
32
+ //# sourceMappingURL=session-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-ports.js","names":[],"sources":["../src/session-ports.ts"],"sourcesContent":["// Fakes for the session runner's ports, so a game's tests can play it through\n// `createLocalSession` with time and saves under the test's control.\n\nimport type { SaveEnvelope } from '@joystyk/contracts';\nimport type { Clock, SaveStore } from '@joystyk/session';\nimport type { FakeClock } from './clock.ts';\n\n/** The runner's `Clock` port, driven by a fake clock: time moves only on `advance`. */\nexport const runnerClock = (fake: FakeClock): Clock => ({\n now: () => fake.now(),\n after: (ms, callback) => {\n const timer = fake.setTimeout(callback, ms);\n return () => timer.cancel();\n },\n});\n\n/** A runner `SaveStore` in memory, with what a test needs to inspect it. */\nexport interface MemorySaveStore extends SaveStore {\n /** What the slot holds: a JSON copy of the last write since the last `clear`, or null. */\n slot(): SaveEnvelope | null;\n /** How many writes landed. */\n writes(): number;\n /** How many times the runner cleared the slot. */\n clears(): number;\n}\n\n/** An in-memory save slot, which the runner writes and clears as the launcher's does. */\nexport const createMemorySaveStore = (): MemorySaveStore => {\n let slot: SaveEnvelope | null = null;\n let writes = 0;\n let clears = 0;\n return {\n write: async (save) => {\n slot = JSON.parse(JSON.stringify(save)) as SaveEnvelope;\n writes += 1;\n },\n clear: async () => {\n slot = null;\n clears += 1;\n },\n slot: () => slot,\n writes: () => writes,\n clears: () => clears,\n };\n};\n"],"mappings":";;AAQA,IAAa,eAAe,UAA4B;CACtD,WAAW,KAAK,IAAI;CACpB,QAAQ,IAAI,aAAa;EACvB,MAAM,QAAQ,KAAK,WAAW,UAAU,EAAE;EAC1C,aAAa,MAAM,OAAO;CAC5B;AACF;;AAaA,IAAa,8BAA+C;CAC1D,IAAI,OAA4B;CAChC,IAAI,SAAS;CACb,IAAI,SAAS;CACb,OAAO;EACL,OAAO,OAAO,SAAS;GACrB,OAAO,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC;GACtC,UAAU;EACZ;EACA,OAAO,YAAY;GACjB,OAAO;GACP,UAAU;EACZ;EACA,YAAY;EACZ,cAAc;EACd,cAAc;CAChB;AACF"}
@@ -0,0 +1,71 @@
1
+ import { type FixedStepSimulation, type Json, type Rng } from '@joystyk/contracts';
2
+ import type { InvariantResult } from './engine-conformance.js';
3
+ import { type ConformanceReport } from './report.js';
4
+ /** Options for `checkSimulation` and `runSimulationConformance`. */
5
+ export type SimulationConformanceOptions<State, Input, Config> = {
6
+ /** A name for reports and the vitest `describe` block. */
7
+ readonly name?: string;
8
+ /** The game id written into replays. Default "test-game". */
9
+ readonly gameId?: string;
10
+ /** The configs to start from; each runs `runs` times with different seeds. */
11
+ readonly configs: readonly Config[];
12
+ /** Runs per config. Default 4. */
13
+ readonly runs?: number;
14
+ /** Ticks per run (a run also stops at its outcome). Default 1000. */
15
+ readonly ticks?: number;
16
+ /** The base seed. Run `r` of config `c` uses the seed string `${seed}/${c}/${r}`. Default "conformance". */
17
+ readonly seed?: string;
18
+ /** Scripted inputs for a tick. Draw any randomness from `rng`. Default: no inputs. */
19
+ readonly inputs?: (state: State, tick: number, rng: Rng) => readonly Input[];
20
+ /** Called on every state the simulation reaches. */
21
+ readonly invariants?: (state: State) => InvariantResult;
22
+ /** Fail a run that has no outcome after `ticks` ticks. Default false. */
23
+ readonly requireOutcome?: boolean;
24
+ /** The tick of the checkpoint in the checkpoint-equivalence check. Default half the run. */
25
+ readonly checkpointAt?: number;
26
+ /** Fail when the mean step takes longer than this many milliseconds. Off by default. */
27
+ readonly stepBudgetMs?: number;
28
+ /** Run only one run, to reproduce a reported failure (`setup` is the config index). */
29
+ readonly only?: {
30
+ readonly setup: number;
31
+ readonly game: number;
32
+ };
33
+ /** Failures kept per check in the report. Default 3. */
34
+ readonly maxFailuresPerCheck?: number;
35
+ };
36
+ /** The checks this suite runs, in report order. */
37
+ export declare const SIMULATION_CHECKS: readonly [{
38
+ readonly id: "determinism";
39
+ readonly title: "Determinism: the same seed and tick inputs give the same states and events";
40
+ }, {
41
+ readonly id: "replay";
42
+ readonly title: "Replay: the recorded tick log reproduces the final state";
43
+ }, {
44
+ readonly id: "checkpoint";
45
+ readonly title: "Checkpoints: saving, restoring and continuing equals running straight through";
46
+ }, {
47
+ readonly id: "json";
48
+ readonly title: "JSON safety of configs, states, inputs, events and outcomes";
49
+ }, {
50
+ readonly id: "schemas";
51
+ readonly title: "Schema agreement: every value passes the simulation's own schemas";
52
+ }, {
53
+ readonly id: "purity";
54
+ readonly title: "Step purity: the same state, inputs and generator give the same next state, without mutation";
55
+ }, {
56
+ readonly id: "invariants";
57
+ readonly title: "Invariants hold on every state the simulation reaches";
58
+ }, {
59
+ readonly id: "outcome";
60
+ readonly title: "Outcomes are valid and detected in time";
61
+ }, {
62
+ readonly id: "budget";
63
+ readonly title: "Step-time budget";
64
+ }];
65
+ /** The id of one simulation check. */
66
+ export type SimulationCheckId = (typeof SIMULATION_CHECKS)[number]['id'];
67
+ /**
68
+ * Runs the Layer 3 conformance suite against `simulation` and returns a report. It never
69
+ * throws for a failing check. Use `runSimulationConformance` to get one vitest case per check.
70
+ */
71
+ export declare const checkSimulation: <State extends Json, Input extends Json, View, Event extends Json, Config extends Json>(simulation: FixedStepSimulation<State, Input, View, Event, Config>, options: SimulationConformanceOptions<State, Input, Config>) => Promise<ConformanceReport>;