@ontrails/testing 1.0.0-beta.15 → 1.0.0-beta.17
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.
- package/CHANGELOG.md +70 -1
- package/README.md +47 -3
- package/package.json +16 -7
- package/src/all.ts +32 -7
- package/src/context.ts +3 -0
- package/src/contracts.ts +1 -16
- package/src/examples.ts +19 -11
- package/src/harness-cli.ts +16 -3
- package/src/harness-http.ts +263 -0
- package/src/harness-mcp.ts +2 -0
- package/src/index.ts +18 -1
- package/src/logger.ts +3 -1
- package/src/scenario.ts +3 -5
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +356 -0
- package/src/types.ts +136 -1
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/all.d.ts +0 -36
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -99
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -72
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -238
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -76
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -118
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -17
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -75
- package/dist/contracts.js.map +0 -1
- package/dist/crosses.d.ts +0 -38
- package/dist/crosses.d.ts.map +0 -1
- package/dist/crosses.js +0 -223
- package/dist/crosses.js.map +0 -1
- package/dist/detours.d.ts +0 -13
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -109
- package/dist/detours.js.map +0 -1
- package/dist/effective-examples.d.ts +0 -30
- package/dist/effective-examples.d.ts.map +0 -1
- package/dist/effective-examples.js +0 -227
- package/dist/effective-examples.js.map +0 -1
- package/dist/examples.d.ts +0 -23
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -240
- package/dist/examples.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -21
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js +0 -205
- package/dist/harness-cli.js.map +0 -1
- package/dist/harness-mcp.d.ts +0 -21
- package/dist/harness-mcp.d.ts.map +0 -1
- package/dist/harness-mcp.js +0 -54
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -19
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -15
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -87
- package/dist/logger.js.map +0 -1
- package/dist/scenario.d.ts +0 -37
- package/dist/scenario.d.ts.map +0 -1
- package/dist/scenario.js +0 -235
- package/dist/scenario.js.map +0 -1
- package/dist/trail.d.ts +0 -20
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -80
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -113
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/all.test.ts +0 -254
- package/src/__tests__/context.test.ts +0 -170
- package/src/__tests__/contracts.test.ts +0 -239
- package/src/__tests__/crosses.test.ts +0 -587
- package/src/__tests__/detours.test.ts +0 -212
- package/src/__tests__/effective-examples.test.ts +0 -203
- package/src/__tests__/examples.test.ts +0 -636
- package/src/__tests__/harness-cli.test.ts +0 -90
- package/src/__tests__/harness-mcp.test.ts +0 -37
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/partial-match.test.ts +0 -126
- package/src/__tests__/scenario.test.ts +0 -381
- package/src/__tests__/trail.test.ts +0 -99
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { resource, Result, topo, trail } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { createCliHarness } from '../harness-cli.js';
|
|
7
|
-
|
|
8
|
-
describe('createCliHarness', () => {
|
|
9
|
-
test('runs top-level commands', async () => {
|
|
10
|
-
const greet = trail('greet', {
|
|
11
|
-
blaze: (input: { name: string }) => Result.ok(`Hello, ${input.name}!`),
|
|
12
|
-
input: z.object({ name: z.string() }),
|
|
13
|
-
});
|
|
14
|
-
const harness = createCliHarness({
|
|
15
|
-
graph: topo('test-app', { greet }),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const result = await harness.run('greet --name Trails');
|
|
19
|
-
|
|
20
|
-
expect(result.exitCode).toBe(0);
|
|
21
|
-
expect(result.stdout).toContain('Hello, Trails!');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('runs nested commands using the full ordered path', async () => {
|
|
25
|
-
const pin = trail('topo.pin', {
|
|
26
|
-
blaze: (input: { name: string }) => Result.ok(`Pinned ${input.name}`),
|
|
27
|
-
input: z.object({ name: z.string() }),
|
|
28
|
-
});
|
|
29
|
-
const harness = createCliHarness({
|
|
30
|
-
graph: topo('test-app', { pin }),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const result = await harness.run('topo pin --name before-auth');
|
|
34
|
-
|
|
35
|
-
expect(result.exitCode).toBe(0);
|
|
36
|
-
expect(result.stdout).toContain('Pinned before-auth');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('prefers the deepest matching executable path', async () => {
|
|
40
|
-
const calls: string[] = [];
|
|
41
|
-
const topoShow = trail('topo', {
|
|
42
|
-
blaze: () => {
|
|
43
|
-
calls.push('topo');
|
|
44
|
-
return Result.ok('topo');
|
|
45
|
-
},
|
|
46
|
-
input: z.object({}),
|
|
47
|
-
});
|
|
48
|
-
const topoPin = trail('topo.pin', {
|
|
49
|
-
blaze: () => {
|
|
50
|
-
calls.push('topo.pin');
|
|
51
|
-
return Result.ok('topo.pin');
|
|
52
|
-
},
|
|
53
|
-
input: z.object({}),
|
|
54
|
-
});
|
|
55
|
-
const harness = createCliHarness({
|
|
56
|
-
graph: topo('test-app', { topoPin, topoShow }),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const child = await harness.run('topo pin');
|
|
60
|
-
const parent = await harness.run('topo');
|
|
61
|
-
|
|
62
|
-
expect(child.exitCode).toBe(0);
|
|
63
|
-
expect(parent.exitCode).toBe(0);
|
|
64
|
-
expect(calls).toEqual(['topo.pin', 'topo']);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('threads resource overrides through CLI projection options', async () => {
|
|
68
|
-
const dbResource = resource('db.main', {
|
|
69
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
70
|
-
});
|
|
71
|
-
const readResource = trail('resource.read', {
|
|
72
|
-
blaze: (_input, ctx) =>
|
|
73
|
-
Result.ok({ source: dbResource.from(ctx).source as string }),
|
|
74
|
-
input: z.object({}),
|
|
75
|
-
output: z.object({ source: z.string() }),
|
|
76
|
-
resources: [dbResource],
|
|
77
|
-
});
|
|
78
|
-
const harness = createCliHarness({
|
|
79
|
-
graph: topo('test-app', { dbResource, readResource }),
|
|
80
|
-
resources: {
|
|
81
|
-
'db.main': { source: 'override' },
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const result = await harness.run('resource read --output json');
|
|
86
|
-
|
|
87
|
-
expect(result.exitCode).toBe(0);
|
|
88
|
-
expect(result.stdout).toContain('"override"');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { resource, Result, topo, trail } from '@ontrails/core';
|
|
4
|
-
import { deriveToolName } from '@ontrails/mcp';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
import { createMcpHarness } from '../harness-mcp.js';
|
|
8
|
-
|
|
9
|
-
describe('createMcpHarness', () => {
|
|
10
|
-
test('threads resource overrides through MCP projection options', async () => {
|
|
11
|
-
const dbResource = resource('db.main', {
|
|
12
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
13
|
-
});
|
|
14
|
-
const readResource = trail('resource.read', {
|
|
15
|
-
blaze: (_input, ctx) =>
|
|
16
|
-
Result.ok({ source: dbResource.from(ctx).source as string }),
|
|
17
|
-
input: z.object({}),
|
|
18
|
-
output: z.object({ source: z.string() }),
|
|
19
|
-
resources: [dbResource],
|
|
20
|
-
});
|
|
21
|
-
const graph = topo('test-app', { dbResource, readResource });
|
|
22
|
-
const harness = createMcpHarness({
|
|
23
|
-
graph,
|
|
24
|
-
resources: {
|
|
25
|
-
'db.main': { source: 'override' },
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const result = await harness.callTool(
|
|
30
|
-
deriveToolName(graph.name, readResource.id),
|
|
31
|
-
{}
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
expect(result.isError).toBe(false);
|
|
35
|
-
expect(JSON.stringify(result.content)).toContain('override');
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { createTestLogger } from '../logger.js';
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Tests
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
describe('createTestLogger: basic operations', () => {
|
|
10
|
-
test('captures info entries', () => {
|
|
11
|
-
const logger = createTestLogger();
|
|
12
|
-
logger.info('hello');
|
|
13
|
-
logger.warn('world');
|
|
14
|
-
expect(logger.entries).toHaveLength(2);
|
|
15
|
-
const [first] = logger.entries;
|
|
16
|
-
expect(first).toBeDefined();
|
|
17
|
-
expect(first?.level).toBe('info');
|
|
18
|
-
expect(first?.message).toBe('hello');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test('captures warn entries', () => {
|
|
22
|
-
const logger = createTestLogger();
|
|
23
|
-
logger.info('hello');
|
|
24
|
-
logger.warn('world');
|
|
25
|
-
const [, second] = logger.entries;
|
|
26
|
-
expect(second).toBeDefined();
|
|
27
|
-
expect(second?.level).toBe('warn');
|
|
28
|
-
expect(second?.message).toBe('world');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('entries contain all logged records', () => {
|
|
32
|
-
const logger = createTestLogger();
|
|
33
|
-
logger.trace('t');
|
|
34
|
-
logger.debug('d');
|
|
35
|
-
logger.info('i');
|
|
36
|
-
logger.warn('w');
|
|
37
|
-
logger.error('e');
|
|
38
|
-
logger.fatal('f');
|
|
39
|
-
expect(logger.entries).toHaveLength(6);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('clear() empties the entries array', () => {
|
|
43
|
-
const logger = createTestLogger();
|
|
44
|
-
logger.info('one');
|
|
45
|
-
logger.info('two');
|
|
46
|
-
expect(logger.entries).toHaveLength(2);
|
|
47
|
-
logger.clear();
|
|
48
|
-
expect(logger.entries).toHaveLength(0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('find() filters entries by predicate', () => {
|
|
52
|
-
const logger = createTestLogger();
|
|
53
|
-
logger.info('alpha');
|
|
54
|
-
logger.warn('beta');
|
|
55
|
-
logger.info('gamma');
|
|
56
|
-
|
|
57
|
-
const warnings = logger.find((r) => r.level === 'warn');
|
|
58
|
-
expect(warnings).toHaveLength(1);
|
|
59
|
-
expect(warnings[0]?.message).toBe('beta');
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('createTestLogger: assertLogged', () => {
|
|
64
|
-
test('passes when matching entry exists', () => {
|
|
65
|
-
const logger = createTestLogger();
|
|
66
|
-
logger.info('operation completed');
|
|
67
|
-
expect(() => logger.assertLogged('info', 'completed')).not.toThrow();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('fails when no matching entry exists', () => {
|
|
71
|
-
const logger = createTestLogger();
|
|
72
|
-
logger.info('something else');
|
|
73
|
-
expect(() => logger.assertLogged('error', 'not found')).toThrow(
|
|
74
|
-
/Expected a log entry/
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('createTestLogger: child loggers', () => {
|
|
80
|
-
test('child() captures to the same entries array', () => {
|
|
81
|
-
const logger = createTestLogger();
|
|
82
|
-
const child = logger.child({ component: 'db' });
|
|
83
|
-
|
|
84
|
-
logger.info('parent message');
|
|
85
|
-
child.info('child message');
|
|
86
|
-
|
|
87
|
-
expect(logger.entries).toHaveLength(2);
|
|
88
|
-
expect(logger.entries[0]?.message).toBe('parent message');
|
|
89
|
-
expect(logger.entries[1]?.message).toBe('child message');
|
|
90
|
-
expect(logger.entries[1]?.metadata).toEqual({ component: 'db' });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('child logger inherits parent metadata', () => {
|
|
94
|
-
const logger = createTestLogger();
|
|
95
|
-
const child = logger.child({ requestId: 'abc' });
|
|
96
|
-
const grandchild = child.child({ trailId: 'greet' });
|
|
97
|
-
|
|
98
|
-
grandchild.info('deep log');
|
|
99
|
-
|
|
100
|
-
expect(logger.entries).toHaveLength(1);
|
|
101
|
-
expect(logger.entries[0]?.metadata).toEqual({
|
|
102
|
-
requestId: 'abc',
|
|
103
|
-
trailId: 'greet',
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('createTestLogger: configuration', () => {
|
|
109
|
-
test('respects minimum log level', () => {
|
|
110
|
-
const logger = createTestLogger({ level: 'warn' });
|
|
111
|
-
logger.debug('should be filtered');
|
|
112
|
-
logger.info('also filtered');
|
|
113
|
-
logger.warn('should appear');
|
|
114
|
-
logger.error('also appears');
|
|
115
|
-
expect(logger.entries).toHaveLength(2);
|
|
116
|
-
expect(logger.entries[0]?.level).toBe('warn');
|
|
117
|
-
expect(logger.entries[1]?.level).toBe('error');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('has name property', () => {
|
|
121
|
-
const logger = createTestLogger();
|
|
122
|
-
expect(logger.name).toBe('test');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('entries have timestamps', () => {
|
|
126
|
-
const logger = createTestLogger();
|
|
127
|
-
logger.info('timestamped');
|
|
128
|
-
expect(logger.entries[0]?.timestamp).toBeInstanceOf(Date);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('entries have category matching logger name', () => {
|
|
132
|
-
const logger = createTestLogger();
|
|
133
|
-
logger.info('categorized');
|
|
134
|
-
expect(logger.entries[0]?.category).toBe('test');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { Result } from '@ontrails/core';
|
|
4
|
-
|
|
5
|
-
import { assertPartialMatch } from '../assertions.js';
|
|
6
|
-
|
|
7
|
-
describe('assertPartialMatch', () => {
|
|
8
|
-
describe('scalar values', () => {
|
|
9
|
-
test('passes with exact scalar match', () => {
|
|
10
|
-
const result = Result.ok('hello');
|
|
11
|
-
assertPartialMatch(result, 'hello');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('passes with numeric match', () => {
|
|
15
|
-
const result = Result.ok(42);
|
|
16
|
-
assertPartialMatch(result, 42);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('fails when scalar does not match', () => {
|
|
20
|
-
const result = Result.ok('hello');
|
|
21
|
-
expect(() => assertPartialMatch(result, 'world')).toThrow();
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('object subset', () => {
|
|
26
|
-
test('passes with full match', () => {
|
|
27
|
-
const result = Result.ok({ id: '1', name: 'Alpha', type: 'concept' });
|
|
28
|
-
assertPartialMatch(result, { id: '1', name: 'Alpha', type: 'concept' });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('passes with partial match (extra keys in actual ignored)', () => {
|
|
32
|
-
const result = Result.ok({
|
|
33
|
-
createdAt: '2026-01-01',
|
|
34
|
-
id: '1',
|
|
35
|
-
name: 'Alpha',
|
|
36
|
-
type: 'concept',
|
|
37
|
-
});
|
|
38
|
-
assertPartialMatch(result, { name: 'Alpha', type: 'concept' });
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('fails when expected key is missing from actual', () => {
|
|
42
|
-
const result = Result.ok({ id: '1', name: 'Alpha' });
|
|
43
|
-
expect(() =>
|
|
44
|
-
assertPartialMatch(result, { missing: true, name: 'Alpha' })
|
|
45
|
-
).toThrow();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('fails when expected value does not match', () => {
|
|
49
|
-
const result = Result.ok({ id: '1', name: 'Alpha' });
|
|
50
|
-
expect(() => assertPartialMatch(result, { name: 'Beta' })).toThrow();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('nested object subset', () => {
|
|
55
|
-
test('passes with nested partial match', () => {
|
|
56
|
-
const result = Result.ok({
|
|
57
|
-
id: '1',
|
|
58
|
-
meta: { stars: 0, tags: ['a', 'b'], views: 5 },
|
|
59
|
-
});
|
|
60
|
-
assertPartialMatch(result, { meta: { stars: 0 } });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('fails when nested value does not match', () => {
|
|
64
|
-
const result = Result.ok({
|
|
65
|
-
id: '1',
|
|
66
|
-
meta: { stars: 0, views: 5 },
|
|
67
|
-
});
|
|
68
|
-
expect(() =>
|
|
69
|
-
assertPartialMatch(result, { meta: { stars: 3 } })
|
|
70
|
-
).toThrow();
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('array subset', () => {
|
|
75
|
-
test('passes when actual contains all expected elements (order-independent)', () => {
|
|
76
|
-
const result = Result.ok({
|
|
77
|
-
tags: ['a', 'b', 'c'],
|
|
78
|
-
});
|
|
79
|
-
assertPartialMatch(result, { tags: ['c', 'a'] });
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('fails when actual is missing expected array element', () => {
|
|
83
|
-
const result = Result.ok({
|
|
84
|
-
tags: ['a', 'b'],
|
|
85
|
-
});
|
|
86
|
-
expect(() => assertPartialMatch(result, { tags: ['a', 'z'] })).toThrow();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('fails when expected has duplicate but actual has only one match', () => {
|
|
90
|
-
const result = Result.ok({
|
|
91
|
-
tags: ['a', 'b'],
|
|
92
|
-
});
|
|
93
|
-
expect(() => assertPartialMatch(result, { tags: ['a', 'a'] })).toThrow();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('passes when expected has duplicate and actual has enough matches', () => {
|
|
97
|
-
const result = Result.ok({
|
|
98
|
-
tags: ['a', 'a', 'b'],
|
|
99
|
-
});
|
|
100
|
-
assertPartialMatch(result, { tags: ['a', 'a'] });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('fails when expected objects have duplicates but actual has only one', () => {
|
|
104
|
-
const result = Result.ok({
|
|
105
|
-
items: [{ id: 1 }],
|
|
106
|
-
});
|
|
107
|
-
expect(() =>
|
|
108
|
-
assertPartialMatch(result, { items: [{ id: 1 }, { id: 1 }] })
|
|
109
|
-
).toThrow();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('passes when expected objects have duplicates and actual has enough', () => {
|
|
113
|
-
const result = Result.ok({
|
|
114
|
-
items: [{ id: 1 }, { id: 1 }, { id: 2 }],
|
|
115
|
-
});
|
|
116
|
-
assertPartialMatch(result, { items: [{ id: 1 }, { id: 1 }] });
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('error on non-ok result', () => {
|
|
121
|
-
test('fails when result is an error', () => {
|
|
122
|
-
const result = Result.err(new Error('boom'));
|
|
123
|
-
expect(() => assertPartialMatch(result, { id: '1' })).toThrow();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|