@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
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { crossDeclarations } from '../rules/cross-declarations.js';
|
|
4
4
|
|
|
5
5
|
const TEST_FILE = 'test.ts';
|
|
6
6
|
|
|
7
|
-
describe('
|
|
7
|
+
describe('cross-declarations', () => {
|
|
8
8
|
describe('clean cases', () => {
|
|
9
9
|
test('declared and called match exactly', () => {
|
|
10
10
|
const code = `
|
|
11
11
|
import { trail, Result } from '@ontrails/core';
|
|
12
12
|
const t = trail('onboard', {
|
|
13
|
-
|
|
13
|
+
crosses: ['entity.add', 'search'],
|
|
14
14
|
input: z.object({ name: z.string() }),
|
|
15
|
-
|
|
16
|
-
await ctx.
|
|
17
|
-
await ctx.
|
|
15
|
+
blaze: async (input, ctx) => {
|
|
16
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
17
|
+
await ctx.cross('search', { query: input.name });
|
|
18
18
|
return Result.ok({});
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
21
|
`;
|
|
22
22
|
|
|
23
|
-
const diagnostics =
|
|
23
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
24
24
|
|
|
25
25
|
expect(diagnostics.length).toBe(0);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test('no
|
|
28
|
+
test('no crosses declaration and no ctx.cross() calls', () => {
|
|
29
29
|
const code = `
|
|
30
30
|
trail('simple', {
|
|
31
31
|
input: z.object({ name: z.string() }),
|
|
32
|
-
|
|
32
|
+
blaze: async (input, ctx) => {
|
|
33
33
|
return Result.ok({ greeting: 'hello ' + input.name });
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
`;
|
|
37
37
|
|
|
38
|
-
const diagnostics =
|
|
38
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
39
39
|
|
|
40
40
|
expect(diagnostics.length).toBe(0);
|
|
41
41
|
});
|
|
@@ -46,20 +46,20 @@ trail('simple', {
|
|
|
46
46
|
const code = `
|
|
47
47
|
trail('onboard', {
|
|
48
48
|
input: z.object({ name: z.string() }),
|
|
49
|
-
|
|
50
|
-
await ctx.
|
|
49
|
+
blaze: async (input, ctx) => {
|
|
50
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
51
51
|
return Result.ok({});
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
`;
|
|
55
55
|
|
|
56
|
-
const diagnostics =
|
|
56
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
57
57
|
|
|
58
58
|
expect(diagnostics.length).toBe(1);
|
|
59
59
|
expect(diagnostics[0]?.severity).toBe('error');
|
|
60
|
-
expect(diagnostics[0]?.rule).toBe('
|
|
61
|
-
expect(diagnostics[0]?.message).toContain("ctx.
|
|
62
|
-
expect(diagnostics[0]?.message).toContain('not declared in
|
|
60
|
+
expect(diagnostics[0]?.rule).toBe('cross-declarations');
|
|
61
|
+
expect(diagnostics[0]?.message).toContain("ctx.cross('entity.add')");
|
|
62
|
+
expect(diagnostics[0]?.message).toContain('not declared in crosses');
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -67,56 +67,56 @@ trail('onboard', {
|
|
|
67
67
|
test('declared but not called produces warning', () => {
|
|
68
68
|
const code = `
|
|
69
69
|
trail('onboard', {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await ctx.
|
|
70
|
+
crosses: ['entity.add', 'search'],
|
|
71
|
+
blaze: async (input, ctx) => {
|
|
72
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
73
73
|
return Result.ok({});
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
76
|
`;
|
|
77
77
|
|
|
78
|
-
const diagnostics =
|
|
78
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
79
79
|
|
|
80
80
|
expect(diagnostics.length).toBe(1);
|
|
81
81
|
expect(diagnostics[0]?.severity).toBe('warn');
|
|
82
|
-
expect(diagnostics[0]?.rule).toBe('
|
|
83
|
-
expect(diagnostics[0]?.message).toContain("'search' declared in
|
|
82
|
+
expect(diagnostics[0]?.rule).toBe('cross-declarations');
|
|
83
|
+
expect(diagnostics[0]?.message).toContain("'search' declared in crosses");
|
|
84
84
|
expect(diagnostics[0]?.message).toContain('never called');
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
describe('single-object overload', () => {
|
|
89
|
-
test('recognizes trail({ id,
|
|
89
|
+
test('recognizes trail({ id, crosses, blaze }) form', () => {
|
|
90
90
|
const code = `
|
|
91
91
|
trail({
|
|
92
92
|
id: 'onboard',
|
|
93
|
-
|
|
93
|
+
crosses: ['entity.add'],
|
|
94
94
|
input: z.object({ name: z.string() }),
|
|
95
|
-
|
|
96
|
-
await ctx.
|
|
95
|
+
blaze: async (input, ctx) => {
|
|
96
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
97
97
|
return Result.ok({});
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
100
|
`;
|
|
101
101
|
|
|
102
|
-
const diagnostics =
|
|
102
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
103
103
|
|
|
104
104
|
expect(diagnostics.length).toBe(0);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
test('detects undeclared
|
|
107
|
+
test('detects undeclared crossings in single-object form', () => {
|
|
108
108
|
const code = `
|
|
109
109
|
trail({
|
|
110
110
|
id: 'onboard',
|
|
111
111
|
input: z.object({ name: z.string() }),
|
|
112
|
-
|
|
113
|
-
await ctx.
|
|
112
|
+
blaze: async (input, ctx) => {
|
|
113
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
114
114
|
return Result.ok({});
|
|
115
115
|
},
|
|
116
116
|
});
|
|
117
117
|
`;
|
|
118
118
|
|
|
119
|
-
const diagnostics =
|
|
119
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
120
120
|
|
|
121
121
|
expect(diagnostics.length).toBe(1);
|
|
122
122
|
expect(diagnostics[0]?.severity).toBe('error');
|
|
@@ -125,112 +125,112 @@ trail({
|
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
describe('context parameter naming', () => {
|
|
128
|
-
test('recognizes context.
|
|
128
|
+
test('recognizes context.cross() when second param is named context', () => {
|
|
129
129
|
const code = `
|
|
130
130
|
trail('onboard', {
|
|
131
|
-
|
|
131
|
+
crosses: ['entity.add'],
|
|
132
132
|
input: z.object({ name: z.string() }),
|
|
133
|
-
|
|
134
|
-
await context.
|
|
133
|
+
blaze: async (input, context) => {
|
|
134
|
+
await context.cross('entity.add', { name: input.name });
|
|
135
135
|
return Result.ok({});
|
|
136
136
|
},
|
|
137
137
|
});
|
|
138
138
|
`;
|
|
139
139
|
|
|
140
|
-
const diagnostics =
|
|
140
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
141
141
|
|
|
142
142
|
expect(diagnostics.length).toBe(0);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
test('detects undeclared context.
|
|
145
|
+
test('detects undeclared context.cross() calls', () => {
|
|
146
146
|
const code = `
|
|
147
147
|
trail('onboard', {
|
|
148
148
|
input: z.object({ name: z.string() }),
|
|
149
|
-
|
|
150
|
-
await context.
|
|
149
|
+
blaze: async (input, context) => {
|
|
150
|
+
await context.cross('entity.add', { name: input.name });
|
|
151
151
|
return Result.ok({});
|
|
152
152
|
},
|
|
153
153
|
});
|
|
154
154
|
`;
|
|
155
155
|
|
|
156
|
-
const diagnostics =
|
|
156
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
157
157
|
|
|
158
158
|
expect(diagnostics.length).toBe(1);
|
|
159
159
|
expect(diagnostics[0]?.severity).toBe('error');
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
test('recognizes destructured
|
|
162
|
+
test('recognizes destructured cross() calls', () => {
|
|
163
163
|
const code = `
|
|
164
164
|
trail('onboard', {
|
|
165
|
-
|
|
165
|
+
crosses: ['entity.add'],
|
|
166
166
|
input: z.object({ name: z.string() }),
|
|
167
|
-
|
|
168
|
-
const {
|
|
169
|
-
await
|
|
167
|
+
blaze: async (input, ctx) => {
|
|
168
|
+
const { cross } = ctx;
|
|
169
|
+
await cross('entity.add', { name: input.name });
|
|
170
170
|
return Result.ok({});
|
|
171
171
|
},
|
|
172
172
|
});
|
|
173
173
|
`;
|
|
174
174
|
|
|
175
|
-
const diagnostics =
|
|
175
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
176
176
|
|
|
177
177
|
expect(diagnostics.length).toBe(0);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
describe('nested run false positives', () => {
|
|
182
|
-
test('
|
|
182
|
+
test('meta.run does not trigger false positives', () => {
|
|
183
183
|
const code = `
|
|
184
184
|
trail('onboard', {
|
|
185
|
-
|
|
185
|
+
crosses: ['entity.add'],
|
|
186
186
|
input: z.object({ name: z.string() }),
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
await ctx.
|
|
187
|
+
meta: { blaze: async () => ctx.cross('phantom') },
|
|
188
|
+
blaze: async (input, ctx) => {
|
|
189
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
190
190
|
return Result.ok({});
|
|
191
191
|
},
|
|
192
192
|
});
|
|
193
193
|
`;
|
|
194
194
|
|
|
195
|
-
const diagnostics =
|
|
195
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
196
196
|
|
|
197
197
|
expect(diagnostics.length).toBe(0);
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
-
describe('identifier resolution in
|
|
202
|
-
test('resolves const identifiers in
|
|
201
|
+
describe('identifier resolution in crosses arrays', () => {
|
|
202
|
+
test('resolves const identifiers in crosses array', () => {
|
|
203
203
|
const code = `
|
|
204
204
|
const ENTITY_ADD = 'entity.add';
|
|
205
205
|
trail('onboard', {
|
|
206
|
-
|
|
206
|
+
crosses: [ENTITY_ADD],
|
|
207
207
|
input: z.object({ name: z.string() }),
|
|
208
|
-
|
|
209
|
-
await ctx.
|
|
208
|
+
blaze: async (input, ctx) => {
|
|
209
|
+
await ctx.cross('entity.add', { name: input.name });
|
|
210
210
|
return Result.ok({});
|
|
211
211
|
},
|
|
212
212
|
});
|
|
213
213
|
`;
|
|
214
214
|
|
|
215
|
-
const diagnostics =
|
|
215
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
216
216
|
|
|
217
217
|
expect(diagnostics.length).toBe(0);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
test('reports error when resolved identifier does not match called
|
|
220
|
+
test('reports error when resolved identifier does not match called cross', () => {
|
|
221
221
|
const code = `
|
|
222
222
|
const ENTITY_ADD = 'entity.add';
|
|
223
223
|
trail('onboard', {
|
|
224
|
-
|
|
224
|
+
crosses: [ENTITY_ADD],
|
|
225
225
|
input: z.object({ name: z.string() }),
|
|
226
|
-
|
|
227
|
-
await ctx.
|
|
226
|
+
blaze: async (input, ctx) => {
|
|
227
|
+
await ctx.cross('search', { name: input.name });
|
|
228
228
|
return Result.ok({});
|
|
229
229
|
},
|
|
230
230
|
});
|
|
231
231
|
`;
|
|
232
232
|
|
|
233
|
-
const diagnostics =
|
|
233
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
234
234
|
|
|
235
235
|
// 'search' called but not declared, 'entity.add' declared but not called
|
|
236
236
|
expect(diagnostics.length).toBe(2);
|
|
@@ -238,20 +238,20 @@ trail('onboard', {
|
|
|
238
238
|
});
|
|
239
239
|
|
|
240
240
|
describe('edge cases', () => {
|
|
241
|
-
test('dynamic
|
|
241
|
+
test('dynamic cross IDs are skipped', () => {
|
|
242
242
|
const code = `
|
|
243
243
|
trail('dispatch', {
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
crosses: ['entity.add'],
|
|
245
|
+
blaze: async (input, ctx) => {
|
|
246
246
|
const trailId = input.target;
|
|
247
|
-
await ctx.
|
|
248
|
-
await ctx.
|
|
247
|
+
await ctx.cross(trailId, input);
|
|
248
|
+
await ctx.cross('entity.add', input);
|
|
249
249
|
return Result.ok({});
|
|
250
250
|
},
|
|
251
251
|
});
|
|
252
252
|
`;
|
|
253
253
|
|
|
254
|
-
const diagnostics =
|
|
254
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
255
255
|
|
|
256
256
|
expect(diagnostics.length).toBe(0);
|
|
257
257
|
});
|
|
@@ -259,22 +259,22 @@ trail('dispatch', {
|
|
|
259
259
|
test('multiple trails in one file are validated independently', () => {
|
|
260
260
|
const code = `
|
|
261
261
|
trail('alpha', {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
await ctx.
|
|
262
|
+
crosses: ['shared'],
|
|
263
|
+
blaze: async (input, ctx) => {
|
|
264
|
+
await ctx.cross('shared', input);
|
|
265
265
|
return Result.ok({});
|
|
266
266
|
},
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
trail('beta', {
|
|
270
|
-
|
|
271
|
-
await ctx.
|
|
270
|
+
blaze: async (input, ctx) => {
|
|
271
|
+
await ctx.cross('undeclared', input);
|
|
272
272
|
return Result.ok({});
|
|
273
273
|
},
|
|
274
274
|
});
|
|
275
275
|
`;
|
|
276
276
|
|
|
277
|
-
const diagnostics =
|
|
277
|
+
const diagnostics = crossDeclarations.check(code, TEST_FILE);
|
|
278
278
|
|
|
279
279
|
expect(diagnostics.length).toBe(1);
|
|
280
280
|
expect(diagnostics[0]?.message).toContain('Trail "beta"');
|
|
@@ -285,14 +285,14 @@ trail('beta', {
|
|
|
285
285
|
test('skips test files', () => {
|
|
286
286
|
const code = `
|
|
287
287
|
trail('onboard', {
|
|
288
|
-
|
|
289
|
-
await ctx.
|
|
288
|
+
blaze: async (input, ctx) => {
|
|
289
|
+
await ctx.cross('entity.add', input);
|
|
290
290
|
return Result.ok({});
|
|
291
291
|
},
|
|
292
292
|
});
|
|
293
293
|
`;
|
|
294
294
|
|
|
295
|
-
const diagnostics =
|
|
295
|
+
const diagnostics = crossDeclarations.check(
|
|
296
296
|
code,
|
|
297
297
|
'src/__tests__/trails.test.ts'
|
|
298
298
|
);
|
|
@@ -4,16 +4,16 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
|
|
6
6
|
import { trail, topo, Result } from '@ontrails/core';
|
|
7
|
-
import {
|
|
7
|
+
import { hashTrailheadMap, generateTrailheadMap } from '@ontrails/schema';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
|
|
10
10
|
import { checkDrift } from '../drift.js';
|
|
11
11
|
|
|
12
12
|
const makeTopo = () => {
|
|
13
13
|
const t = trail('test.hello', {
|
|
14
|
+
blaze: () => Result.ok({ greeting: 'hi' }),
|
|
14
15
|
input: z.object({ name: z.string() }),
|
|
15
16
|
output: z.object({ greeting: z.string() }),
|
|
16
|
-
run: () => Result.ok({ greeting: 'hi' }),
|
|
17
17
|
});
|
|
18
18
|
return topo('test-app', { t });
|
|
19
19
|
};
|
|
@@ -47,8 +47,8 @@ describe('checkDrift', () => {
|
|
|
47
47
|
const dir = createTempDir();
|
|
48
48
|
try {
|
|
49
49
|
const tp = makeTopo();
|
|
50
|
-
const hash =
|
|
51
|
-
writeFileSync(join(dir, '
|
|
50
|
+
const hash = hashTrailheadMap(generateTrailheadMap(tp));
|
|
51
|
+
writeFileSync(join(dir, 'trailhead.lock'), `${hash}\n`);
|
|
52
52
|
|
|
53
53
|
const result = await checkDrift(dir, tp);
|
|
54
54
|
expect(result.stale).toBe(false);
|
|
@@ -62,7 +62,7 @@ describe('checkDrift', () => {
|
|
|
62
62
|
test('returns stale: true when lock does not match', async () => {
|
|
63
63
|
const dir = createTempDir();
|
|
64
64
|
try {
|
|
65
|
-
writeFileSync(join(dir, '
|
|
65
|
+
writeFileSync(join(dir, 'trailhead.lock'), 'outdated-hash\n');
|
|
66
66
|
|
|
67
67
|
const result = await checkDrift(dir, makeTopo());
|
|
68
68
|
expect(result.stale).toBe(true);
|
|
@@ -67,7 +67,7 @@ describe('formatGitHubAnnotations', () => {
|
|
|
67
67
|
|
|
68
68
|
test('emits drift as a single ::error annotation', () => {
|
|
69
69
|
const output = formatGitHubAnnotations(reportWithDrift);
|
|
70
|
-
expect(output).toContain('::error::drift:
|
|
70
|
+
expect(output).toContain('::error::drift: trailhead.lock is stale');
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
test('produces one line per diagnostic', () => {
|
|
@@ -148,7 +148,7 @@ describe('formatSummary', () => {
|
|
|
148
148
|
test('includes drift section when stale', () => {
|
|
149
149
|
const output = formatSummary(reportWithDrift);
|
|
150
150
|
expect(output).toContain('### Drift');
|
|
151
|
-
expect(output).toContain('
|
|
151
|
+
expect(output).toContain('trailhead.lock is stale');
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
test('omits drift section when clean', () => {
|
|
@@ -8,7 +8,7 @@ describe('implementation-returns-result', () => {
|
|
|
8
8
|
test('flags raw object return in trail implementation', () => {
|
|
9
9
|
const code = `
|
|
10
10
|
trail("entity.show", {
|
|
11
|
-
|
|
11
|
+
blaze: async (input, ctx) => {
|
|
12
12
|
return { name: "foo" };
|
|
13
13
|
}
|
|
14
14
|
})`;
|
|
@@ -20,18 +20,18 @@ trail("entity.show", {
|
|
|
20
20
|
expect(diagnostics[0]?.severity).toBe('error');
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
test('allows Result.ok() and returning ctx.
|
|
23
|
+
test('allows Result.ok() and returning ctx.cross() results', () => {
|
|
24
24
|
const code = `
|
|
25
25
|
trail("entity.onboard", {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const result = await ctx.
|
|
26
|
+
crosses: ["entity.create"],
|
|
27
|
+
blaze: async (input, ctx) => {
|
|
28
|
+
const result = await ctx.cross("entity.create", input);
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
trail("entity.create", {
|
|
34
|
-
|
|
34
|
+
blaze: async (input, ctx) => Result.ok({ id: "123" })
|
|
35
35
|
})`;
|
|
36
36
|
|
|
37
37
|
const diagnostics = implementationReturnsResult.check(code, TEST_FILE);
|
|
@@ -42,7 +42,7 @@ trail("entity.create", {
|
|
|
42
42
|
test('flags concise raw implementation bodies', () => {
|
|
43
43
|
const code = `
|
|
44
44
|
trail("entity.create", {
|
|
45
|
-
|
|
45
|
+
blaze: async (input, ctx) => ({ id: "123" })
|
|
46
46
|
})`;
|
|
47
47
|
|
|
48
48
|
const diagnostics = implementationReturnsResult.check(code, TEST_FILE);
|
|
@@ -54,7 +54,7 @@ trail("entity.create", {
|
|
|
54
54
|
test('ignores return statements inside nested callbacks like .map()', () => {
|
|
55
55
|
const code = `
|
|
56
56
|
trail("entity.list", {
|
|
57
|
-
|
|
57
|
+
blaze: async (input, ctx) => {
|
|
58
58
|
const items = ["a", "b", "c"];
|
|
59
59
|
const mapped = items.map((item) => {
|
|
60
60
|
return { name: item };
|
|
@@ -74,7 +74,7 @@ trail("entity.list", {
|
|
|
74
74
|
test('ignores return statements inside .then() callbacks', () => {
|
|
75
75
|
const code = `
|
|
76
76
|
trail("entity.fetch", {
|
|
77
|
-
|
|
77
|
+
blaze: async (input, ctx) => {
|
|
78
78
|
const data = await somePromise.then((res) => {
|
|
79
79
|
return res.json();
|
|
80
80
|
});
|
|
@@ -90,7 +90,7 @@ trail("entity.fetch", {
|
|
|
90
90
|
test('still flags raw returns at the implementation level', () => {
|
|
91
91
|
const code = `
|
|
92
92
|
trail("entity.list", {
|
|
93
|
-
|
|
93
|
+
blaze: async (input, ctx) => {
|
|
94
94
|
const items = ["a", "b"].map((item) => {
|
|
95
95
|
return { name: item };
|
|
96
96
|
});
|
|
@@ -113,7 +113,7 @@ const buildDiff = async (): Promise<Result<object, Error>> =>
|
|
|
113
113
|
Result.ok({ breaking: [] });
|
|
114
114
|
|
|
115
115
|
trail("survey", {
|
|
116
|
-
|
|
116
|
+
blaze: async (input, ctx) => {
|
|
117
117
|
if (input.diff) {
|
|
118
118
|
return await buildDiff();
|
|
119
119
|
}
|
|
@@ -8,11 +8,11 @@ describe('no-direct-implementation-call', () => {
|
|
|
8
8
|
import { trail, Result } from "@ontrails/core";
|
|
9
9
|
|
|
10
10
|
const entityShow = trail("entity.show", {
|
|
11
|
-
|
|
11
|
+
blaze: async (input, ctx) => Result.ok({ id: input.id }),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
async function run() {
|
|
15
|
-
const result = await entityShow.
|
|
15
|
+
const result = await entityShow.blaze({ id: "1" }, ctx);
|
|
16
16
|
return result;
|
|
17
17
|
}`;
|
|
18
18
|
|
|
@@ -21,15 +21,15 @@ async function run() {
|
|
|
21
21
|
expect(diagnostics).toHaveLength(1);
|
|
22
22
|
expect(diagnostics[0]?.rule).toBe('no-direct-implementation-call');
|
|
23
23
|
expect(diagnostics[0]?.severity).toBe('warn');
|
|
24
|
-
expect(diagnostics[0]?.message).toContain('ctx.
|
|
24
|
+
expect(diagnostics[0]?.message).toContain('ctx.cross');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
test('allows ctx.
|
|
27
|
+
test('allows ctx.cross() calls', () => {
|
|
28
28
|
const code = `
|
|
29
29
|
trail("entity.onboard", {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const result = await ctx.
|
|
30
|
+
crosses: ["entity.create"],
|
|
31
|
+
blaze: async (input, ctx) => {
|
|
32
|
+
const result = await ctx.cross("entity.create", input);
|
|
33
33
|
return Result.ok(result);
|
|
34
34
|
},
|
|
35
35
|
});
|
|
@@ -43,7 +43,7 @@ trail("entity.onboard", {
|
|
|
43
43
|
test('ignores test files', () => {
|
|
44
44
|
const code = `
|
|
45
45
|
async function run() {
|
|
46
|
-
return await entityShow.
|
|
46
|
+
return await entityShow.blaze({ id: "1" }, ctx);
|
|
47
47
|
}`;
|
|
48
48
|
|
|
49
49
|
const diagnostics = noDirectImplementationCall.check(
|
|
@@ -57,7 +57,7 @@ async function run() {
|
|
|
57
57
|
test('ignores framework internals that intentionally call implementations', () => {
|
|
58
58
|
const code = `
|
|
59
59
|
export async function run() {
|
|
60
|
-
return await entityShow.
|
|
60
|
+
return await entityShow.blaze({ id: "1" }, ctx);
|
|
61
61
|
}`;
|
|
62
62
|
|
|
63
63
|
const diagnostics = noDirectImplementationCall.check(
|
|
@@ -70,7 +70,7 @@ export async function run() {
|
|
|
70
70
|
|
|
71
71
|
test('ignores implementation references inside template strings', () => {
|
|
72
72
|
const code = `
|
|
73
|
-
const generated = \`const result = await entityShow.
|
|
73
|
+
const generated = \`const result = await entityShow.blaze({ id: "1" }, ctx);\`;
|
|
74
74
|
`;
|
|
75
75
|
|
|
76
76
|
const diagnostics = noDirectImplementationCall.check(
|
|
@@ -6,7 +6,7 @@ describe('no-sync-result-assumption', () => {
|
|
|
6
6
|
test('flags direct result access on implementation calls', () => {
|
|
7
7
|
const code = `
|
|
8
8
|
async function run() {
|
|
9
|
-
const isOk = entityShow.
|
|
9
|
+
const isOk = entityShow.blaze({ id: "1" }, ctx).isOk();
|
|
10
10
|
return isOk;
|
|
11
11
|
}`;
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ async function run() {
|
|
|
20
20
|
|
|
21
21
|
test('flags a stored implementation result that is used synchronously', () => {
|
|
22
22
|
const code = `
|
|
23
|
-
const result = entityShow.
|
|
23
|
+
const result = entityShow.blaze({ id: "1" }, ctx);
|
|
24
24
|
|
|
25
25
|
if (result.isOk()) {
|
|
26
26
|
console.log("ok");
|
|
@@ -35,7 +35,7 @@ if (result.isOk()) {
|
|
|
35
35
|
test('allows awaited implementation calls before result access', () => {
|
|
36
36
|
const code = `
|
|
37
37
|
async function run() {
|
|
38
|
-
const result = await entityShow.
|
|
38
|
+
const result = await entityShow.blaze({ id: "1" }, ctx);
|
|
39
39
|
return result.isOk();
|
|
40
40
|
}`;
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@ async function run() {
|
|
|
47
47
|
test('allows awaited implementation calls when the property access is chained', () => {
|
|
48
48
|
const code = `
|
|
49
49
|
async function run() {
|
|
50
|
-
return (await entityShow.
|
|
50
|
+
return (await entityShow.blaze({ id: "1" }, ctx)).isOk();
|
|
51
51
|
}`;
|
|
52
52
|
|
|
53
53
|
const diagnostics = noSyncResultAssumption.check(code, 'src/app.ts');
|
|
@@ -57,7 +57,7 @@ async function run() {
|
|
|
57
57
|
|
|
58
58
|
test('ignores test files', () => {
|
|
59
59
|
const code = `
|
|
60
|
-
const result = entityShow.
|
|
60
|
+
const result = entityShow.blaze({ id: "1" }, ctx);
|
|
61
61
|
result.isOk();
|
|
62
62
|
`;
|
|
63
63
|
|
|
@@ -71,7 +71,7 @@ result.isOk();
|
|
|
71
71
|
|
|
72
72
|
test('ignores framework internals that intentionally call implementations', () => {
|
|
73
73
|
const code = `
|
|
74
|
-
const result = entityShow.
|
|
74
|
+
const result = entityShow.blaze({ id: "1" }, ctx);
|
|
75
75
|
result.isOk();
|
|
76
76
|
`;
|
|
77
77
|
|
|
@@ -9,11 +9,11 @@ describe('no-throw-in-detour-target', () => {
|
|
|
9
9
|
const code = `
|
|
10
10
|
trail("entity.show", {
|
|
11
11
|
detours: { NotFoundError: ["entity.fallback"] },
|
|
12
|
-
|
|
12
|
+
blaze: async (input, ctx) => Result.ok({ id: "123" })
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
trail("entity.fallback", {
|
|
16
|
-
|
|
16
|
+
blaze: async (input, ctx) => {
|
|
17
17
|
throw new Error("boom");
|
|
18
18
|
}
|
|
19
19
|
})`;
|
|
@@ -28,7 +28,7 @@ trail("entity.fallback", {
|
|
|
28
28
|
test('allows throw in implementations that are not detour targets', () => {
|
|
29
29
|
const code = `
|
|
30
30
|
trail("entity.show", {
|
|
31
|
-
|
|
31
|
+
blaze: async (input, ctx) => {
|
|
32
32
|
throw new Error("boom");
|
|
33
33
|
}
|
|
34
34
|
})`;
|
|
@@ -42,11 +42,11 @@ trail("entity.show", {
|
|
|
42
42
|
const code = `
|
|
43
43
|
trail("entity.show", {
|
|
44
44
|
detours: { NotFoundError: ["entity.fallback"] },
|
|
45
|
-
|
|
45
|
+
blaze: async (input, ctx) => Result.ok({ id: "123" })
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
trail("entity.fallback", {
|
|
49
|
-
|
|
49
|
+
blaze: async (input, ctx) => { throw new Error("boom"); }
|
|
50
50
|
})`;
|
|
51
51
|
|
|
52
52
|
const diagnostics = noThrowInDetourTarget.check(code, TEST_FILE);
|
|
@@ -58,7 +58,7 @@ trail("entity.fallback", {
|
|
|
58
58
|
test('uses project context when the detour target is defined in another file', () => {
|
|
59
59
|
const code = `
|
|
60
60
|
trail("entity.fallback", {
|
|
61
|
-
|
|
61
|
+
blaze: async (input, ctx) => {
|
|
62
62
|
throw new Error("boom");
|
|
63
63
|
}
|
|
64
64
|
})`;
|