@ontrails/testing 1.0.0-beta.12 → 1.0.0-beta.13

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 (43) hide show
  1. package/CHANGELOG.md +58 -27
  2. package/README.md +9 -9
  3. package/dist/context.d.ts +11 -11
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js +23 -21
  6. package/dist/context.js.map +1 -1
  7. package/dist/contracts.js +8 -8
  8. package/dist/contracts.js.map +1 -1
  9. package/dist/crosses.d.ts +38 -0
  10. package/dist/crosses.d.ts.map +1 -0
  11. package/dist/crosses.js +213 -0
  12. package/dist/crosses.js.map +1 -0
  13. package/dist/examples.d.ts +4 -4
  14. package/dist/examples.js +31 -31
  15. package/dist/examples.js.map +1 -1
  16. package/dist/harness-mcp.d.ts +1 -1
  17. package/dist/harness-mcp.js +2 -2
  18. package/dist/harness-mcp.js.map +1 -1
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/trail.js +1 -1
  24. package/dist/trail.js.map +1 -1
  25. package/dist/types.d.ts +8 -8
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/all.test.ts +20 -18
  29. package/src/__tests__/context.test.ts +27 -27
  30. package/src/__tests__/contracts.test.ts +29 -29
  31. package/src/__tests__/{follows.test.ts → crosses.test.ts} +193 -195
  32. package/src/__tests__/detours.test.ts +3 -3
  33. package/src/__tests__/examples.test.ts +145 -144
  34. package/src/__tests__/trail.test.ts +4 -4
  35. package/src/context.ts +36 -32
  36. package/src/contracts.ts +12 -12
  37. package/src/{follows.ts → crosses.ts} +97 -92
  38. package/src/examples.ts +43 -43
  39. package/src/harness-mcp.ts +2 -2
  40. package/src/index.ts +6 -6
  41. package/src/trail.ts +1 -1
  42. package/src/types.ts +9 -9
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -1,64 +1,66 @@
1
1
  import { describe } from 'bun:test';
2
2
 
3
- import { Result, service, trail, topo } from '@ontrails/core';
3
+ import { Result, provision, trail, topo } from '@ontrails/core';
4
4
  import { z } from 'zod';
5
5
 
6
6
  import { testAll } from '../all.js';
7
7
 
