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

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 (249) hide show
  1. package/CHANGELOG.md +323 -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 +250 -19
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/error-projection.ts +51 -0
  19. package/src/errors.ts +335 -48
  20. package/src/execute.ts +1577 -0
  21. package/src/fire.ts +1169 -0
  22. package/src/glob.ts +81 -0
  23. package/src/index.ts +526 -35
  24. package/src/internal/fork-ctx.ts +69 -0
  25. package/src/layer-projection.ts +193 -0
  26. package/src/layer.ts +43 -6
  27. package/src/observe.ts +361 -0
  28. package/src/path-scope.ts +66 -0
  29. package/src/path-security.ts +1 -1
  30. package/src/permits.ts +12 -0
  31. package/src/redaction/patterns.ts +6 -3
  32. package/src/resilience.ts +11 -11
  33. package/src/resource-config.ts +792 -0
  34. package/src/resource.ts +194 -0
  35. package/src/result.ts +18 -0
  36. package/src/run.ts +76 -0
  37. package/src/schedule-runtime.ts +689 -0
  38. package/src/schedule.ts +326 -0
  39. package/src/serialization.ts +92 -55
  40. package/src/signal-diagnostics.ts +633 -0
  41. package/src/signal-ref.ts +87 -0
  42. package/src/signal.ts +104 -0
  43. package/src/store/accessor-protocol.ts +56 -0
  44. package/src/store/index.ts +4 -0
  45. package/src/structured-examples.ts +248 -0
  46. package/src/surface-derivation.ts +91 -0
  47. package/src/surface-filter.ts +101 -0
  48. package/src/surface-versioning.ts +42 -0
  49. package/src/topo.ts +769 -67
  50. package/src/tracing.ts +345 -0
  51. package/src/trail-id-glob.ts +15 -0
  52. package/src/trail.ts +1168 -30
  53. package/src/trails/derive-trail.ts +842 -0
  54. package/src/trails/index.ts +9 -0
  55. package/src/trails/ingest.ts +139 -0
  56. package/src/trails-db.ts +206 -0
  57. package/src/transport-error-map.ts +174 -0
  58. package/src/type-utils.ts +87 -0
  59. package/src/types.ts +254 -12
  60. package/src/validate-established-topo.ts +69 -0
  61. package/src/validate-topo.ts +414 -56
  62. package/src/validation.ts +152 -4
  63. package/src/version-marker.ts +716 -0
  64. package/src/version-resolution.ts +308 -0
  65. package/src/version-runtime.ts +120 -0
  66. package/src/webhook.ts +278 -0
  67. package/src/workspace.ts +162 -1
  68. package/src/zod-wrappers.ts +72 -0
  69. package/.turbo/turbo-build.log +0 -1
  70. package/.turbo/turbo-lint.log +0 -3
  71. package/.turbo/turbo-typecheck.log +0 -1
  72. package/dist/adapters.d.ts +0 -39
  73. package/dist/adapters.d.ts.map +0 -1
  74. package/dist/adapters.js +0 -2
  75. package/dist/adapters.js.map +0 -1
  76. package/dist/blob-ref.d.ts +0 -20
  77. package/dist/blob-ref.d.ts.map +0 -1
  78. package/dist/blob-ref.js +0 -22
  79. package/dist/blob-ref.js.map +0 -1
  80. package/dist/branded.d.ts +0 -36
  81. package/dist/branded.d.ts.map +0 -1
  82. package/dist/branded.js +0 -89
  83. package/dist/branded.js.map +0 -1
  84. package/dist/collections.d.ts +0 -31
  85. package/dist/collections.d.ts.map +0 -1
  86. package/dist/collections.js +0 -60
  87. package/dist/collections.js.map +0 -1
  88. package/dist/context.d.ts +0 -10
  89. package/dist/context.d.ts.map +0 -1
  90. package/dist/context.js +0 -15
  91. package/dist/context.js.map +0 -1
  92. package/dist/derive.d.ts +0 -33
  93. package/dist/derive.d.ts.map +0 -1
  94. package/dist/derive.js +0 -125
  95. package/dist/derive.js.map +0 -1
  96. package/dist/errors.d.ts +0 -83
  97. package/dist/errors.d.ts.map +0 -1
  98. package/dist/errors.js +0 -142
  99. package/dist/errors.js.map +0 -1
  100. package/dist/event.d.ts +0 -45
  101. package/dist/event.d.ts.map +0 -1
  102. package/dist/event.js +0 -17
  103. package/dist/event.js.map +0 -1
  104. package/dist/fetch.d.ts +0 -15
  105. package/dist/fetch.d.ts.map +0 -1
  106. package/dist/fetch.js +0 -102
  107. package/dist/fetch.js.map +0 -1
  108. package/dist/guards.d.ts +0 -17
  109. package/dist/guards.d.ts.map +0 -1
  110. package/dist/guards.js +0 -25
  111. package/dist/guards.js.map +0 -1
  112. package/dist/health.d.ts +0 -18
  113. package/dist/health.d.ts.map +0 -1
  114. package/dist/health.js +0 -5
  115. package/dist/health.js.map +0 -1
  116. package/dist/hike.d.ts +0 -36
  117. package/dist/hike.d.ts.map +0 -1
  118. package/dist/hike.js +0 -20
  119. package/dist/hike.js.map +0 -1
  120. package/dist/index.d.ts +0 -36
  121. package/dist/index.d.ts.map +0 -1
  122. package/dist/index.js +0 -40
  123. package/dist/index.js.map +0 -1
  124. package/dist/job.d.ts +0 -24
  125. package/dist/job.d.ts.map +0 -1
  126. package/dist/job.js +0 -17
  127. package/dist/job.js.map +0 -1
  128. package/dist/layer.d.ts +0 -17
  129. package/dist/layer.d.ts.map +0 -1
  130. package/dist/layer.js +0 -21
  131. package/dist/layer.js.map +0 -1
  132. package/dist/path-security.d.ts +0 -28
  133. package/dist/path-security.d.ts.map +0 -1
  134. package/dist/path-security.js +0 -63
  135. package/dist/path-security.js.map +0 -1
  136. package/dist/patterns/bulk.d.ts +0 -15
  137. package/dist/patterns/bulk.d.ts.map +0 -1
  138. package/dist/patterns/bulk.js +0 -14
  139. package/dist/patterns/bulk.js.map +0 -1
  140. package/dist/patterns/change.d.ts +0 -10
  141. package/dist/patterns/change.d.ts.map +0 -1
  142. package/dist/patterns/change.js +0 -10
  143. package/dist/patterns/change.js.map +0 -1
  144. package/dist/patterns/date-range.d.ts +0 -10
  145. package/dist/patterns/date-range.d.ts.map +0 -1
  146. package/dist/patterns/date-range.js +0 -10
  147. package/dist/patterns/date-range.js.map +0 -1
  148. package/dist/patterns/index.d.ts +0 -9
  149. package/dist/patterns/index.d.ts.map +0 -1
  150. package/dist/patterns/index.js +0 -9
  151. package/dist/patterns/index.js.map +0 -1
  152. package/dist/patterns/pagination.d.ts +0 -18
  153. package/dist/patterns/pagination.d.ts.map +0 -1
  154. package/dist/patterns/pagination.js +0 -18
  155. package/dist/patterns/pagination.js.map +0 -1
  156. package/dist/patterns/progress.d.ts +0 -11
  157. package/dist/patterns/progress.d.ts.map +0 -1
  158. package/dist/patterns/progress.js +0 -11
  159. package/dist/patterns/progress.js.map +0 -1
  160. package/dist/patterns/sorting.d.ts +0 -13
  161. package/dist/patterns/sorting.d.ts.map +0 -1
  162. package/dist/patterns/sorting.js +0 -10
  163. package/dist/patterns/sorting.js.map +0 -1
  164. package/dist/patterns/status.d.ts +0 -15
  165. package/dist/patterns/status.d.ts.map +0 -1
  166. package/dist/patterns/status.js +0 -9
  167. package/dist/patterns/status.js.map +0 -1
  168. package/dist/patterns/timestamps.d.ts +0 -10
  169. package/dist/patterns/timestamps.d.ts.map +0 -1
  170. package/dist/patterns/timestamps.js +0 -10
  171. package/dist/patterns/timestamps.js.map +0 -1
  172. package/dist/redaction/index.d.ts +0 -4
  173. package/dist/redaction/index.d.ts.map +0 -1
  174. package/dist/redaction/index.js +0 -3
  175. package/dist/redaction/index.js.map +0 -1
  176. package/dist/redaction/patterns.d.ts +0 -9
  177. package/dist/redaction/patterns.d.ts.map +0 -1
  178. package/dist/redaction/patterns.js +0 -39
  179. package/dist/redaction/patterns.js.map +0 -1
  180. package/dist/redaction/redactor.d.ts +0 -27
  181. package/dist/redaction/redactor.d.ts.map +0 -1
  182. package/dist/redaction/redactor.js +0 -89
  183. package/dist/redaction/redactor.js.map +0 -1
  184. package/dist/resilience.d.ts +0 -34
  185. package/dist/resilience.d.ts.map +0 -1
  186. package/dist/resilience.js +0 -164
  187. package/dist/resilience.js.map +0 -1
  188. package/dist/result.d.ts +0 -57
  189. package/dist/result.d.ts.map +0 -1
  190. package/dist/result.js +0 -156
  191. package/dist/result.js.map +0 -1
  192. package/dist/serialization.d.ts +0 -27
  193. package/dist/serialization.d.ts.map +0 -1
  194. package/dist/serialization.js +0 -153
  195. package/dist/serialization.js.map +0 -1
  196. package/dist/topo.d.ts +0 -18
  197. package/dist/topo.d.ts.map +0 -1
  198. package/dist/topo.js +0 -80
  199. package/dist/topo.js.map +0 -1
  200. package/dist/trail.d.ts +0 -83
  201. package/dist/trail.d.ts.map +0 -1
  202. package/dist/trail.js +0 -16
  203. package/dist/trail.js.map +0 -1
  204. package/dist/types.d.ts +0 -46
  205. package/dist/types.d.ts.map +0 -1
  206. package/dist/types.js +0 -2
  207. package/dist/types.js.map +0 -1
  208. package/dist/validate-topo.d.ts +0 -24
  209. package/dist/validate-topo.d.ts.map +0 -1
  210. package/dist/validate-topo.js +0 -156
  211. package/dist/validate-topo.js.map +0 -1
  212. package/dist/validation.d.ts +0 -27
  213. package/dist/validation.d.ts.map +0 -1
  214. package/dist/validation.js +0 -134
  215. package/dist/validation.js.map +0 -1
  216. package/dist/workspace.d.ts +0 -25
  217. package/dist/workspace.d.ts.map +0 -1
  218. package/dist/workspace.js +0 -57
  219. package/dist/workspace.js.map +0 -1
  220. package/src/__tests__/blob-ref.test.ts +0 -103
  221. package/src/__tests__/branded.test.ts +0 -148
  222. package/src/__tests__/collections.test.ts +0 -126
  223. package/src/__tests__/context.test.ts +0 -66
  224. package/src/__tests__/derive.test.ts +0 -203
  225. package/src/__tests__/errors.test.ts +0 -309
  226. package/src/__tests__/event.test.ts +0 -82
  227. package/src/__tests__/fetch.test.ts +0 -217
  228. package/src/__tests__/guards.test.ts +0 -102
  229. package/src/__tests__/hike.test.ts +0 -117
  230. package/src/__tests__/job.test.ts +0 -98
  231. package/src/__tests__/layer.test.ts +0 -224
  232. package/src/__tests__/path-security.test.ts +0 -114
  233. package/src/__tests__/patterns.test.ts +0 -273
  234. package/src/__tests__/redaction.test.ts +0 -244
  235. package/src/__tests__/resilience.test.ts +0 -246
  236. package/src/__tests__/result.test.ts +0 -155
  237. package/src/__tests__/serialization.test.ts +0 -401
  238. package/src/__tests__/topo.test.ts +0 -220
  239. package/src/__tests__/trail.test.ts +0 -179
  240. package/src/__tests__/validate-topo.test.ts +0 -280
  241. package/src/__tests__/validation.test.ts +0 -283
  242. package/src/__tests__/workspace.test.ts +0 -183
  243. package/src/adapters.ts +0 -68
  244. package/src/event.ts +0 -77
  245. package/src/health.ts +0 -23
  246. package/src/hike.ts +0 -77
  247. package/src/job.ts +0 -20
  248. package/tsconfig.json +0 -9
  249. 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
- });