@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.30

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 (245) hide show
  1. package/CHANGELOG.md +305 -8
  2. package/README.md +114 -33
  3. package/package.json +11 -1
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +241 -16
  15. package/src/detours.ts +8 -0
  16. package/src/draft.ts +342 -0
  17. package/src/error-projection.ts +51 -0
  18. package/src/errors.ts +335 -48
  19. package/src/execute.ts +1577 -0
  20. package/src/fire.ts +1169 -0
  21. package/src/index.ts +495 -34
  22. package/src/internal/fork-ctx.ts +69 -0
  23. package/src/layer-projection.ts +193 -0
  24. package/src/layer.ts +43 -6
  25. package/src/observe.ts +361 -0
  26. package/src/path-security.ts +1 -1
  27. package/src/permits.ts +12 -0
  28. package/src/redaction/patterns.ts +6 -3
  29. package/src/resilience.ts +11 -11
  30. package/src/resource-config.ts +792 -0
  31. package/src/resource.ts +194 -0
  32. package/src/result.ts +18 -0
  33. package/src/run.ts +76 -0
  34. package/src/schedule-runtime.ts +689 -0
  35. package/src/schedule.ts +326 -0
  36. package/src/serialization.ts +92 -55
  37. package/src/signal-diagnostics.ts +633 -0
  38. package/src/signal-ref.ts +87 -0
  39. package/src/signal.ts +104 -0
  40. package/src/store/accessor-protocol.ts +56 -0
  41. package/src/store/index.ts +4 -0
  42. package/src/structured-examples.ts +248 -0
  43. package/src/surface-derivation.ts +91 -0
  44. package/src/surface-filter.ts +176 -0
  45. package/src/surface-versioning.ts +42 -0
  46. package/src/topo.ts +769 -67
  47. package/src/tracing.ts +345 -0
  48. package/src/trail.ts +1168 -30
  49. package/src/trails/derive-trail.ts +842 -0
  50. package/src/trails/index.ts +9 -0
  51. package/src/trails/ingest.ts +139 -0
  52. package/src/trails-db.ts +206 -0
  53. package/src/transport-error-map.ts +174 -0
  54. package/src/type-utils.ts +87 -0
  55. package/src/types.ts +254 -12
  56. package/src/validate-established-topo.ts +69 -0
  57. package/src/validate-topo.ts +393 -45
  58. package/src/validation.ts +152 -4
  59. package/src/version-marker.ts +716 -0
  60. package/src/version-resolution.ts +308 -0
  61. package/src/version-runtime.ts +120 -0
  62. package/src/webhook.ts +278 -0
  63. package/src/workspace.ts +1 -1
  64. package/src/zod-wrappers.ts +72 -0
  65. package/.turbo/turbo-build.log +0 -1
  66. package/.turbo/turbo-lint.log +0 -3
  67. package/.turbo/turbo-typecheck.log +0 -1
  68. package/dist/adapters.d.ts +0 -39
  69. package/dist/adapters.d.ts.map +0 -1
  70. package/dist/adapters.js +0 -2
  71. package/dist/adapters.js.map +0 -1
  72. package/dist/blob-ref.d.ts +0 -20
  73. package/dist/blob-ref.d.ts.map +0 -1
  74. package/dist/blob-ref.js +0 -22
  75. package/dist/blob-ref.js.map +0 -1
  76. package/dist/branded.d.ts +0 -36
  77. package/dist/branded.d.ts.map +0 -1
  78. package/dist/branded.js +0 -89
  79. package/dist/branded.js.map +0 -1
  80. package/dist/collections.d.ts +0 -31
  81. package/dist/collections.d.ts.map +0 -1
  82. package/dist/collections.js +0 -60
  83. package/dist/collections.js.map +0 -1
  84. package/dist/context.d.ts +0 -10
  85. package/dist/context.d.ts.map +0 -1
  86. package/dist/context.js +0 -15
  87. package/dist/context.js.map +0 -1
  88. package/dist/derive.d.ts +0 -33
  89. package/dist/derive.d.ts.map +0 -1
  90. package/dist/derive.js +0 -125
  91. package/dist/derive.js.map +0 -1
  92. package/dist/errors.d.ts +0 -83
  93. package/dist/errors.d.ts.map +0 -1
  94. package/dist/errors.js +0 -142
  95. package/dist/errors.js.map +0 -1
  96. package/dist/event.d.ts +0 -45
  97. package/dist/event.d.ts.map +0 -1
  98. package/dist/event.js +0 -17
  99. package/dist/event.js.map +0 -1
  100. package/dist/fetch.d.ts +0 -15
  101. package/dist/fetch.d.ts.map +0 -1
  102. package/dist/fetch.js +0 -102
  103. package/dist/fetch.js.map +0 -1
  104. package/dist/guards.d.ts +0 -17
  105. package/dist/guards.d.ts.map +0 -1
  106. package/dist/guards.js +0 -25
  107. package/dist/guards.js.map +0 -1
  108. package/dist/health.d.ts +0 -18
  109. package/dist/health.d.ts.map +0 -1
  110. package/dist/health.js +0 -5
  111. package/dist/health.js.map +0 -1
  112. package/dist/hike.d.ts +0 -36
  113. package/dist/hike.d.ts.map +0 -1
  114. package/dist/hike.js +0 -20
  115. package/dist/hike.js.map +0 -1
  116. package/dist/index.d.ts +0 -36
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -40
  119. package/dist/index.js.map +0 -1
  120. package/dist/job.d.ts +0 -24
  121. package/dist/job.d.ts.map +0 -1
  122. package/dist/job.js +0 -17
  123. package/dist/job.js.map +0 -1
  124. package/dist/layer.d.ts +0 -17
  125. package/dist/layer.d.ts.map +0 -1
  126. package/dist/layer.js +0 -21
  127. package/dist/layer.js.map +0 -1
  128. package/dist/path-security.d.ts +0 -28
  129. package/dist/path-security.d.ts.map +0 -1
  130. package/dist/path-security.js +0 -63
  131. package/dist/path-security.js.map +0 -1
  132. package/dist/patterns/bulk.d.ts +0 -15
  133. package/dist/patterns/bulk.d.ts.map +0 -1
  134. package/dist/patterns/bulk.js +0 -14
  135. package/dist/patterns/bulk.js.map +0 -1
  136. package/dist/patterns/change.d.ts +0 -10
  137. package/dist/patterns/change.d.ts.map +0 -1
  138. package/dist/patterns/change.js +0 -10
  139. package/dist/patterns/change.js.map +0 -1
  140. package/dist/patterns/date-range.d.ts +0 -10
  141. package/dist/patterns/date-range.d.ts.map +0 -1
  142. package/dist/patterns/date-range.js +0 -10
  143. package/dist/patterns/date-range.js.map +0 -1
  144. package/dist/patterns/index.d.ts +0 -9
  145. package/dist/patterns/index.d.ts.map +0 -1
  146. package/dist/patterns/index.js +0 -9
  147. package/dist/patterns/index.js.map +0 -1
  148. package/dist/patterns/pagination.d.ts +0 -18
  149. package/dist/patterns/pagination.d.ts.map +0 -1
  150. package/dist/patterns/pagination.js +0 -18
  151. package/dist/patterns/pagination.js.map +0 -1
  152. package/dist/patterns/progress.d.ts +0 -11
  153. package/dist/patterns/progress.d.ts.map +0 -1
  154. package/dist/patterns/progress.js +0 -11
  155. package/dist/patterns/progress.js.map +0 -1
  156. package/dist/patterns/sorting.d.ts +0 -13
  157. package/dist/patterns/sorting.d.ts.map +0 -1
  158. package/dist/patterns/sorting.js +0 -10
  159. package/dist/patterns/sorting.js.map +0 -1
  160. package/dist/patterns/status.d.ts +0 -15
  161. package/dist/patterns/status.d.ts.map +0 -1
  162. package/dist/patterns/status.js +0 -9
  163. package/dist/patterns/status.js.map +0 -1
  164. package/dist/patterns/timestamps.d.ts +0 -10
  165. package/dist/patterns/timestamps.d.ts.map +0 -1
  166. package/dist/patterns/timestamps.js +0 -10
  167. package/dist/patterns/timestamps.js.map +0 -1
  168. package/dist/redaction/index.d.ts +0 -4
  169. package/dist/redaction/index.d.ts.map +0 -1
  170. package/dist/redaction/index.js +0 -3
  171. package/dist/redaction/index.js.map +0 -1
  172. package/dist/redaction/patterns.d.ts +0 -9
  173. package/dist/redaction/patterns.d.ts.map +0 -1
  174. package/dist/redaction/patterns.js +0 -39
  175. package/dist/redaction/patterns.js.map +0 -1
  176. package/dist/redaction/redactor.d.ts +0 -27
  177. package/dist/redaction/redactor.d.ts.map +0 -1
  178. package/dist/redaction/redactor.js +0 -89
  179. package/dist/redaction/redactor.js.map +0 -1
  180. package/dist/resilience.d.ts +0 -34
  181. package/dist/resilience.d.ts.map +0 -1
  182. package/dist/resilience.js +0 -164
  183. package/dist/resilience.js.map +0 -1
  184. package/dist/result.d.ts +0 -57
  185. package/dist/result.d.ts.map +0 -1
  186. package/dist/result.js +0 -156
  187. package/dist/result.js.map +0 -1
  188. package/dist/serialization.d.ts +0 -27
  189. package/dist/serialization.d.ts.map +0 -1
  190. package/dist/serialization.js +0 -153
  191. package/dist/serialization.js.map +0 -1
  192. package/dist/topo.d.ts +0 -18
  193. package/dist/topo.d.ts.map +0 -1
  194. package/dist/topo.js +0 -80
  195. package/dist/topo.js.map +0 -1
  196. package/dist/trail.d.ts +0 -83
  197. package/dist/trail.d.ts.map +0 -1
  198. package/dist/trail.js +0 -16
  199. package/dist/trail.js.map +0 -1
  200. package/dist/types.d.ts +0 -46
  201. package/dist/types.d.ts.map +0 -1
  202. package/dist/types.js +0 -2
  203. package/dist/types.js.map +0 -1
  204. package/dist/validate-topo.d.ts +0 -24
  205. package/dist/validate-topo.d.ts.map +0 -1
  206. package/dist/validate-topo.js +0 -156
  207. package/dist/validate-topo.js.map +0 -1
  208. package/dist/validation.d.ts +0 -27
  209. package/dist/validation.d.ts.map +0 -1
  210. package/dist/validation.js +0 -134
  211. package/dist/validation.js.map +0 -1
  212. package/dist/workspace.d.ts +0 -25
  213. package/dist/workspace.d.ts.map +0 -1
  214. package/dist/workspace.js +0 -57
  215. package/dist/workspace.js.map +0 -1
  216. package/src/__tests__/blob-ref.test.ts +0 -103
  217. package/src/__tests__/branded.test.ts +0 -148
  218. package/src/__tests__/collections.test.ts +0 -126
  219. package/src/__tests__/context.test.ts +0 -66
  220. package/src/__tests__/derive.test.ts +0 -203
  221. package/src/__tests__/errors.test.ts +0 -309
  222. package/src/__tests__/event.test.ts +0 -82
  223. package/src/__tests__/fetch.test.ts +0 -217
  224. package/src/__tests__/guards.test.ts +0 -102
  225. package/src/__tests__/hike.test.ts +0 -117
  226. package/src/__tests__/job.test.ts +0 -98
  227. package/src/__tests__/layer.test.ts +0 -224
  228. package/src/__tests__/path-security.test.ts +0 -114
  229. package/src/__tests__/patterns.test.ts +0 -273
  230. package/src/__tests__/redaction.test.ts +0 -244
  231. package/src/__tests__/resilience.test.ts +0 -246
  232. package/src/__tests__/result.test.ts +0 -155
  233. package/src/__tests__/serialization.test.ts +0 -401
  234. package/src/__tests__/topo.test.ts +0 -220
  235. package/src/__tests__/trail.test.ts +0 -179
  236. package/src/__tests__/validate-topo.test.ts +0 -280
  237. package/src/__tests__/validation.test.ts +0 -283
  238. package/src/__tests__/workspace.test.ts +0 -183
  239. package/src/adapters.ts +0 -68
  240. package/src/event.ts +0 -77
  241. package/src/health.ts +0 -23
  242. package/src/hike.ts +0 -77
  243. package/src/job.ts +0 -20
  244. package/tsconfig.json +0 -9
  245. package/tsconfig.tsbuildinfo +0 -1
