@ontrails/core 1.0.0-beta.4 → 1.0.0-beta.41

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