@ontrails/testing 1.0.0-beta.14 → 1.0.0-beta.15

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 (83) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +11 -11
  4. package/dist/all.d.ts +10 -5
  5. package/dist/all.d.ts.map +1 -1
  6. package/dist/all.js +78 -26
  7. package/dist/all.js.map +1 -1
  8. package/dist/assertions.d.ts +23 -0
  9. package/dist/assertions.d.ts.map +1 -1
  10. package/dist/assertions.js +154 -0
  11. package/dist/assertions.js.map +1 -1
  12. package/dist/context.d.ts +17 -16
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +31 -20
  15. package/dist/context.js.map +1 -1
  16. package/dist/contracts.d.ts.map +1 -1
  17. package/dist/contracts.js +9 -5
  18. package/dist/contracts.js.map +1 -1
  19. package/dist/crosses.d.ts +4 -4
  20. package/dist/crosses.d.ts.map +1 -1
  21. package/dist/crosses.js +49 -39
  22. package/dist/crosses.js.map +1 -1
  23. package/dist/detours.d.ts +5 -4
  24. package/dist/detours.d.ts.map +1 -1
  25. package/dist/detours.js +93 -14
  26. package/dist/detours.js.map +1 -1
  27. package/dist/effective-examples.d.ts +30 -0
  28. package/dist/effective-examples.d.ts.map +1 -0
  29. package/dist/effective-examples.js +227 -0
  30. package/dist/effective-examples.js.map +1 -0
  31. package/dist/examples.d.ts +1 -1
  32. package/dist/examples.d.ts.map +1 -1
  33. package/dist/examples.js +79 -41
  34. package/dist/examples.js.map +1 -1
  35. package/dist/harness-cli.d.ts +3 -3
  36. package/dist/harness-cli.d.ts.map +1 -1
  37. package/dist/harness-cli.js +16 -11
  38. package/dist/harness-cli.js.map +1 -1
  39. package/dist/harness-mcp.d.ts +3 -3
  40. package/dist/harness-mcp.d.ts.map +1 -1
  41. package/dist/harness-mcp.js +9 -8
  42. package/dist/harness-mcp.js.map +1 -1
  43. package/dist/index.d.ts +6 -4
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +5 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/scenario.d.ts +37 -0
  48. package/dist/scenario.d.ts.map +1 -0
  49. package/dist/scenario.js +235 -0
  50. package/dist/scenario.js.map +1 -0
  51. package/dist/types.d.ts +38 -5
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +7 -6
  54. package/src/__tests__/all.test.ts +143 -24
  55. package/src/__tests__/context.test.ts +32 -12
  56. package/src/__tests__/contracts.test.ts +72 -18
  57. package/src/__tests__/crosses.test.ts +78 -78
  58. package/src/__tests__/detours.test.ts +176 -19
  59. package/src/__tests__/effective-examples.test.ts +203 -0
  60. package/src/__tests__/examples.test.ts +152 -50
  61. package/src/__tests__/harness-cli.test.ts +28 -4
  62. package/src/__tests__/harness-mcp.test.ts +37 -0
  63. package/src/__tests__/partial-match.test.ts +126 -0
  64. package/src/__tests__/scenario.test.ts +381 -0
  65. package/src/all.ts +149 -12
  66. package/src/assertions.ts +253 -0
  67. package/src/context.ts +64 -38
  68. package/src/contracts.ts +14 -8
  69. package/src/crosses.ts +93 -51
  70. package/src/detours.ts +155 -18
  71. package/src/effective-examples.ts +350 -0
  72. package/src/examples.ts +127 -59
  73. package/src/harness-cli.ts +19 -11
  74. package/src/harness-mcp.ts +9 -8
  75. package/src/index.ts +13 -3
  76. package/src/scenario.ts +370 -0
  77. package/src/types.ts +63 -5
  78. package/tsconfig.tests.json +10 -0
  79. package/tsconfig.tsbuildinfo +1 -1
  80. package/dist/follows.d.ts +0 -38
  81. package/dist/follows.d.ts.map +0 -1
  82. package/dist/follows.js +0 -212
  83. package/dist/follows.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/testing",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.15",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -14,13 +14,14 @@
14
14
  "clean": "rm -rf dist *.tsbuildinfo"
15
15
  },