8
- const mockDbService = service('db.mock.all', {
8
+ const mockDbProvision = provision('db.mock.all', {
9
9
  create: () => Result.ok({ source: 'factory' }),
10
10
  mock: () => ({ source: 'mock' }),
11
11
  });
12
12
 
13
- const mockedTrail = trail('service.mocked.all', {
14
- description: 'Trail that uses a mocked service through testAll',
13
+ const mockedTrail = trail('provision.mocked.all', {
14
+ blaze: (_input, ctx) =>
15
+ Result.ok({ source: mockDbProvision.from(ctx).source }),
16
+ description: 'Trail that uses a mocked provision through testAll',
15
17
  examples: [
16
18
  {
17
19
  expected: { source: 'mock' },
18
20
  input: {},
19
- name: 'Uses auto-resolved service mock',
21
+ name: 'Uses auto-resolved provision mock',
20
22
  },
21
23
  ],
22
24
  input: z.object({}),
23
25
  output: z.object({ source: z.string() }),
24
- run: (_input, ctx) => Result.ok({ source: mockDbService.from(ctx).source }),
25
- services: [mockDbService],
26
+ provisions: [mockDbProvision],
26
27
  });
27
28
 
28
- const overrideTrail = trail('service.override.all', {
29
+ const overrideTrail = trail('provision.override.all', {
30
+ blaze: (_input, ctx) =>
31
+ Result.ok({ source: mockDbProvision.from(ctx).source }),
29
32
  description: 'Trail that prefers explicit overrides over mock factories',
30
33
  examples: [
31
34
  {
32
35
  expected: { source: 'override' },
33
36
  input: {},
34
- name: 'Explicit service override wins',
37
+ name: 'Explicit provision override wins',
35
38
  },
36
39
  ],
37
40
  input: z.object({}),
38
41
  output: z.object({ source: z.string() }),
39
- run: (_input, ctx) => Result.ok({ source: mockDbService.from(ctx).source }),
40
- services: [mockDbService],
42
+ provisions: [mockDbProvision],
41
43
  });
42
44
 
43
- describe('testAll service mocks', () => {
45
+ describe('testAll provision mocks', () => {
44
46
  // eslint-disable-next-line jest/require-hook
45
47
  testAll(
46
- topo('test-all-service-mock-app', {
47
- mockDbService,
48
+ topo('test-all-provision-mock-app', {
49
+ mockDbProvision,
48
50
  mockedTrail,
49
51
  } as Record<string, unknown>)
50
52
  );
51
53
  });
52
54
 
53
- describe('testAll explicit service overrides', () => {
55
+ describe('testAll explicit provision overrides', () => {
54
56
  // eslint-disable-next-line jest/require-hook
55
57
  testAll(
56
- topo('test-all-service-override-app', {
57
- mockDbService,
58
+ topo('test-all-provision-override-app', {
59
+ mockDbProvision,
58
60
  overrideTrail,
59
61
  } as Record<string, unknown>),
60
62
  {
61
- services: { 'db.mock.all': { source: 'override' } },
63
+ provisions: { 'db.mock.all': { source: 'override' } },
62
64
  }
63
65
  );
64
66
  });
@@ -1,12 +1,12 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
- import { Result, service, topo } from '@ontrails/core';
3
+ import { Result, provision, topo } from '@ontrails/core';
4
4
 
5
5
  import {
6
- createFollowContext,
6
+ createCrossContext,
7
7
  createTestContext,
8
8
  mergeTestContext,
9
- resolveMockServices,
9
+ resolveMockProvisions,
10
10
  } from '../context.js';
11
11
  import type { TestLogger } from '../types.js';
12
12
 
@@ -19,8 +19,8 @@ describe('createTestContext', () => {
19
19
  const ctx = createTestContext();
20
20
  expect(ctx.cwd).toBe(process.cwd());
21
21
  expect(ctx.requestId).toBe('test-request-001');
22
- expect(ctx.signal).toBeDefined();
23
- expect(ctx.signal.aborted).toBe(false);
22
+ expect(ctx.abortSignal).toBeDefined();
23
+ expect(ctx.abortSignal.aborted).toBe(false);
24
24
  expect(ctx.logger).toBeDefined();
25
25
  expect(ctx.workspaceRoot).toBe(process.cwd());
26
26
  });
@@ -46,11 +46,11 @@ describe('createTestContext', () => {
46
46
  expect(ctx.requestId).toBe('custom-id');
47
47
  });
48
48
 
49
- test('overrides signal', () => {
49
+ test('overrides abortSignal', () => {
50
50
  const controller = new AbortController();
51
51
  controller.abort();
52
- const ctx = createTestContext({ signal: controller.signal });
53
- expect(ctx.signal.aborted).toBe(true);
52
+ const ctx = createTestContext({ abortSignal: controller.signal });
53
+ expect(ctx.abortSignal.aborted).toBe(true);
54
54
  });
55
55
 
56
56
  test('overrides cwd', () => {
@@ -68,41 +68,41 @@ describe('createTestContext', () => {
68
68
  });
69
69
  });
70
70
 
71
- describe('createFollowContext', () => {
71
+ describe('createCrossContext', () => {
72
72
  test('returns configured ok response for registered id', async () => {
73
- const follow = createFollowContext({
73
+ const cross = createCrossContext({
74
74
  responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
75
75
  });
76
- const result = await follow('entity.add', { name: 'Alpha' });
76
+ const result = await cross('entity.add', { name: 'Alpha' });
77
77
  expect(result.unwrap()).toEqual({ id: '1', name: 'Alpha' });
78
78
  });
79
79
 
80
80
  test('returns configured err response for registered id', async () => {
81
81
  const err = new Error('conflict');
82
- const follow = createFollowContext({
82
+ const cross = createCrossContext({
83
83
  responses: { 'entity.add': Result.err(err) },
84
84
  });
85
- const result = await follow('entity.add', {});
85
+ const result = await cross('entity.add', {});
86
86
  expect(result.isErr()).toBe(true);
87
87
  expect(result.error?.message).toBe('conflict');
88
88
  });
89
89
 
90
90
  test('returns err with descriptive message for unregistered id', async () => {
91
- const follow = createFollowContext();
92
- const result = await follow('unknown.trail', {});
91
+ const cross = createCrossContext();
92
+ const result = await cross('unknown.trail', {});
93
93
  expect(result.isErr()).toBe(true);
94
94
  expect(result.error?.message).toContain('unknown.trail');
95
95
  });
96
96
 
97
97
  test('no options defaults to empty responses', async () => {
98
- const follow = createFollowContext();
99
- const result = await follow('any.id', {});
98
+ const cross = createCrossContext();
99
+ const result = await cross('any.id', {});
100
100
  expect(result.isErr()).toBe(true);
101
101
  });
102
102
  });
103
103
 
104
104
  describe('mergeTestContext', () => {
105
- test('merges service overrides into extensions and the service accessor', () => {
105
+ test('merges provision overrides into extensions and the provision accessor', () => {
106
106
  const ctx = mergeTestContext(
107
107
  {
108
108
  extensions: { existing: true },
@@ -114,14 +114,14 @@ describe('mergeTestContext', () => {
114
114
  'db.main': { source: 'mock' },
115
115
  existing: true,
116
116
  });
117
- expect(ctx.service<{ source: string }>('db.main').source).toBe('mock');
117
+ expect(ctx.provision<{ source: string }>('db.main').source).toBe('mock');
118
118
  });
119
119
  });
120
120
 
121
- describe('resolveMockServices', () => {
122
- test('creates fresh mock services for each invocation', async () => {
121
+ describe('resolveMockProvisions', () => {
122
+ test('creates fresh mock provisions for each invocation', async () => {
123
123
  let mockCalls = 0;
124
- const mockable = service(`service.mock.${Bun.randomUUIDv7()}`, {
124
+ const mockable = provision(`provision.mock.${Bun.randomUUIDv7()}`, {
125
125
  create: () => Result.ok({ source: 'factory' }),
126
126
  mock: () => {
127
127
  mockCalls += 1;
@@ -130,8 +130,8 @@ describe('resolveMockServices', () => {
130
130
  });
131
131
  const app = topo('mock-app', { mockable } as Record<string, unknown>);
132
132
 
133
- const first = await resolveMockServices(app);
134
- const second = await resolveMockServices(app);
133
+ const first = await resolveMockProvisions(app);
134
+ const second = await resolveMockProvisions(app);
135
135
 
136
136
  expect(first).toEqual({ [mockable.id]: { source: 'mock' } });
137
137
  expect(second).toEqual(first);
@@ -139,12 +139,12 @@ describe('resolveMockServices', () => {
139
139
  expect(mockCalls).toBe(2);
140
140
  });
141
141
 
142
- test('skips services without mock factories', async () => {
143
- const plain = service(`service.plain.${Bun.randomUUIDv7()}`, {
142
+ test('skips provisions without mock factories', async () => {
143
+ const plain = provision(`provision.plain.${Bun.randomUUIDv7()}`, {
144
144
  create: () => Result.ok({ source: 'factory' }),
145
145
  });
146
146
  const app = topo('plain-app', { plain } as Record<string, unknown>);
147
147
 
148
- expect(await resolveMockServices(app)).toEqual({});
148
+ expect(await resolveMockProvisions(app)).toEqual({});
149
149
  });
150
150
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, test } from 'bun:test';
2
2
 
3
- import { Result, service, trail, topo } from '@ontrails/core';
3
+ import { Result, provision, trail, topo } from '@ontrails/core';
4
4
  import { z } from 'zod';
5
5
 
6
6
  import { testContracts } from '../contracts.js';
@@ -11,6 +11,7 @@ import { testContracts } from '../contracts.js';
11
11
 
12
12
  /** Trail whose implementation matches the output schema. */
13
13
  const validTrail = trail('valid', {
14
+ blaze: (input: { name: string }) => Result.ok({ id: 1, name: input.name }),
14
15
  examples: [
15
16
  {
16
17
  expected: { id: 1, name: 'Alpha' },
@@ -20,21 +21,20 @@ const validTrail = trail('valid', {
20
21
  ],
21
22
  input: z.object({ name: z.string() }),
22
23
  output: z.object({ id: z.number(), name: z.string() }),
23
- run: (input: { name: string }) => Result.ok({ id: 1, name: input.name }),
24
24
  });
25
25
 
26
26
  /** Trail without output schema -- should be skipped. */
27
27
  const noSchemaTrail = trail('noschema', {
28
+ blaze: (input: { x: number }) => Result.ok(input.x * 2),
28
29
  examples: [{ expected: 10, input: { x: 5 }, name: 'No schema' }],
29
30
  input: z.object({ x: z.number() }),
30
- run: (input: { x: number }) => Result.ok(input.x * 2),
31
31
  });
32
32
 
33
33
  /** Trail without examples -- should be skipped. */
34
34
  const noExamplesTrail = trail('noexamples', {
35
+ blaze: (input: { x: number }) => Result.ok({ value: input.x }),
35
36
  input: z.object({ x: z.number() }),
36
37
  output: z.object({ value: z.number() }),
37
- run: (input: { x: number }) => Result.ok({ value: input.x }),
38
38
  });
39
39
 
40
40
  // ---------------------------------------------------------------------------
@@ -43,6 +43,9 @@ const noExamplesTrail = trail('noexamples', {
43
43
 
44
44
  /** Composition trail whose implementation matches the output schema. */
45
45
  const compositionTrail = trail('composition.valid', {
46
+ blaze: (input: { a: number; b: number }) =>
47
+ Result.ok({ total: input.a + input.b }),
48
+ crosses: ['valid'],
46
49
  examples: [
47
50
  {
48
51
  expected: { total: 3 },
@@ -50,14 +53,12 @@ const compositionTrail = trail('composition.valid', {
50
53
  name: 'Valid composition output',
51
54
  },
52
55
  ],
53
- follow: ['valid'],
54
56
  input: z.object({ a: z.number(), b: z.number() }),
55
57
  output: z.object({ total: z.number() }),
56
- run: (input: { a: number; b: number }) =>
57
- Result.ok({ total: input.a + input.b }),
58
58
  });
59
59
 
60
60
  const transformedInputTrail = trail('contract.transformed', {
61
+ blaze: (input: { value: number }) => Result.ok({ value: input.value }),
61
62
  examples: [
62
63
  {
63
64
  expected: { value: 2 },
@@ -69,49 +70,48 @@ const transformedInputTrail = trail('contract.transformed', {
69
70
  .object({ value: z.string() })
70
71
  .transform(({ value }) => ({ value: Number(value) + 1 })),
71
72
  output: z.object({ value: z.number() }),
72
- run: (input: { value: number }) => Result.ok({ value: input.value }),
73
73
  });
74
74
 
75
- const undeclaredContractDbService = service('db.undeclared.contracts', {
75
+ const undeclaredContractDbProvision = provision('db.undeclared.contracts', {
76
76
  create: () => Result.ok({ source: 'factory' }),
77
77
  mock: () => ({ source: 'mock' }),
78
78
  });
79
79
 
80
- const undeclaredContractTrail = trail('service.undeclared.contracts', {
80
+ const undeclaredContractTrail = trail('provision.undeclared.contracts', {
81
+ blaze: (_input, ctx) =>
82
+ Result.ok({
83
+ hasInjectedProvision:
84
+ ctx.extensions?.[undeclaredContractDbProvision.id] !== undefined,
85
+ }),
81
86
  examples: [
82
87
  {
83
- expected: { hasInjectedService: false },
88
+ expected: { hasInjectedProvision: false },
84
89
  input: {},
85
- name: 'Undeclared services are not preloaded into contract contexts',
90
+ name: 'Undeclared provisions are not preloaded into contract contexts',
86
91
  },
87
92
  ],
88
93
  input: z.object({}),
89
- output: z.object({ hasInjectedService: z.literal(false) }),
90
- run: (_input, ctx) =>
91
- Result.ok({
92
- hasInjectedService:
93
- ctx.extensions?.[undeclaredContractDbService.id] !== undefined,
94
- }),
94
+ output: z.object({ hasInjectedProvision: z.literal(false) }),
95
95
  });
96
96
 
97
- const ctxOverrideContractService = service('db.mock.contracts', {
97
+ const ctxOverrideContractProvision = provision('db.mock.contracts', {
98
98
  create: () => Result.ok({ source: 'factory' }),
99
99
  mock: () => ({ source: 'mock' }),
100
100
  });
101
101
 
102
- const ctxOverrideContractTrail = trail('service.ctx.contracts', {
102
+ const ctxOverrideContractTrail = trail('provision.ctx.contracts', {
103
+ blaze: (_input, ctx) =>
104
+ Result.ok({ source: ctxOverrideContractProvision.from(ctx).source }),
103
105
  examples: [
104
106
  {
105
107
  expected: { source: 'ctx' },
106
108
  input: {},
107
- name: 'Context extensions beat auto-resolved contract mocks',
109
+ name: 'Context extensions beat auto-resolved contract provision mocks',
108
110
  },
109
111
  ],
110
112
  input: z.object({}),
111
113
  output: z.object({ source: z.literal('ctx') }),
112
- run: (_input, ctx) =>
113
- Result.ok({ source: ctxOverrideContractService.from(ctx).source }),
114
- services: [ctxOverrideContractService],
114
+ provisions: [ctxOverrideContractProvision],
115
115
  });
116
116
 
117
117
  // ---------------------------------------------------------------------------
@@ -143,7 +143,7 @@ describe('testContracts: skips trails without examples', () => {
143
143
  });
144
144
  });
145
145
 
146
- describe('testContracts: validates composition trail output schemas', () => {
146
+ describe('testContracts: validates output schemas for trails with crossings', () => {
147
147
  // eslint-disable-next-line jest/require-hook
148
148
  testContracts(
149
149
  topo('test-app', { compositionTrail } as Record<string, unknown>)
@@ -163,7 +163,7 @@ describe('testContracts: context extension overrides', () => {
163
163
  // eslint-disable-next-line jest/require-hook
164
164
  testContracts(
165
165
  topo('ctx-contract-app', {
166
- ctxOverrideContractService,
166
+ ctxOverrideContractProvision,
167
167
  ctxOverrideContractTrail,
168
168
  } as Record<string, unknown>),
169
169
  {
@@ -174,11 +174,11 @@ describe('testContracts: context extension overrides', () => {
174
174
  );
175
175
  });
176
176
 
177
- describe('testContracts service declarations', () => {
177
+ describe('testContracts provision declarations', () => {
178
178
  // eslint-disable-next-line jest/require-hook
179
179
  testContracts(
180
- topo('undeclared-contract-service-app', {
181
- undeclaredContractDbService,
180
+ topo('undeclared-contract-provision-app', {
181
+ undeclaredContractDbProvision,
182
182
  undeclaredContractTrail,
183
183
  } as Record<string, unknown>)
184
184
  );