@ontrails/warden 1.0.0-beta.11 → 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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +59 -31
- package/README.md +17 -17
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -10
- package/dist/cli.js.map +1 -1
- package/dist/drift.d.ts +6 -6
- package/dist/drift.d.ts.map +1 -1
- package/dist/drift.js +8 -8
- package/dist/drift.js.map +1 -1
- package/dist/formatters.js +2 -2
- package/dist/formatters.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/rules/ast.d.ts +15 -11
- package/dist/rules/ast.d.ts.map +1 -1
- package/dist/rules/ast.js +34 -30
- package/dist/rules/ast.js.map +1 -1
- package/dist/rules/context-no-trailhead-types.d.ts +12 -0
- package/dist/rules/context-no-trailhead-types.d.ts.map +1 -0
- package/dist/rules/context-no-trailhead-types.js +96 -0
- package/dist/rules/context-no-trailhead-types.js.map +1 -0
- package/dist/rules/cross-declarations.d.ts +13 -0
- package/dist/rules/cross-declarations.d.ts.map +1 -0
- package/dist/rules/cross-declarations.js +264 -0
- package/dist/rules/cross-declarations.js.map +1 -0
- package/dist/rules/follow-declarations.d.ts +1 -1
- package/dist/rules/follow-declarations.js +5 -5
- package/dist/rules/follow-declarations.js.map +1 -1
- package/dist/rules/implementation-returns-result.d.ts +2 -2
- package/dist/rules/implementation-returns-result.js +6 -6
- package/dist/rules/implementation-returns-result.js.map +1 -1
- package/dist/rules/index.d.ts +4 -4
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +12 -12
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +4 -4
- package/dist/rules/no-direct-impl-in-route.js +14 -14
- package/dist/rules/no-direct-impl-in-route.js.map +1 -1
- package/dist/rules/no-direct-implementation-call.d.ts +3 -3
- package/dist/rules/no-direct-implementation-call.js +7 -7
- package/dist/rules/no-direct-implementation-call.js.map +1 -1
- package/dist/rules/no-sync-result-assumption.d.ts +1 -1
- package/dist/rules/no-sync-result-assumption.js +5 -5
- package/dist/rules/no-sync-result-assumption.js.map +1 -1
- package/dist/rules/no-throw-in-detour-target.js +2 -2
- package/dist/rules/no-throw-in-detour-target.js.map +1 -1
- package/dist/rules/no-throw-in-implementation.d.ts +1 -1
- package/dist/rules/no-throw-in-implementation.js +3 -3
- package/dist/rules/no-throw-in-implementation.js.map +1 -1
- package/dist/rules/provision-declarations.d.ts +14 -0
- package/dist/rules/provision-declarations.d.ts.map +1 -0
- package/dist/rules/provision-declarations.js +344 -0
- package/dist/rules/provision-declarations.js.map +1 -0
- package/dist/rules/provision-exists.d.ts +6 -0
- package/dist/rules/provision-exists.d.ts.map +1 -0
- package/dist/rules/provision-exists.js +89 -0
- package/dist/rules/provision-exists.js.map +1 -0
- package/dist/rules/service-declarations.d.ts +7 -5
- package/dist/rules/service-declarations.d.ts.map +1 -1
- package/dist/rules/service-declarations.js +106 -103
- package/dist/rules/service-declarations.js.map +1 -1
- package/dist/rules/service-exists.d.ts +3 -1
- package/dist/rules/service-exists.d.ts.map +1 -1
- package/dist/rules/service-exists.js +35 -33
- package/dist/rules/service-exists.js.map +1 -1
- package/dist/rules/specs.d.ts +1 -1
- package/dist/rules/specs.d.ts.map +1 -1
- package/dist/rules/specs.js +1 -1
- package/dist/rules/specs.js.map +1 -1
- package/dist/rules/types.d.ts +2 -2
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/trails/context-no-surface-types.trail.js +1 -1
- package/dist/trails/context-no-trailhead-types.trail.d.ts +13 -0
- package/dist/trails/context-no-trailhead-types.trail.d.ts.map +1 -0
- package/dist/trails/context-no-trailhead-types.trail.js +21 -0
- package/dist/trails/context-no-trailhead-types.trail.js.map +1 -0
- package/dist/trails/cross-declarations.trail.d.ts +13 -0
- package/dist/trails/cross-declarations.trail.d.ts.map +1 -0
- package/dist/trails/cross-declarations.trail.js +22 -0
- package/dist/trails/cross-declarations.trail.js.map +1 -0
- package/dist/trails/follow-declarations.trail.js +1 -1
- package/dist/trails/implementation-returns-result.trail.js +1 -1
- package/dist/trails/index.d.ts +4 -4
- package/dist/trails/index.d.ts.map +1 -1
- package/dist/trails/index.js +4 -4
- package/dist/trails/index.js.map +1 -1
- package/dist/trails/no-direct-impl-in-route.trail.js +4 -4
- package/dist/trails/no-direct-impl-in-route.trail.js.map +1 -1
- package/dist/trails/no-direct-implementation-call.trail.js +2 -2
- package/dist/trails/no-direct-implementation-call.trail.js.map +1 -1
- package/dist/trails/no-sync-result-assumption.trail.js +2 -2
- package/dist/trails/no-sync-result-assumption.trail.js.map +1 -1
- package/dist/trails/no-throw-in-detour-target.trail.d.ts +1 -1
- package/dist/trails/no-throw-in-detour-target.trail.js +1 -1
- package/dist/trails/no-throw-in-implementation.trail.js +1 -1
- package/dist/trails/prefer-schema-inference.trail.js +1 -1
- package/dist/trails/provision-declarations.trail.d.ts +13 -0
- package/dist/trails/provision-declarations.trail.d.ts.map +1 -0
- package/dist/trails/provision-declarations.trail.js +25 -0
- package/dist/trails/provision-declarations.trail.js.map +1 -0
- package/dist/trails/provision-exists.trail.d.ts +15 -0
- package/dist/trails/provision-exists.trail.d.ts.map +1 -0
- package/dist/trails/provision-exists.trail.js +27 -0
- package/dist/trails/provision-exists.trail.js.map +1 -0
- package/dist/trails/run.d.ts +2 -2
- package/dist/trails/run.d.ts.map +1 -1
- package/dist/trails/run.js +6 -6
- package/dist/trails/run.js.map +1 -1
- package/dist/trails/schema.d.ts +1 -1
- package/dist/trails/schema.js +2 -2
- package/dist/trails/schema.js.map +1 -1
- package/dist/trails/service-declarations.trail.d.ts +13 -0
- package/dist/trails/service-declarations.trail.d.ts.map +1 -1
- package/dist/trails/service-declarations.trail.js +9 -7
- package/dist/trails/service-declarations.trail.js.map +1 -1
- package/dist/trails/service-exists.trail.d.ts +17 -0
- package/dist/trails/service-exists.trail.d.ts.map +1 -1
- package/dist/trails/service-exists.trail.js +10 -8
- package/dist/trails/service-exists.trail.js.map +1 -1
- package/dist/trails/valid-describe-refs.trail.d.ts +1 -1
- package/dist/trails/valid-detour-refs.trail.d.ts +1 -1
- package/dist/trails/valid-detour-refs.trail.js +2 -2
- package/dist/trails/wrap-rule.js +14 -14
- package/dist/trails/wrap-rule.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/cli.test.ts +8 -8
- package/src/__tests__/{follow-declarations.test.ts → cross-declarations.test.ts} +78 -78
- package/src/__tests__/drift.test.ts +5 -5
- package/src/__tests__/formatters.test.ts +2 -2
- package/src/__tests__/implementation-returns-result.test.ts +11 -11
- package/src/__tests__/no-direct-implementation-call.test.ts +10 -10
- package/src/__tests__/no-sync-result-assumption.test.ts +6 -6
- package/src/__tests__/no-throw-in-detour-target.test.ts +6 -6
- package/src/__tests__/prefer-schema-inference.test.ts +4 -4
- package/src/__tests__/provision-declarations.test.ts +318 -0
- package/src/__tests__/provision-exists.test.ts +122 -0
- package/src/__tests__/rules.test.ts +38 -38
- package/src/__tests__/valid-describe-refs.test.ts +4 -4
- package/src/__tests__/wrap-rule.test.ts +4 -4
- package/src/cli.ts +17 -13
- package/src/drift.ts +12 -12
- package/src/formatters.ts +2 -2
- package/src/index.ts +8 -8
- package/src/rules/ast.ts +36 -31
- package/src/rules/{context-no-surface-types.ts → context-no-trailhead-types.ts} +8 -8
- package/src/rules/{follow-declarations.ts → cross-declarations.ts} +63 -56
- package/src/rules/implementation-returns-result.ts +6 -6
- package/src/rules/index.ts +12 -12
- package/src/rules/no-direct-impl-in-route.ts +17 -17
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-sync-result-assumption.ts +5 -5
- package/src/rules/no-throw-in-detour-target.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +3 -3
- package/src/rules/{service-declarations.ts → provision-declarations.ts} +145 -129
- package/src/rules/{service-exists.ts → provision-exists.ts} +51 -46
- package/src/rules/specs.ts +4 -4
- package/src/rules/types.ts +2 -2
- package/src/trails/{context-no-surface-types.trail.ts → context-no-trailhead-types.trail.ts} +5 -5
- package/src/trails/cross-declarations.trail.ts +22 -0
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/index.ts +4 -4
- package/src/trails/no-direct-impl-in-route.trail.ts +4 -4
- package/src/trails/no-direct-implementation-call.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-target.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/provision-declarations.trail.ts +25 -0
- package/src/trails/provision-exists.trail.ts +27 -0
- package/src/trails/run.ts +7 -7
- package/src/trails/schema.ts +2 -2
- package/src/trails/valid-detour-refs.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/tsconfig.tsbuildinfo +1 -1
- package/src/__tests__/service-declarations.test.ts +0 -318
- package/src/__tests__/service-exists.test.ts +0 -122
- package/src/trails/follow-declarations.trail.ts +0 -22
- package/src/trails/service-declarations.trail.ts +0 -25
- package/src/trails/service-exists.trail.ts +0 -27
|
@@ -10,7 +10,7 @@ trail("entity.show", {
|
|
|
10
10
|
fields: {
|
|
11
11
|
firstName: { label: "First Name" },
|
|
12
12
|
},
|
|
13
|
-
|
|
13
|
+
blaze: (input) => Result.ok(input),
|
|
14
14
|
})`;
|
|
15
15
|
|
|
16
16
|
const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
|
|
@@ -31,7 +31,7 @@ trail("entity.paint", {
|
|
|
31
31
|
options: [{ value: "red" }, { value: "green" }],
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
|
-
|
|
34
|
+
blaze: (input) => Result.ok(input),
|
|
35
35
|
})`;
|
|
36
36
|
|
|
37
37
|
const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
|
|
@@ -56,7 +56,7 @@ trail("entity.paint", {
|
|
|
56
56
|
},
|
|
57
57
|
displayName: { label: "Public name" },
|
|
58
58
|
},
|
|
59
|
-
|
|
59
|
+
blaze: (input) => Result.ok(input),
|
|
60
60
|
})`;
|
|
61
61
|
|
|
62
62
|
const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
|
|
@@ -74,7 +74,7 @@ trail("entity.show", {
|
|
|
74
74
|
message: "Who should we greet?",
|
|
75
75
|
},
|
|
76
76
|
},
|
|
77
|
-
|
|
77
|
+
blaze: (input) => Result.ok(input),
|
|
78
78
|
})`;
|
|
79
79
|
|
|
80
80
|
const diagnostics = preferSchemaInference.check(code, 'src/entity.ts');
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { provisionDeclarations } from '../rules/provision-declarations.js';
|
|
4
|
+
|
|
5
|
+
const TEST_FILE = 'test.ts';
|
|
6
|
+
|
|
7
|
+
describe('provision-declarations', () => {
|
|
8
|
+
describe('clean cases', () => {
|
|
9
|
+
test('declared provisions match provision.from(ctx) usage', () => {
|
|
10
|
+
const code = `
|
|
11
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
12
|
+
|
|
13
|
+
const db = provision('db.main', {
|
|
14
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
trail('entity.show', {
|
|
18
|
+
provisions: [db],
|
|
19
|
+
blaze: async (_input, ctx) => {
|
|
20
|
+
return Result.ok({ source: db.from(ctx).source });
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
26
|
+
|
|
27
|
+
expect(diagnostics.length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('local helper named provision() is not treated as ctx lookup', () => {
|
|
31
|
+
const code = `
|
|
32
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
33
|
+
|
|
34
|
+
const db = provision('db.main', {
|
|
35
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
trail('entity.show', {
|
|
39
|
+
provisions: [db],
|
|
40
|
+
blaze: async (_input, ctx) => {
|
|
41
|
+
const provision = (id: string) => id;
|
|
42
|
+
return Result.ok({
|
|
43
|
+
resolved: provision('db.main'),
|
|
44
|
+
source: db.from(ctx).source,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
51
|
+
|
|
52
|
+
expect(diagnostics.length).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('declared provisions match ctx.provision() usage', () => {
|
|
56
|
+
const code = `
|
|
57
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
58
|
+
|
|
59
|
+
const db = provision('db.main', {
|
|
60
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
trail('entity.show', {
|
|
64
|
+
provisions: [db],
|
|
65
|
+
blaze: async (_input, ctx) => {
|
|
66
|
+
const resolved = ctx.provision('db.main');
|
|
67
|
+
return Result.ok(resolved);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
73
|
+
|
|
74
|
+
expect(diagnostics.length).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('recognizes destructured provision() calls', () => {
|
|
78
|
+
const code = `
|
|
79
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
80
|
+
|
|
81
|
+
const db = provision('db.main', {
|
|
82
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
trail('entity.show', {
|
|
86
|
+
provisions: [db],
|
|
87
|
+
blaze: async (_input, ctx) => {
|
|
88
|
+
const { provision } = ctx;
|
|
89
|
+
return Result.ok(provision('db.main'));
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
95
|
+
|
|
96
|
+
expect(diagnostics.length).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('recognizes ctx.provision(db) lookups by declared provision object', () => {
|
|
100
|
+
const code = `
|
|
101
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
102
|
+
|
|
103
|
+
const db = provision('db.main', {
|
|
104
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
trail('entity.show', {
|
|
108
|
+
provisions: [db],
|
|
109
|
+
blaze: async (_input, ctx) => {
|
|
110
|
+
return Result.ok(ctx.provision(db));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
116
|
+
|
|
117
|
+
expect(diagnostics.length).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('error cases', () => {
|
|
122
|
+
test('provision.from(ctx) without a declaration produces an error', () => {
|
|
123
|
+
const code = `
|
|
124
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
125
|
+
|
|
126
|
+
const db = provision('db.main', {
|
|
127
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
trail('entity.show', {
|
|
131
|
+
blaze: async (_input, ctx) => {
|
|
132
|
+
return Result.ok({ source: db.from(ctx).source });
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
138
|
+
|
|
139
|
+
expect(diagnostics.length).toBe(1);
|
|
140
|
+
expect(diagnostics[0]?.severity).toBe('error');
|
|
141
|
+
expect(diagnostics[0]?.rule).toBe('provision-declarations');
|
|
142
|
+
expect(diagnostics[0]?.message).toContain('db.from(ctx)');
|
|
143
|
+
expect(diagnostics[0]?.message).toContain('not declared in provisions');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('ctx.provision() without a declaration produces an error', () => {
|
|
147
|
+
const code = `
|
|
148
|
+
trail('entity.show', {
|
|
149
|
+
blaze: async (_input, ctx) => {
|
|
150
|
+
return Result.ok(ctx.provision('db.main'));
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
156
|
+
|
|
157
|
+
expect(diagnostics.length).toBe(1);
|
|
158
|
+
expect(diagnostics[0]?.severity).toBe('error');
|
|
159
|
+
expect(diagnostics[0]?.message).toContain("ctx.provision('db.main')");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('unresolved imported provision declarations do not suppress lookup diagnostics', () => {
|
|
163
|
+
const code = `
|
|
164
|
+
import { Result, trail } from '@ontrails/core';
|
|
165
|
+
import { db } from './provisions';
|
|
166
|
+
|
|
167
|
+
// const db = provision('db.main', {
|
|
168
|
+
// create: () => Result.ok({ source: 'factory' }),
|
|
169
|
+
// });
|
|
170
|
+
|
|
171
|
+
trail('entity.show', {
|
|
172
|
+
provisions: [db],
|
|
173
|
+
blaze: async (_input, ctx) => {
|
|
174
|
+
return Result.ok(ctx.provision('db.main'));
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
180
|
+
|
|
181
|
+
expect(diagnostics.length).toBe(1);
|
|
182
|
+
expect(diagnostics[0]?.severity).toBe('error');
|
|
183
|
+
expect(diagnostics[0]?.message).toContain("ctx.provision('db.main')");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('ctx.provision(db) without a declaration produces an error', () => {
|
|
187
|
+
const code = `
|
|
188
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
189
|
+
|
|
190
|
+
const db = provision('db.main', {
|
|
191
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
trail('entity.show', {
|
|
195
|
+
blaze: async (_input, ctx) => {
|
|
196
|
+
return Result.ok(ctx.provision(db));
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
202
|
+
|
|
203
|
+
expect(diagnostics.length).toBe(1);
|
|
204
|
+
expect(diagnostics[0]?.severity).toBe('error');
|
|
205
|
+
expect(diagnostics[0]?.message).toContain('ctx.provision(db)');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('warn cases', () => {
|
|
210
|
+
test('declared but unused provision produces a warning', () => {
|
|
211
|
+
const code = `
|
|
212
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
213
|
+
|
|
214
|
+
const db = provision('db.main', {
|
|
215
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
trail('entity.show', {
|
|
219
|
+
provisions: [db],
|
|
220
|
+
blaze: async () => {
|
|
221
|
+
return Result.ok({ ok: true });
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
227
|
+
|
|
228
|
+
expect(diagnostics.length).toBe(1);
|
|
229
|
+
expect(diagnostics[0]?.severity).toBe('warn');
|
|
230
|
+
expect(diagnostics[0]?.rule).toBe('provision-declarations');
|
|
231
|
+
expect(diagnostics[0]?.message).toContain("'db' declared in provisions");
|
|
232
|
+
expect(diagnostics[0]?.message).toContain('never used');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('single-object overload', () => {
|
|
237
|
+
test('recognizes trail({ id, provisions, blaze }) form', () => {
|
|
238
|
+
const code = `
|
|
239
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
240
|
+
|
|
241
|
+
const db = provision('db.main', {
|
|
242
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
trail({
|
|
246
|
+
id: 'entity.show',
|
|
247
|
+
provisions: [db],
|
|
248
|
+
blaze: async (_input, ctx) => {
|
|
249
|
+
return Result.ok({ source: db.from(ctx).source });
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
255
|
+
|
|
256
|
+
expect(diagnostics.length).toBe(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('context parameter naming', () => {
|
|
261
|
+
test('recognizes database.from(context) when second param is named context', () => {
|
|
262
|
+
const code = `
|
|
263
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
264
|
+
|
|
265
|
+
const database = provision('db.main', {
|
|
266
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
trail('entity.show', {
|
|
270
|
+
provisions: [database],
|
|
271
|
+
blaze: async (_input, context) => {
|
|
272
|
+
return Result.ok(database.from(context));
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
278
|
+
|
|
279
|
+
expect(diagnostics.length).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('nested run false positives', () => {
|
|
284
|
+
test('meta.run does not trigger false positives', () => {
|
|
285
|
+
const code = `
|
|
286
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
287
|
+
|
|
288
|
+
const db = provision('db.main', {
|
|
289
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
trail('entity.show', {
|
|
293
|
+
provisions: [db],
|
|
294
|
+
meta: { blaze: async () => ctx.provision('phantom') },
|
|
295
|
+
blaze: async (_input, ctx) => {
|
|
296
|
+
return Result.ok(db.from(ctx));
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
const diagnostics = provisionDeclarations.check(code, TEST_FILE);
|
|
302
|
+
|
|
303
|
+
expect(diagnostics.length).toBe(0);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('skips test files', () => {
|
|
308
|
+
const code = `
|
|
309
|
+
trail('entity.show', {
|
|
310
|
+
blaze: async (_input, ctx) => {
|
|
311
|
+
return Result.ok(ctx.provision('db.main'));
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
`;
|
|
315
|
+
|
|
316
|
+
expect(provisionDeclarations.check(code, 'entity.test.ts')).toEqual([]);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { provisionExists } from '../rules/provision-exists.js';
|
|
4
|
+
|
|
5
|
+
const TEST_FILE = 'entity.ts';
|
|
6
|
+
|
|
7
|
+
describe('provision-exists', () => {
|
|
8
|
+
test('passes when a locally declared provision exists', () => {
|
|
9
|
+
const code = `
|
|
10
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
11
|
+
import type { Provision } from '@ontrails/core';
|
|
12
|
+
|
|
13
|
+
const db: Provision<{ source: string }> = provision('db.main', {
|
|
14
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
trail('entity.show', {
|
|
18
|
+
provisions: [db],
|
|
19
|
+
blaze: async (_input, ctx) => Result.ok(db.from(ctx)),
|
|
20
|
+
});
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
expect(provisionExists.check(code, TEST_FILE)).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('ignores commented-out provision declarations when resolving local ids', () => {
|
|
27
|
+
const code = `
|
|
28
|
+
import { Result, trail } from '@ontrails/core';
|
|
29
|
+
|
|
30
|
+
trail('entity.show', {
|
|
31
|
+
provisions: ['db.main'],
|
|
32
|
+
blaze: async (_input, ctx) => Result.ok(ctx.provision('db.main')),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// const db = provision('db.main', {
|
|
36
|
+
// create: () => Result.ok({ source: 'factory' }),
|
|
37
|
+
// });
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const diagnostics = provisionExists.check(code, TEST_FILE);
|
|
41
|
+
|
|
42
|
+
expect(diagnostics).toHaveLength(1);
|
|
43
|
+
expect(diagnostics[0]?.rule).toBe('provision-exists');
|
|
44
|
+
expect(diagnostics[0]?.message).toContain('db.main');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('flags a declared provision missing from project context', () => {
|
|
48
|
+
const code = `
|
|
49
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
50
|
+
|
|
51
|
+
const db = provision('db.main', {
|
|
52
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
trail('entity.show', {
|
|
56
|
+
provisions: [db],
|
|
57
|
+
blaze: async (_input, ctx) => Result.ok(db.from(ctx)),
|
|
58
|
+
});
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const diagnostics = provisionExists.checkWithContext(code, TEST_FILE, {
|
|
62
|
+
knownProvisionIds: new Set(['db.other']),
|
|
63
|
+
knownTrailIds: new Set(['entity.show']),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(diagnostics).toHaveLength(1);
|
|
67
|
+
expect(diagnostics[0]?.rule).toBe('provision-exists');
|
|
68
|
+
expect(diagnostics[0]?.message).toContain('db.main');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('passes when project context includes the declared provision', () => {
|
|
72
|
+
const code = `
|
|
73
|
+
import { Result, provision, trail } from '@ontrails/core';
|
|
74
|
+
|
|
75
|
+
const db = provision('db.main', {
|
|
76
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
trail('entity.show', {
|
|
80
|
+
provisions: [db],
|
|
81
|
+
blaze: async (_input, ctx) => Result.ok(db.from(ctx)),
|
|
82
|
+
});
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const diagnostics = provisionExists.checkWithContext(code, TEST_FILE, {
|
|
86
|
+
knownProvisionIds: new Set(['db.main']),
|
|
87
|
+
knownTrailIds: new Set(['entity.show']),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(diagnostics).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('skips unresolved imported provisions instead of guessing', () => {
|
|
94
|
+
const code = `
|
|
95
|
+
import { trail } from '@ontrails/core';
|
|
96
|
+
import { db } from './provisions';
|
|
97
|
+
|
|
98
|
+
trail('entity.show', {
|
|
99
|
+
provisions: [db],
|
|
100
|
+
blaze: async (_input, ctx) => Result.ok(db.from(ctx)),
|
|
101
|
+
});
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
expect(
|
|
105
|
+
provisionExists.checkWithContext(code, TEST_FILE, {
|
|
106
|
+
knownProvisionIds: new Set(['db.main']),
|
|
107
|
+
knownTrailIds: new Set(['entity.show']),
|
|
108
|
+
})
|
|
109
|
+
).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('skips test files', () => {
|
|
113
|
+
const code = `
|
|
114
|
+
trail('entity.show', {
|
|
115
|
+
provisions: ['db.main'],
|
|
116
|
+
blaze: async (_input, ctx) => Result.ok(ctx.provision('db.main')),
|
|
117
|
+
});
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
expect(provisionExists.check(code, 'entity.test.ts')).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { contextNoTrailheadTypes } from '../rules/context-no-trailhead-types.js';
|
|
4
4
|
import { noDirectImplInRoute } from '../rules/no-direct-impl-in-route.js';
|
|
5
5
|
import { noThrowInImplementation } from '../rules/no-throw-in-implementation.js';
|
|
6
6
|
import { validDetourRefs } from '../rules/valid-detour-refs.js';
|
|
@@ -14,7 +14,7 @@ describe('no-throw-in-implementation', () => {
|
|
|
14
14
|
test('flags throw inside implementation body', () => {
|
|
15
15
|
const code = `
|
|
16
16
|
trail("entity.show", {
|
|
17
|
-
|
|
17
|
+
blaze: async (input, ctx) => {
|
|
18
18
|
throw new Error("boom");
|
|
19
19
|
}
|
|
20
20
|
})`;
|
|
@@ -27,7 +27,7 @@ trail("entity.show", {
|
|
|
27
27
|
test('allows Result.err() in implementation', () => {
|
|
28
28
|
const code = `
|
|
29
29
|
trail("entity.show", {
|
|
30
|
-
|
|
30
|
+
blaze: async (input, ctx) => {
|
|
31
31
|
return Result.err(new NotFoundError("not found"));
|
|
32
32
|
}
|
|
33
33
|
})`;
|
|
@@ -42,7 +42,7 @@ function helper() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
trail("entity.show", {
|
|
45
|
-
|
|
45
|
+
blaze: async (input, ctx) => {
|
|
46
46
|
return Result.ok(data);
|
|
47
47
|
}
|
|
48
48
|
})`;
|
|
@@ -52,20 +52,20 @@ trail("entity.show", {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
|
-
// context-no-
|
|
55
|
+
// context-no-trailhead-types
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
|
-
describe('context-no-
|
|
57
|
+
describe('context-no-trailhead-types', () => {
|
|
58
58
|
test('flags express import in trail file', () => {
|
|
59
59
|
const code = `
|
|
60
60
|
import { Request, Response } from "express";
|
|
61
61
|
trail("entity.show", {
|
|
62
|
-
|
|
62
|
+
blaze: async (input, ctx) => {
|
|
63
63
|
return Result.ok(data);
|
|
64
64
|
}
|
|
65
65
|
})`;
|
|
66
|
-
const diagnostics =
|
|
66
|
+
const diagnostics = contextNoTrailheadTypes.check(code, TEST_FILE);
|
|
67
67
|
expect(diagnostics.length).toBe(1);
|
|
68
|
-
expect(diagnostics[0]?.rule).toBe('context-no-
|
|
68
|
+
expect(diagnostics[0]?.rule).toBe('context-no-trailhead-types');
|
|
69
69
|
expect(diagnostics[0]?.message).toContain('express');
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -73,11 +73,11 @@ trail("entity.show", {
|
|
|
73
73
|
const code = `
|
|
74
74
|
import type { McpSession } from "@modelcontextprotocol/sdk";
|
|
75
75
|
trail("entity.show", {
|
|
76
|
-
|
|
76
|
+
blaze: async (input, ctx) => {
|
|
77
77
|
return Result.ok(data);
|
|
78
78
|
}
|
|
79
79
|
})`;
|
|
80
|
-
const diagnostics =
|
|
80
|
+
const diagnostics = contextNoTrailheadTypes.check(code, TEST_FILE);
|
|
81
81
|
expect(diagnostics.length).toBe(1);
|
|
82
82
|
});
|
|
83
83
|
|
|
@@ -85,11 +85,11 @@ trail("entity.show", {
|
|
|
85
85
|
const code = `
|
|
86
86
|
import { trail, Result } from "@ontrails/core";
|
|
87
87
|
trail("entity.show", {
|
|
88
|
-
|
|
88
|
+
blaze: async (input, ctx) => {
|
|
89
89
|
return Result.ok(data);
|
|
90
90
|
}
|
|
91
91
|
})`;
|
|
92
|
-
const diagnostics =
|
|
92
|
+
const diagnostics = contextNoTrailheadTypes.check(code, TEST_FILE);
|
|
93
93
|
expect(diagnostics.length).toBe(0);
|
|
94
94
|
});
|
|
95
95
|
|
|
@@ -97,7 +97,7 @@ trail("entity.show", {
|
|
|
97
97
|
const code = `
|
|
98
98
|
import { Request, Response } from "express";
|
|
99
99
|
export function handleRequest(req: Request, res: Response) {}`;
|
|
100
|
-
const diagnostics =
|
|
100
|
+
const diagnostics = contextNoTrailheadTypes.check(code, TEST_FILE);
|
|
101
101
|
expect(diagnostics.length).toBe(0);
|
|
102
102
|
});
|
|
103
103
|
});
|
|
@@ -110,7 +110,7 @@ describe('valid-detour-refs', () => {
|
|
|
110
110
|
const code = `
|
|
111
111
|
trail("entity.show", {
|
|
112
112
|
detours: [{ target: "entity.edit" }],
|
|
113
|
-
|
|
113
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
114
114
|
})`;
|
|
115
115
|
const diagnostics = validDetourRefs.check(code, TEST_FILE);
|
|
116
116
|
expect(diagnostics.length).toBe(1);
|
|
@@ -120,12 +120,12 @@ trail("entity.show", {
|
|
|
120
120
|
test('passes when detour target exists', () => {
|
|
121
121
|
const code = `
|
|
122
122
|
trail("entity.edit", {
|
|
123
|
-
|
|
123
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
trail("entity.show", {
|
|
127
127
|
detours: [{ target: "entity.edit" }],
|
|
128
|
-
|
|
128
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
129
129
|
})`;
|
|
130
130
|
const diagnostics = validDetourRefs.check(code, TEST_FILE);
|
|
131
131
|
expect(diagnostics.length).toBe(0);
|
|
@@ -135,7 +135,7 @@ trail("entity.show", {
|
|
|
135
135
|
const code = `
|
|
136
136
|
trail("entity.show", {
|
|
137
137
|
detours: [{ target: "entity.edit" }],
|
|
138
|
-
|
|
138
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
139
139
|
})`;
|
|
140
140
|
const context = { knownTrailIds: new Set(['entity.show', 'entity.edit']) };
|
|
141
141
|
const diagnostics = validDetourRefs.checkWithContext(
|
|
@@ -146,28 +146,28 @@ trail("entity.show", {
|
|
|
146
146
|
expect(diagnostics.length).toBe(0);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
test('flags detour target in trail with
|
|
149
|
+
test('flags detour target in trail with crossings that does not exist', () => {
|
|
150
150
|
const code = `
|
|
151
151
|
trail("entity.onboard", {
|
|
152
152
|
detours: [{ target: "entity.missing" }],
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
crosses: ["entity.create"],
|
|
154
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
155
155
|
})`;
|
|
156
156
|
const diagnostics = validDetourRefs.check(code, TEST_FILE);
|
|
157
157
|
expect(diagnostics.length).toBe(1);
|
|
158
158
|
expect(diagnostics[0]?.message).toContain('entity.missing');
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
test('passes when trail with
|
|
161
|
+
test('passes when trail with crossings detour target exists', () => {
|
|
162
162
|
const code = `
|
|
163
163
|
trail("entity.fallback", {
|
|
164
|
-
|
|
164
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
165
165
|
})
|
|
166
166
|
|
|
167
167
|
trail("entity.onboard", {
|
|
168
168
|
detours: [{ target: "entity.fallback" }],
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
crosses: ["entity.create"],
|
|
170
|
+
blaze: async (input, ctx) => Result.ok(data)
|
|
171
171
|
})`;
|
|
172
172
|
const diagnostics = validDetourRefs.check(code, TEST_FILE);
|
|
173
173
|
expect(diagnostics.length).toBe(0);
|
|
@@ -178,27 +178,27 @@ trail("entity.onboard", {
|
|
|
178
178
|
// no-direct-impl-in-route
|
|
179
179
|
// ---------------------------------------------------------------------------
|
|
180
180
|
describe('no-direct-impl-in-route', () => {
|
|
181
|
-
test('warns on direct .
|
|
181
|
+
test('warns on direct .blaze() call in trail with crossings', () => {
|
|
182
182
|
const code = `
|
|
183
183
|
trail("entity.onboard", {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const result = await entityCreate.
|
|
184
|
+
crosses: ["entity.create"],
|
|
185
|
+
blaze: async (input, ctx) => {
|
|
186
|
+
const result = await entityCreate.blaze(data);
|
|
187
187
|
return Result.ok(result);
|
|
188
188
|
}
|
|
189
189
|
})`;
|
|
190
190
|
const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
|
|
191
191
|
expect(diagnostics.length).toBe(1);
|
|
192
192
|
expect(diagnostics[0]?.severity).toBe('warn');
|
|
193
|
-
expect(diagnostics[0]?.message).toContain('ctx.
|
|
193
|
+
expect(diagnostics[0]?.message).toContain('ctx.cross');
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
test('allows ctx.
|
|
196
|
+
test('allows ctx.cross() calls', () => {
|
|
197
197
|
const code = `
|
|
198
198
|
trail("entity.onboard", {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const result = await ctx.
|
|
199
|
+
crosses: ["entity.create"],
|
|
200
|
+
blaze: async (input, ctx) => {
|
|
201
|
+
const result = await ctx.cross("entity.create", data);
|
|
202
202
|
return Result.ok(result);
|
|
203
203
|
}
|
|
204
204
|
})`;
|
|
@@ -206,11 +206,11 @@ trail("entity.onboard", {
|
|
|
206
206
|
expect(diagnostics.length).toBe(0);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
test('ignores trails without
|
|
209
|
+
test('ignores trails without crossings', () => {
|
|
210
210
|
const code = `
|
|
211
211
|
trail("entity.show", {
|
|
212
|
-
|
|
213
|
-
const result = await someTrail.
|
|
212
|
+
blaze: async (input, ctx) => {
|
|
213
|
+
const result = await someTrail.blaze(data);
|
|
214
214
|
return Result.ok(result);
|
|
215
215
|
}
|
|
216
216
|
})`;
|
|
@@ -220,7 +220,7 @@ trail("entity.show", {
|
|
|
220
220
|
|
|
221
221
|
test('ignores files without trail() calls', () => {
|
|
222
222
|
const code = `
|
|
223
|
-
const result = await someTrail.
|
|
223
|
+
const result = await someTrail.blaze(data);`;
|
|
224
224
|
const diagnostics = noDirectImplInRoute.check(code, TEST_FILE);
|
|
225
225
|
expect(diagnostics.length).toBe(0);
|
|
226
226
|
});
|