16
16
  "devDependencies": {
17
- "@ontrails/store": "^1.0.0-beta.13"
17
+ "@ontrails/drizzle": "^1.0.0-beta.14",
18
+ "@ontrails/store": "^1.0.0-beta.14"
18
19
  },
19
20
  "peerDependencies": {
20
- "@ontrails/cli": "^1.0.0-beta.13",
21
- "@ontrails/core": "^1.0.0-beta.13",
22
- "@ontrails/logging": "^1.0.0-beta.13",
23
- "@ontrails/mcp": "^1.0.0-beta.13",
21
+ "@ontrails/cli": "^1.0.0-beta.14",
22
+ "@ontrails/core": "^1.0.0-beta.14",
23
+ "@ontrails/logging": "^1.0.0-beta.14",
24
+ "@ontrails/mcp": "^1.0.0-beta.14",
24
25
  "zod": "^4.3.5"
25
26
  }
26
27
  }
@@ -1,11 +1,25 @@
1
- import { describe } from 'bun:test';
1
+ import { afterAll, describe, expect, mock, test } from 'bun:test';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
2
4
 
3
- import { Result, trail, topo } from '@ontrails/core';
5
+ import { contour, Result, trail, topo } from '@ontrails/core';
6
+ import { connectDrizzle } from '@ontrails/drizzle';
4
7
  import { z } from 'zod';
5
8
 
6
9
  import { testAll } from '../all.js';
7
10
  import { store as defineStore } from '@ontrails/store';
8
- import { connectDrizzle } from '@ontrails/store/drizzle';
11
+
12
+ const requireContourExample = (
13
+ contourDef: { examples?: readonly Record<string, unknown>[] },
14
+ index: number
15
+ ) => {
16
+ const example = contourDef.examples?.[index];
17
+ expect(example).toBeDefined();
18
+ if (!example) {
19
+ throw new Error(`Expected contour example at index ${index}`);
20
+ }
21
+ return example;
22
+ };
9
23
 