@@ -1,179 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { Result } from '../result';
6
- import { trail } from '../trail';
7
- import type { TrailContext } from '../types';
8
-
9
- const stubCtx: TrailContext = {
10
- requestId: 'test-123',
11
- signal: AbortSignal.timeout(5000),
12
- };
13
-
14
- describe('trail()', () => {
15
- const inputSchema = z.object({ name: z.string() });
16
- const outputSchema = z.object({ greeting: z.string() });
17
-
18
- const greet = trail('greet', {
19
- description: 'Greet someone',
20
- implementation: (input) => Result.ok({ greeting: `Hello, ${input.name}!` }),
21
- input: inputSchema,
22
- output: outputSchema,
23
- });
24
-
25
- describe('basics', () => {
26
- test('returns correct id', () => {
27
- expect(greet.id).toBe('greet');
28
- });
29
-
30
- test("returns kind 'trail'", () => {
31
- expect(greet.kind).toBe('trail');
32
- });
33
-
34
- test('preserves input schema', () => {
35
- const parsed = greet.input.safeParse({ name: 'World' });
36
- expect(parsed.success).toBe(true);
37
-
38
- const bad = greet.input.safeParse({ name: 42 });
39
- expect(bad.success).toBe(false);
40
- });
41
-
42
- test('output schema is optional', () => {
43
- const minimal = trail('noop', {
44
- implementation: () => Result.ok(),
45
- input: z.object({}),
46
- });
47
- expect(minimal.output).toBeUndefined();
48
- });
49
-
50
- test('implementation is callable', async () => {
51
- const result = await greet.implementation({ name: 'World' }, stubCtx);
52
- expect(result.isOk()).toBe(true);
53
- expect(result.unwrap()).toEqual({ greeting: 'Hello, World!' });
54
- });
55
- });
56
-
57
- describe('metadata', () => {
58
- test('examples are stored', () => {
59
- const withExamples = trail('echo', {
60
- examples: [
61
- { error: 'ValidationError', input: { text: '' }, name: 'error-case' },
62
- { expected: { text: 'hi' }, input: { text: 'hi' }, name: 'basic' },
63
- ],
64
- implementation: (input) => Result.ok({ text: input.text }),
65
- input: z.object({ text: z.string() }),
66
- });
67
- expect(withExamples.examples).toHaveLength(2);
68
- const first = withExamples.examples?.[0];
69
- expect(first?.name).toBe('error-case');
70
- const second = withExamples.examples?.[1];
71
- expect(second?.name).toBe('basic');
72
- });
73
-
74
- test('markers are stored', () => {
75
- const withMarkers = trail('tagged', {
76
- implementation: () => Result.ok(),
77
- input: z.object({}),
78
- markers: { domain: 'billing', tier: 1 },
79
- });
80
- expect(withMarkers.markers).toEqual({ domain: 'billing', tier: 1 });
81
- });
82
-
83
- test('detours are stored', () => {
84
- const withDetours = trail('orchestrator', {
85
- detours: {
86
- onFailure: ['alert'],
87
- onSuccess: ['notify', 'audit'],
88
- },
89
- implementation: () => Result.ok(),
90
- input: z.object({}),
91
- });
92
- expect(withDetours.detours).toEqual({
93
- onFailure: ['alert'],
94
- onSuccess: ['notify', 'audit'],
95
- });
96
- });
97
- });
98
-
99
- describe('boolean flags', () => {
100
- test('boolean flags default to undefined', () => {
101
- const minimal = trail('bare', {
102
- implementation: () => Result.ok(),
103
- input: z.object({}),
104
- });
105
- expect(minimal.readOnly).toBeUndefined();
106
- expect(minimal.destructive).toBeUndefined();
107
- expect(minimal.idempotent).toBeUndefined();
108
- });
109
-
110
- test('boolean flags are preserved when set', () => {
111
- const withFlags = trail('flagged', {
112
- destructive: false,
113
- idempotent: true,
114
- implementation: () => Result.ok(),
115
- input: z.object({}),
116
- readOnly: true,
117
- });
118
- expect(withFlags.readOnly).toBe(true);
119
- expect(withFlags.destructive).toBe(false);
120
- expect(withFlags.idempotent).toBe(true);
121
- });
122
- });
123
-
124
- describe('single-object overload', () => {
125
- test('accepts spec with id property', () => {
126
- const t = trail({
127
- id: 'entity.show',
128
- implementation: (input: { name: string }, _ctx: TrailContext) =>
129
- Result.ok({ greeting: `Hi, ${input.name}` }),
130
- input: inputSchema,
131
- });
132
- expect(t.id).toBe('entity.show');
133
- expect(t.kind).toBe('trail');
134
- });
135
-
136
- test('preserves all spec fields', () => {
137
- const t = trail({
138
- description: 'A full trail',
139
- destructive: false,
140
- examples: [{ input: { name: 'World' }, name: 'test' }],
141
- id: 'full',
142
- implementation: (input: { name: string }, _ctx: TrailContext) =>
143
- Result.ok({ greeting: `Hi, ${input.name}` }),
144
- input: inputSchema,
145
- output: outputSchema,
146
- readOnly: true,
147
- });
148
- expect(t.description).toBe('A full trail');
149
- expect(t.readOnly).toBe(true);
150
- expect(t.examples).toHaveLength(1);
151
- });
152
-
153
- test('implementation is callable', async () => {
154
- const t = trail({
155
- id: 'callable',
156
- implementation: (input: { x: number }) => Result.ok(input.x * 2),
157
- input: z.object({ x: z.number() }),
158
- });
159
- const result = await t.implementation({ x: 5 }, stubCtx);
160
- expect(result.isOk()).toBe(true);
161
- expect(result.unwrap()).toBe(10);
162
- });
163
-
164
- test('sync implementations are normalized to an awaitable runtime function', async () => {
165
- const t = trail('normalized', {
166
- implementation: (input: { value: number }) =>
167
- Result.ok(input.value + 1),
168
- input: z.object({ value: z.number() }),
169
- });
170
-
171
- const promise = t.implementation({ value: 2 }, stubCtx);
172
- expect(promise).toBeInstanceOf(Promise);
173
-
174
- const result = await promise;
175
- expect(result.isOk()).toBe(true);
176
- expect(result.unwrap()).toBe(3);
177
- });
178
- });
179
- });
@@ -1,280 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { Result } from '../result.js';
6
- import { topo } from '../topo.js';
7
- import type { TopoIssue } from '../validate-topo.js';
8
- import { validateTopo } from '../validate-topo.js';
9
-
10
- // ---------------------------------------------------------------------------
11
- // Helpers
12
- // ---------------------------------------------------------------------------
13
-
14
- // oxlint-disable-next-line require-await -- satisfies async interface
15
- const noop = async () => Result.ok();
16
-
17
- const mockTrail = (
18
- id: string,
19
- overrides?: {
20
- examples?: readonly {
21
- name: string;
22
- input: unknown;
23
- expected?: unknown;
24
- error?: string;
25
- }[];
26
- output?: z.ZodType;
27
- }
28
- ) => ({
29
- id,
30
- implementation: noop,
31
- input: z.object({ name: z.string() }),
32
- kind: 'trail' as const,
33
- ...overrides,
34
- });
35
-
36
- const mockHike = (id: string, follows: readonly string[]) => ({
37
- follows,
38
- id,
39
- implementation: noop,
40
- input: z.object({ q: z.string() }),
41
- kind: 'hike' as const,
42
- });
43
-
44
- const mockEvent = (id: string, from?: readonly string[]) => ({
45
- from,
46
- id,
47
- kind: 'event' as const,
48
- payload: z.object({ data: z.string() }),
49
- });
50
-
51
- /** Extract issues from a failed validateTopo result. */
52
- const extractIssues = (result: Result<void, Error>): TopoIssue[] => {
53
- if (result.isOk()) {
54
- return [];
55
- }
56
- const ctx = (result.error as { context?: { issues?: TopoIssue[] } }).context;
57
- return (ctx?.issues ?? []) as TopoIssue[];
58
- };
59
-
60
- // ---------------------------------------------------------------------------
61
- // Tests
62
- // ---------------------------------------------------------------------------
63
-
64
- describe('validateTopo', () => {
65
- test('valid topo passes', () => {
66
- const app = topo('app', {
67
- add: mockTrail('entity.add'),
68
- onboard: mockHike('entity.onboard', ['entity.add']),
69
- updated: mockEvent('entity.updated', ['entity.add']),
70
- });
71
-
72
- const result = validateTopo(app);
73
- expect(result.isOk()).toBe(true);
74
- });
75
-
76
- describe('hike follows', () => {
77
- test('hike following non-existent trail fails', () => {
78
- const app = topo('app', {
79
- onboard: mockHike('entity.onboard', ['entity.missing']),
80
- });
81
-
82
- const result = validateTopo(app);
83
- expect(result.isErr()).toBe(true);
84
-
85
- const issues = extractIssues(result);
86
- expect(issues).toHaveLength(1);
87
- expect(issues[0]?.rule).toBe('follows-exist');
88
- expect(issues[0]?.message).toContain('entity.missing');
89
- });
90
-
91
- test('hike following itself fails', () => {
92
- const app = topo('app', {
93
- loop: mockHike('entity.loop', ['entity.loop']),
94
- });
95
-
96
- const result = validateTopo(app);
97
- expect(result.isErr()).toBe(true);
98
-
99
- const issues = extractIssues(result);
100
- expect(issues.some((i) => i.rule === 'no-self-follow')).toBe(true);
101
- });
102
-
103
- test('two-node cycle (a→b→a) is detected', () => {
104
- const app = topo('app', {
105
- a: mockHike('a', ['b']),
106
- b: mockHike('b', ['a']),
107
- });
108
-
109
- const result = validateTopo(app);
110
- expect(result.isErr()).toBe(true);
111
-
112
- const issues = extractIssues(result);
113
- const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
114
- expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
115
- expect(cycleIssues[0]?.message).toContain('Cycle detected');
116
- });
117
-
118
- test('three-node cycle (a→b→c→a) is detected', () => {
119
- const app = topo('app', {
120
- a: mockHike('a', ['b']),
121
- b: mockHike('b', ['c']),
122
- c: mockHike('c', ['a']),
123
- });
124
-
125
- const result = validateTopo(app);
126
- expect(result.isErr()).toBe(true);
127
-
128
- const issues = extractIssues(result);
129
- const cycleIssues = issues.filter((i) => i.rule === 'follow-cycle');
130
- expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
131
- expect(cycleIssues[0]?.message).toContain('Cycle detected');
132
- });
133
-
134
- test('valid DAG with shared targets is not flagged', () => {
135
- const app = topo('app', {
136
- a: mockHike('a', ['c']),
137
- b: mockHike('b', ['c']),
138
- c: mockHike('c', []),
139
- });
140
-
141
- const result = validateTopo(app);
142
- expect(result.isOk()).toBe(true);
143
- });
144
- });
145
-
146
- describe('example validation', () => {
147
- test('example with invalid input fails', () => {
148
- const app = topo('app', {
149
- show: mockTrail('entity.show', {
150
- examples: [{ input: { name: 123 }, name: 'Bad input' }],
151
- }),
152
- });
153
-
154
- const result = validateTopo(app);
155
- expect(result.isErr()).toBe(true);
156
-
157
- const issues = extractIssues(result);
158
- expect(issues).toHaveLength(1);
159
- expect(issues[0]?.rule).toBe('example-input-valid');
160
- expect(issues[0]?.message).toContain('Bad input');
161
- });
162
-
163
- test('example with expected output but no output schema warns', () => {
164
- const app = topo('app', {
165
- show: mockTrail('entity.show', {
166
- examples: [
167
- {
168
- expected: { result: 'ok' },
169
- input: { name: 'test' },
170
- name: 'Has expected',
171
- },
172
- ],
173
- }),
174
- });
175
-
176
- const result = validateTopo(app);
177
- expect(result.isErr()).toBe(true);
178
-
179
- const issues = extractIssues(result);
180
- expect(issues).toHaveLength(1);
181
- expect(issues[0]?.rule).toBe('output-schema-present');
182
- });
183
-
184
- test('ValidationError example with invalid input is allowed', () => {
185
- const app = topo('app', {
186
- show: mockTrail('entity.show', {
187
- examples: [
188
- {
189
- error: 'ValidationError',
190
- input: { name: 123 },
191
- name: 'Error case',
192
- },
193
- ],
194
- }),
195
- });
196
-
197
- const result = validateTopo(app);
198
- expect(result.isOk()).toBe(true);
199
- });
200
-
201
- test('NotFoundError example with invalid input fails', () => {
202
- const app = topo('app', {
203
- show: mockTrail('entity.show', {
204
- examples: [
205
- {
206
- error: 'NotFoundError',
207
- input: { name: 123 },
208
- name: 'Not found case',
209
- },
210
- ],
211
- }),
212
- });
213
-
214
- const result = validateTopo(app);
215
- expect(result.isErr()).toBe(true);
216
-
217
- const issues = extractIssues(result);
218
- expect(issues).toHaveLength(1);
219
- expect(issues[0]?.rule).toBe('example-input-valid');
220
- });
221
-
222
- test('NotFoundError example with valid input passes', () => {
223
- const app = topo('app', {
224
- show: mockTrail('entity.show', {
225
- examples: [
226
- {
227
- error: 'NotFoundError',
228
- input: { name: 'test' },
229
- name: 'Not found case',
230
- },
231
- ],
232
- }),
233
- });
234
-
235
- const result = validateTopo(app);
236
- expect(result.isOk()).toBe(true);
237
- });
238
- });
239
-
240
- describe('event origins', () => {
241
- test('event with non-existent origin fails', () => {
242
- const app = topo('app', {
243
- updated: mockEvent('entity.updated', ['entity.ghost']),
244
- });
245
-
246
- const result = validateTopo(app);
247
- expect(result.isErr()).toBe(true);
248
-
249
- const issues = extractIssues(result);
250
- expect(issues).toHaveLength(1);
251
- expect(issues[0]?.rule).toBe('event-origin-exists');
252
- expect(issues[0]?.message).toContain('entity.ghost');
253
- });
254
-
255
- test('event without origins is accepted', () => {
256
- const app = topo('app', {
257
- updated: mockEvent('entity.updated'),
258
- });
259
-
260
- const result = validateTopo(app);
261
- expect(result.isOk()).toBe(true);
262
- });
263
- });
264
-
265
- test('collects multiple issues', () => {
266
- const app = topo('app', {
267
- broken: mockHike('entity.broken', ['entity.missing']),
268
- show: mockTrail('entity.show', {
269
- examples: [{ input: { name: 123 }, name: 'Bad' }],
270
- }),
271
- updated: mockEvent('entity.updated', ['entity.ghost']),
272
- });
273
-
274
- const result = validateTopo(app);
275
- expect(result.isErr()).toBe(true);
276
-
277
- const issues = extractIssues(result);
278
- expect(issues).toHaveLength(3);
279
- });
280
- });