10
24
  const dbDefinition = defineStore({
11
25
  entities: {
@@ -26,7 +40,7 @@ const dbDefinition = defineStore({
26
40
  },
27
41
  });
28
42
 
29
- const createDbProvision = (
43
+ const createDbResource = (
30
44
  seed?: readonly {
31
45
  readonly id: string;
32
46
  readonly name: string;
@@ -40,7 +54,7 @@ const createDbProvision = (
40
54
  });
41
55
 
42
56
  const createOverrideStore = () => {
43
- const { mock } = createDbProvision([
57
+ const { mock: mockFactory } = createDbResource([
44
58
  {
45
59
  id: 'seed-1',
46
60
  name: 'Override',
@@ -48,11 +62,11 @@ const createOverrideStore = () => {
48
62
  },
49
63
  ]);
50
64
 
51
- if (mock === undefined) {
65
+ if (mockFactory === undefined) {
52
66
  throw new Error('Expected drizzle test store to expose a mock factory');
53
67
  }
54
68
 
55
- const created = mock();
69
+ const created = mockFactory();
56
70
  if (created instanceof Promise) {
57
71
  throw new TypeError(
58
72
  'Expected drizzle test store mock to resolve synchronously'
@@ -62,11 +76,11 @@ const createOverrideStore = () => {
62
76
  return created;
63
77
  };
64
78
 
65
- const mockDbProvision = createDbProvision();
79
+ const mockDbResource = createDbResource();
66
80
 
67
- const mockedTrail = trail('provision.mocked.all', {
81
+ const mockedTrail = trail('resource.mocked.all', {
68
82
  blaze: async (_input, ctx) => {
69
- const entity = await mockDbProvision.from(ctx).entities.get('seed-1');
83
+ const entity = await mockDbResource.from(ctx).entities.get('seed-1');
70
84
  if (entity === null) {
71
85
  return Result.err(new Error('expected seeded entity to exist'));
72
86
  }
@@ -74,22 +88,22 @@ const mockedTrail = trail('provision.mocked.all', {
74
88
  return Result.ok({ name: entity.name, source: entity.source });
75
89
  },
76
90
  description:
77
- 'Trail that uses a mocked connector-bound provision through testAll',
91
+ 'Trail that uses a mocked connector-bound resource through testAll',
78
92
  examples: [
79
93
  {
80
94
  expected: { name: 'Alpha', source: 'mock' },
81
95
  input: {},
82
- name: 'Uses auto-resolved provision mock',
96
+ name: 'Uses auto-resolved resource mock',
83
97
  },
84
98
  ],
85
99
  input: z.object({}),
86
100
  output: z.object({ name: z.string(), source: z.string() }),
87
- provisions: [mockDbProvision],
101
+ resources: [mockDbResource],
88
102
  });
89
103
 
90
- const overrideTrail = trail('provision.override.all', {
104
+ const overrideTrail = trail('resource.override.all', {
91
105
  blaze: async (_input, ctx) => {
92
- const entity = await mockDbProvision.from(ctx).entities.get('seed-1');
106
+ const entity = await mockDbResource.from(ctx).entities.get('seed-1');
93
107
  if (entity === null) {
94
108
  return Result.err(new Error('expected overridden entity to exist'));
95
109
  }
@@ -101,35 +115,140 @@ const overrideTrail = trail('provision.override.all', {
101
115
  {
102
116
  expected: { name: 'Override', source: 'override' },
103
117
  input: {},
104
- name: 'Explicit provision override wins',
118
+ name: 'Explicit resource override wins',
105
119
  },
106
120
  ],
107
121
  input: z.object({}),
108
122
  output: z.object({ name: z.string(), source: z.string() }),
109
- provisions: [mockDbProvision],
123
+ resources: [mockDbResource],
110
124
  });
111
125
 
112
- describe('testAll provision mocks', () => {
126
+ const contourFixture = contour(
127
+ 'allFixture',
128
+ {
129
+ id: z.string().uuid(),
130
+ name: z.string(),
131
+ },
132
+ {
133
+ examples: [
134
+ {
135
+ id: 'f46b837e-6c8d-42ec-8539-536f4e6daf0e',
136
+ name: 'Contour-derived governance fixture',
137
+ },
138
+ ],
139
+ identity: 'id',
140
+ }
141
+ );
142
+
143
+ const contourDerivedBlaze = mock(() =>
144
+ Result.ok(requireContourExample(contourFixture, 0))
145
+ );
146
+
147
+ const contourDerivedTrail = trail('contour.derived.all', {
148
+ blaze: () => contourDerivedBlaze(),
149
+ contours: [contourFixture],
150
+ description: 'Trail that relies on contour-derived fixtures inside testAll',
151
+ input: z.object({ id: contourFixture.shape.id }),
152
+ output: contourFixture,
153
+ });
154
+
155
+ const repoTempDir = (): string =>
156
+ join(
157
+ resolve(import.meta.dir, '../..'),
158
+ '.tmp-tests',
159
+ `test-all-established-${Date.now()}-${Math.random().toString(36).slice(2)}`
160
+ );
161
+
162
+ const runGeneratedGovernanceSuite = (
163
+ helperName: 'testAll' | 'testAllEstablished'
164
+ ): { readonly exitCode: number; readonly output: string } => {
165
+ const dir = repoTempDir();
166
+ const testFile = join(dir, `${helperName}.test.ts`);
167
+
168
+ mkdirSync(dir, { recursive: true });
169
+ writeFileSync(
170
+ testFile,
171
+ `import { Result, trail, topo } from '@ontrails/core';
172
+ import { ${helperName} } from '../../src/index.ts';
173
+ import { z } from 'zod';
174
+
175
+ const draftTrail = trail('_draft.entity.prepare', {
176
+ blaze: async () => Result.ok({ ok: true }),
177
+ input: z.object({}),
178
+ output: z.object({ ok: z.boolean() }),
179
+ });
180
+
181
+ ${helperName}(topo('draft-topo', { draftTrail }));
182
+ `
183
+ );
184
+
185
+ try {
186
+ const proc = Bun.spawnSync({
187
+ cmd: ['bun', 'test', testFile, '--bail'],
188
+ cwd: resolve(import.meta.dir, '..', '..', '..'),
189
+ stderr: 'pipe',
190
+ stdout: 'pipe',
191
+ });
192
+
193
+ return {
194
+ exitCode: proc.exitCode,
195
+ output: `${proc.stdout.toString()}\n${proc.stderr.toString()}`,
196
+ };
197
+ } finally {
198
+ rmSync(dir, { force: true, recursive: true });
199
+ }
200
+ };
201
+
202
+ describe('testAll resource mocks', () => {
113
203
  // eslint-disable-next-line jest/require-hook
114
204
  testAll(
115
- topo('test-all-provision-mock-app', {
116
- mockDbProvision,
205
+ topo('test-all-resource-mock-app', {
206
+ mockDbResource,
117
207
  mockedTrail,
118
208
  } as Record<string, unknown>)
119
209
  );
120
210
  });
121
211
 
122
- describe('testAll explicit provision overrides', () => {
212
+ describe('testAll explicit resource overrides', () => {
123
213
  // eslint-disable-next-line jest/require-hook
124
214
  testAll(
125
- topo('test-all-provision-override-app', {
126
- mockDbProvision,
215
+ topo('test-all-resource-override-app', {
216
+ mockDbResource,
127
217
  overrideTrail,
128
218
  } as Record<string, unknown>),
129
219
  () => ({
130
- provisions: {
220
+ resources: {
131
221
  'db.mock.all': createOverrideStore(),
132
222
  },
133
223
  })
134
224
  );
135
225
  });
226
+
227
+ describe('testAll contour-derived fixtures', () => {
228
+ // eslint-disable-next-line jest/require-hook
229
+ testAll(
230
+ topo('test-all-contour-derived-app', {
231
+ contourDerivedTrail,
232
+ contourFixture,
233
+ } as Record<string, unknown>)
234
+ );
235
+
236
+ afterAll(() => {
237
+ expect(contourDerivedBlaze).toHaveBeenCalledTimes(2);
238
+ });
239
+ });
240
+
241
+ describe('testAllEstablished draft hygiene', () => {
242
+ test('keeps testAll draft-friendly for in-progress graphs', () => {
243
+ const result = runGeneratedGovernanceSuite('testAll');
244
+
245
+ expect(result.exitCode).toBe(0);
246
+ });
247
+
248
+ test('fails draft-contaminated graphs under testAllEstablished', () => {
249
+ const result = runGeneratedGovernanceSuite('testAllEstablished');
250
+
251
+ expect(result.exitCode).toBe(1);
252
+ expect(result.output).toContain('_draft.entity.prepare');
253
+ });
254
+ });
@@ -1,12 +1,12 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
- import { Result, provision, topo } from '@ontrails/core';
3
+ import { Result, resource, topo } from '@ontrails/core';
4
4
 
5
5
  import {
6
6
  createCrossContext,
7
7
  createTestContext,
8
8
  mergeTestContext,
9
- resolveMockProvisions,
9
+ createMockResources,
10
10
  } from '../context.js';
11
11
  import type { TestLogger } from '../types.js';
12
12
 
@@ -22,6 +22,7 @@ describe('createTestContext', () => {
22
22
  expect(ctx.abortSignal).toBeDefined();
23
23
  expect(ctx.abortSignal.aborted).toBe(false);
24
24
  expect(ctx.logger).toBeDefined();
25
+ expect(ctx.trace).toBeDefined();
25
26
  expect(ctx.workspaceRoot).toBe(process.cwd());
26
27
  });
27
28
 
@@ -66,6 +67,25 @@ describe('createTestContext', () => {
66
67
  const env = ctx['env'] as Record<string, string>;
67
68
  expect(env).toEqual({ CUSTOM: '1', NODE_ENV: 'test' });
68
69
  });
70
+
71
+ test('default trace passthrough resolves the callback result', async () => {
72
+ const ctx = createTestContext();
73
+ const result = await ctx.trace('test-span', () => Promise.resolve('ok'));
74
+ expect(result).toBe('ok');
75
+ });
76
+
77
+ test('overrides trace', async () => {
78
+ const calls: string[] = [];
79
+ const ctx = createTestContext({
80
+ trace: async (label, fn) => {
81
+ calls.push(label);
82
+ return await fn();
83
+ },
84
+ });
85
+
86
+ await ctx.trace('custom-span', () => Promise.resolve('done'));
87
+ expect(calls).toEqual(['custom-span']);
88
+ });
69
89
  });
70
90
 
71
91
  describe('createCrossContext', () => {
@@ -102,7 +122,7 @@ describe('createCrossContext', () => {
102
122
  });
103
123
 
104
124
  describe('mergeTestContext', () => {
105
- test('merges provision overrides into extensions and the provision accessor', () => {
125
+ test('merges resource overrides into extensions and the resource accessor', () => {
106
126
  const ctx = mergeTestContext(
107
127
  {
108
128
  extensions: { existing: true },
@@ -114,14 +134,14 @@ describe('mergeTestContext', () => {
114
134
  'db.main': { source: 'mock' },
115
135
  existing: true,
116
136
  });
117
- expect(ctx.provision<{ source: string }>('db.main').source).toBe('mock');
137
+ expect(ctx.resource<{ source: string }>('db.main').source).toBe('mock');
118
138
  });
119
139
  });
120
140
 
121
- describe('resolveMockProvisions', () => {
122
- test('creates fresh mock provisions for each invocation', async () => {
141
+ describe('createMockResources', () => {
142
+ test('creates fresh mock resources for each invocation', async () => {
123
143
  let mockCalls = 0;
124
- const mockable = provision(`provision.mock.${Bun.randomUUIDv7()}`, {
144
+ const mockable = resource(`resource.mock.${Bun.randomUUIDv7()}`, {
125
145
  create: () => Result.ok({ source: 'factory' }),
126
146
  mock: () => {
127
147
  mockCalls += 1;
@@ -130,8 +150,8 @@ describe('resolveMockProvisions', () => {
130
150
  });
131
151
  const app = topo('mock-app', { mockable } as Record<string, unknown>);
132
152
 
133
- const first = await resolveMockProvisions(app);
134
- const second = await resolveMockProvisions(app);
153
+ const first = await createMockResources(app);
154
+ const second = await createMockResources(app);
135
155
 
136
156
  expect(first).toEqual({ [mockable.id]: { source: 'mock' } });
137
157
  expect(second).toEqual(first);
@@ -139,12 +159,12 @@ describe('resolveMockProvisions', () => {
139
159
  expect(mockCalls).toBe(2);
140
160
  });
141
161
 
142
- test('skips provisions without mock factories', async () => {
143
- const plain = provision(`provision.plain.${Bun.randomUUIDv7()}`, {
162
+ test('skips resources without mock factories', async () => {
163
+ const plain = resource(`resource.plain.${Bun.randomUUIDv7()}`, {
144
164
  create: () => Result.ok({ source: 'factory' }),
145
165
  });
146
166
  const app = topo('plain-app', { plain } as Record<string, unknown>);
147
167
 
148
- expect(await resolveMockProvisions(app)).toEqual({});
168
+ expect(await createMockResources(app)).toEqual({});
149
169
  });
150
170
  });
@@ -1,10 +1,22 @@
1
- import { describe, test } from 'bun:test';
1
+ import { afterAll, describe, expect, mock, test } from 'bun:test';
2
2
 
3
- import { Result, provision, trail, topo } from '@ontrails/core';
3
+ import { contour, Result, resource, trail, topo } from '@ontrails/core';
4
4
  import { z } from 'zod';
5
5
 
6
6
  import { testContracts } from '../contracts.js';
7
7
 
8
+ const requireContourExample = (
9
+ contourDef: { examples?: readonly Record<string, unknown>[] },
10
+ index: number
11
+ ) => {
12
+ const example = contourDef.examples?.[index];
13
+ expect(example).toBeDefined();
14
+ if (!example) {
15
+ throw new Error(`Expected contour example at index ${index}`);
16
+ }
17
+ return example;
18
+ };
19
+
8
20
  // ---------------------------------------------------------------------------
9
21
  // Test trails
10
22
  // ---------------------------------------------------------------------------
@@ -72,46 +84,74 @@ const transformedInputTrail = trail('contract.transformed', {
72
84
  output: z.object({ value: z.number() }),
73
85
  });
74
86
 
75
- const undeclaredContractDbProvision = provision('db.undeclared.contracts', {
87
+ const undeclaredContractDbResource = resource('db.undeclared.contracts', {
76
88
  create: () => Result.ok({ source: 'factory' }),
77
89
  mock: () => ({ source: 'mock' }),
78
90
  });
79
91
 
80
- const undeclaredContractTrail = trail('provision.undeclared.contracts', {
92
+ const undeclaredContractTrail = trail('resource.undeclared.contracts', {
81
93
  blaze: (_input, ctx) =>
82
94
  Result.ok({
83
- hasInjectedProvision:
84
- ctx.extensions?.[undeclaredContractDbProvision.id] !== undefined,
95
+ hasInjectedResource:
96
+ ctx.extensions?.[undeclaredContractDbResource.id] !== undefined,
85
97
  }),
86
98
  examples: [
87
99
  {
88
- expected: { hasInjectedProvision: false },
100
+ expected: { hasInjectedResource: false },
89
101
  input: {},
90
- name: 'Undeclared provisions are not preloaded into contract contexts',
102
+ name: 'Undeclared resources are not preloaded into contract contexts',
91
103
  },
92
104
  ],
93
105
  input: z.object({}),
94
- output: z.object({ hasInjectedProvision: z.literal(false) }),
106
+ output: z.object({ hasInjectedResource: z.literal(false) }),
95
107
  });
96
108
 
97
- const ctxOverrideContractProvision = provision('db.mock.contracts', {
109
+ const ctxOverrideContractResource = resource('db.mock.contracts', {
98
110
  create: () => Result.ok({ source: 'factory' }),
99
111
  mock: () => ({ source: 'mock' }),
100
112
  });
101
113
 
102
- const ctxOverrideContractTrail = trail('provision.ctx.contracts', {
114
+ const ctxOverrideContractTrail = trail('resource.ctx.contracts', {
103
115
  blaze: (_input, ctx) =>
104
- Result.ok({ source: ctxOverrideContractProvision.from(ctx).source }),
116
+ Result.ok({ source: ctxOverrideContractResource.from(ctx).source }),
105
117
  examples: [
106
118
  {
107
119
  expected: { source: 'ctx' },
108
120
  input: {},
109
- name: 'Context extensions beat auto-resolved contract provision mocks',
121
+ name: 'Context extensions beat auto-resolved contract resource mocks',
110
122
  },
111
123
  ],
112
124
  input: z.object({}),
113
125
  output: z.object({ source: z.literal('ctx') }),
114
- provisions: [ctxOverrideContractProvision],
126
+ resources: [ctxOverrideContractResource],
127
+ });
128
+
129
+ const derivedContractContour = contour(
130
+ 'contractFixture',
131
+ {
132
+ id: z.string().uuid(),
133
+ name: z.string(),
134
+ },
135
+ {
136
+ examples: [
137
+ {
138
+ id: '03a5873c-0ca6-43c4-9201-3cb3c07ca6bf',
139
+ name: 'Contour contract fixture',
140
+ },
141
+ ],
142
+ identity: 'id',
143
+ }
144
+ );
145
+
146
+ const derivedContractBlaze = mock(() =>
147
+ Result.ok(requireContourExample(derivedContractContour, 0))
148
+ );
149
+
150
+ const derivedContractTrail = trail('contract.derived', {
151
+ blaze: () => derivedContractBlaze(),
152
+ contours: [derivedContractContour],
153
+ input: z.object({ id: derivedContractContour.shape.id }),
154
+ output: derivedContractContour,
115
155
  });
116
156
 
117
157
  // ---------------------------------------------------------------------------
@@ -163,7 +203,7 @@ describe('testContracts: context extension overrides', () => {
163
203
  // eslint-disable-next-line jest/require-hook
164
204
  testContracts(
165
205
  topo('ctx-contract-app', {
166
- ctxOverrideContractProvision,
206
+ ctxOverrideContractResource,
167
207
  ctxOverrideContractTrail,
168
208
  } as Record<string, unknown>),
169
209
  {
@@ -174,12 +214,26 @@ describe('testContracts: context extension overrides', () => {
174
214
  );
175
215
  });
176
216
 
177
- describe('testContracts provision declarations', () => {
217
+ describe('testContracts resource declarations', () => {
178
218
  // eslint-disable-next-line jest/require-hook
179
219
  testContracts(
180
- topo('undeclared-contract-provision-app', {
181
- undeclaredContractDbProvision,
220
+ topo('undeclared-contract-resource-app', {
221
+ undeclaredContractDbResource,
182
222
  undeclaredContractTrail,
183
223
  } as Record<string, unknown>)
184
224
  );
185
225
  });
226
+
227
+ describe('testContracts derives contour examples when trail examples are absent', () => {
228
+ // eslint-disable-next-line jest/require-hook
229
+ testContracts(
230
+ topo('derived-contract-app', {
231
+ derivedContractContour,
232
+ derivedContractTrail,
233
+ } as Record<string, unknown>)
234
+ );
235
+
236
+ afterAll(() => {
237
+ expect(derivedContractBlaze).toHaveBeenCalledTimes(1);
238
+ });
239
+ });