@ontrails/trails 1.0.0-beta.15 → 1.0.0-beta.16
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 +197 -2
- package/README.md +27 -0
- package/package.json +19 -8
- package/src/app.ts +15 -5
- package/src/cli.ts +303 -10
- package/src/completions.ts +240 -0
- package/src/load-app-mirror.ts +160 -0
- package/src/local-state-io.ts +153 -0
- package/src/project-writes.ts +320 -0
- package/src/run-collision.ts +125 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/add-surface.ts +45 -23
- package/src/trails/add-trail.ts +27 -17
- package/src/trails/add-verify.ts +57 -17
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-scaffold.ts +86 -33
- package/src/trails/create.ts +11 -3
- package/src/trails/dev-clean.ts +6 -1
- package/src/trails/dev-reset.ts +6 -1
- package/src/trails/dev-stats.ts +6 -1
- package/src/trails/dev-support.ts +29 -17
- package/src/trails/draft-promote.ts +289 -80
- package/src/trails/guide.ts +54 -34
- package/src/trails/load-app.ts +251 -56
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +482 -0
- package/src/trails/run-examples.ts +141 -0
- package/src/trails/run.ts +403 -0
- package/src/trails/survey.ts +506 -200
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-compile.ts +55 -0
- package/src/trails/topo-history.ts +6 -1
- package/src/trails/topo-output-schemas.ts +175 -0
- package/src/trails/topo-pin.ts +19 -6
- package/src/trails/topo-read-support.ts +171 -228
- package/src/trails/topo-reports.ts +400 -25
- package/src/trails/topo-store-support.ts +43 -19
- package/src/trails/topo-support.ts +18 -28
- package/src/trails/topo-unpin.ts +6 -1
- package/src/trails/topo-verify.ts +18 -5
- package/src/trails/topo.ts +60 -23
- package/src/trails/warden-guide.ts +121 -0
- package/src/trails/warden.ts +137 -56
- package/src/versions.ts +3 -18
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -45
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -22
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -14
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -110
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -12
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -104
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-trailhead.d.ts +0 -13
- package/dist/src/trails/add-trailhead.d.ts.map +0 -1
- package/dist/src/trails/add-trailhead.js +0 -88
- package/dist/src/trails/add-trailhead.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -68
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -295
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -18
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -126
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/dev-clean.d.ts +0 -9
- package/dist/src/trails/dev-clean.d.ts.map +0 -1
- package/dist/src/trails/dev-clean.js +0 -66
- package/dist/src/trails/dev-clean.js.map +0 -1
- package/dist/src/trails/dev-reset.d.ts +0 -6
- package/dist/src/trails/dev-reset.d.ts.map +0 -1
- package/dist/src/trails/dev-reset.js +0 -39
- package/dist/src/trails/dev-reset.js.map +0 -1
- package/dist/src/trails/dev-stats.d.ts +0 -7
- package/dist/src/trails/dev-stats.d.ts.map +0 -1
- package/dist/src/trails/dev-stats.js +0 -61
- package/dist/src/trails/dev-stats.js.map +0 -1
- package/dist/src/trails/dev-support.d.ts +0 -64
- package/dist/src/trails/dev-support.d.ts.map +0 -1
- package/dist/src/trails/dev-support.js +0 -181
- package/dist/src/trails/dev-support.js.map +0 -1
- package/dist/src/trails/draft-promote.d.ts +0 -18
- package/dist/src/trails/draft-promote.d.ts.map +0 -1
- package/dist/src/trails/draft-promote.js +0 -400
- package/dist/src/trails/draft-promote.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -21
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -61
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -12
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -415
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -54
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -18
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -234
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/topo-constants.d.ts +0 -3
- package/dist/src/trails/topo-constants.d.ts.map +0 -1
- package/dist/src/trails/topo-constants.js +0 -3
- package/dist/src/trails/topo-constants.js.map +0 -1
- package/dist/src/trails/topo-export.d.ts +0 -19
- package/dist/src/trails/topo-export.d.ts.map +0 -1
- package/dist/src/trails/topo-export.js +0 -31
- package/dist/src/trails/topo-export.js.map +0 -1
- package/dist/src/trails/topo-history.d.ts +0 -20
- package/dist/src/trails/topo-history.d.ts.map +0 -1
- package/dist/src/trails/topo-history.js +0 -32
- package/dist/src/trails/topo-history.js.map +0 -1
- package/dist/src/trails/topo-pin.d.ts +0 -17
- package/dist/src/trails/topo-pin.d.ts.map +0 -1
- package/dist/src/trails/topo-pin.js +0 -31
- package/dist/src/trails/topo-pin.js.map +0 -1
- package/dist/src/trails/topo-read-support.d.ts +0 -58
- package/dist/src/trails/topo-read-support.d.ts.map +0 -1
- package/dist/src/trails/topo-read-support.js +0 -167
- package/dist/src/trails/topo-read-support.js.map +0 -1
- package/dist/src/trails/topo-reports.d.ts +0 -54
- package/dist/src/trails/topo-reports.d.ts.map +0 -1
- package/dist/src/trails/topo-reports.js +0 -128
- package/dist/src/trails/topo-reports.js.map +0 -1
- package/dist/src/trails/topo-show.d.ts +0 -23
- package/dist/src/trails/topo-show.d.ts.map +0 -1
- package/dist/src/trails/topo-show.js +0 -49
- package/dist/src/trails/topo-show.js.map +0 -1
- package/dist/src/trails/topo-store-support.d.ts +0 -13
- package/dist/src/trails/topo-store-support.d.ts.map +0 -1
- package/dist/src/trails/topo-store-support.js +0 -55
- package/dist/src/trails/topo-store-support.js.map +0 -1
- package/dist/src/trails/topo-support.d.ts +0 -76
- package/dist/src/trails/topo-support.d.ts.map +0 -1
- package/dist/src/trails/topo-support.js +0 -132
- package/dist/src/trails/topo-support.js.map +0 -1
- package/dist/src/trails/topo-unpin.d.ts +0 -20
- package/dist/src/trails/topo-unpin.d.ts.map +0 -1
- package/dist/src/trails/topo-unpin.js +0 -44
- package/dist/src/trails/topo-unpin.js.map +0 -1
- package/dist/src/trails/topo-verify.d.ts +0 -5
- package/dist/src/trails/topo-verify.d.ts.map +0 -1
- package/dist/src/trails/topo-verify.js +0 -24
- package/dist/src/trails/topo-verify.js.map +0 -1
- package/dist/src/trails/topo.d.ts +0 -5
- package/dist/src/trails/topo.d.ts.map +0 -1
- package/dist/src/trails/topo.js +0 -63
- package/dist/src/trails/topo.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -20
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -98
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/src/versions.d.ts +0 -12
- package/dist/src/versions.d.ts.map +0 -1
- package/dist/src/versions.js +0 -23
- package/dist/src/versions.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/add-trail.test.ts +0 -97
- package/src/__tests__/create.test.ts +0 -415
- package/src/__tests__/draft-promote.test.ts +0 -144
- package/src/__tests__/guide.test.ts +0 -96
- package/src/__tests__/load-app.test.ts +0 -419
- package/src/__tests__/survey.test.ts +0 -377
- package/src/__tests__/topo-dev.test.ts +0 -426
- package/src/__tests__/warden.test.ts +0 -74
- package/src/trails/topo-export.ts +0 -35
- package/src/trails/topo-show.ts +0 -54
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from 'node:fs';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import { basename, dirname, join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
import { Result } from '@ontrails/core';
|
|
13
|
-
|
|
14
|
-
import { addSurface } from '../trails/add-surface.js';
|
|
15
|
-
import { addVerify } from '../trails/add-verify.js';
|
|
16
|
-
import { createRoute } from '../trails/create.js';
|
|
17
|
-
import { createScaffold } from '../trails/create-scaffold.js';
|
|
18
|
-
import { isInsideProject } from '../trails/project.js';
|
|
19
|
-
import {
|
|
20
|
-
ontrailsPackageRange,
|
|
21
|
-
scaffoldDependencyVersions,
|
|
22
|
-
} from '../versions.js';
|
|
23
|
-
|
|
24
|
-
type Starter = 'empty' | 'entity' | 'hello';
|
|
25
|
-
type Surface = 'cli' | 'http' | 'mcp';
|
|
26
|
-
|
|
27
|
-
const makeTempProject = (): string =>
|
|
28
|
-
join(
|
|
29
|
-
tmpdir(),
|
|
30
|
-
`trails-create-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const readJson = (dir: string, relativePath: string): Record<string, unknown> =>
|
|
34
|
-
JSON.parse(readFileSync(join(dir, relativePath), 'utf8')) as Record<
|
|
35
|
-
string,
|
|
36
|
-
unknown
|
|
37
|
-
>;
|
|
38
|
-
|
|
39
|
-
const readText = (dir: string, relativePath: string): string =>
|
|
40
|
-
readFileSync(join(dir, relativePath), 'utf8');
|
|
41
|
-
|
|
42
|
-
const expectPaths = (
|
|
43
|
-
dir: string,
|
|
44
|
-
relativePaths: readonly string[],
|
|
45
|
-
expected: boolean
|
|
46
|
-
): void => {
|
|
47
|
-
for (const relativePath of relativePaths) {
|
|
48
|
-
expect(existsSync(join(dir, relativePath))).toBe(expected);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const expectContainsAll = (
|
|
53
|
-
content: string,
|
|
54
|
-
snippets: readonly string[]
|
|
55
|
-
): void => {
|
|
56
|
-
for (const snippet of snippets) {
|
|
57
|
-
expect(content).toContain(snippet);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const expectOk = <T>(result: Result<T, Error>): T => {
|
|
62
|
-
if (result.isErr()) {
|
|
63
|
-
throw result.error;
|
|
64
|
-
}
|
|
65
|
-
return result.value;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const expectErr = <T>(result: Result<T, Error>): Error => {
|
|
69
|
-
if (result.isOk()) {
|
|
70
|
-
throw new Error('Expected error result');
|
|
71
|
-
}
|
|
72
|
-
return result.error;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const runCross = async (
|
|
76
|
-
id: string,
|
|
77
|
-
input: unknown
|
|
78
|
-
): Promise<Result<unknown, Error>> => {
|
|
79
|
-
switch (id) {
|
|
80
|
-
case 'create.scaffold': {
|
|
81
|
-
return await createScaffold.blaze(input as never, {} as never);
|
|
82
|
-
}
|
|
83
|
-
case 'add.surface': {
|
|
84
|
-
return await addSurface.blaze(input as never, {} as never);
|
|
85
|
-
}
|
|
86
|
-
case 'add.verify': {
|
|
87
|
-
return await addVerify.blaze(input as never, {} as never);
|
|
88
|
-
}
|
|
89
|
-
default: {
|
|
90
|
-
return Result.err(new Error(`Unknown cross target: ${id}`));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const runCreate = (
|
|
96
|
-
projectDir: string,
|
|
97
|
-
overrides?: Partial<{
|
|
98
|
-
starter: Starter;
|
|
99
|
-
surfaces: readonly Surface[];
|
|
100
|
-
verify: boolean;
|
|
101
|
-
}>
|
|
102
|
-
) =>
|
|
103
|
-
createRoute.blaze(
|
|
104
|
-
{
|
|
105
|
-
dir: dirname(projectDir),
|
|
106
|
-
name: basename(projectDir),
|
|
107
|
-
starter: overrides?.starter ?? 'hello',
|
|
108
|
-
surfaces: [...(overrides?.surfaces ?? ['cli'])],
|
|
109
|
-
verify: overrides?.verify ?? true,
|
|
110
|
-
},
|
|
111
|
-
{ cross: runCross } as never
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const setupMinimalProject = (dir: string): void => {
|
|
115
|
-
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
116
|
-
mkdirSync(join(dir, '.trails'), { recursive: true });
|
|
117
|
-
writeFileSync(
|
|
118
|
-
join(dir, 'src', 'app.ts'),
|
|
119
|
-
"import { topo } from '@ontrails/core';\nexport const app = topo('test');\n"
|
|
120
|
-
);
|
|
121
|
-
writeFileSync(
|
|
122
|
-
join(dir, 'package.json'),
|
|
123
|
-
JSON.stringify(
|
|
124
|
-
{
|
|
125
|
-
dependencies: { '@ontrails/core': ontrailsPackageRange },
|
|
126
|
-
name: 'test',
|
|
127
|
-
},
|
|
128
|
-
null,
|
|
129
|
-
2
|
|
130
|
-
)
|
|
131
|
-
);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const assertDefaultProjectFiles = (dir: string): void => {
|
|
135
|
-
expectPaths(
|
|
136
|
-
dir,
|
|
137
|
-
[
|
|
138
|
-
'package.json',
|
|
139
|
-
'tsconfig.json',
|
|
140
|
-
'.gitignore',
|
|
141
|
-
'.oxlintrc.json',
|
|
142
|
-
'.oxfmtrc.jsonc',
|
|
143
|
-
'src/app.ts',
|
|
144
|
-
'.trails',
|
|
145
|
-
'src/cli.ts',
|
|
146
|
-
'src/trails/hello.ts',
|
|
147
|
-
'__tests__/examples.test.ts',
|
|
148
|
-
'lefthook.yml',
|
|
149
|
-
],
|
|
150
|
-
true
|
|
151
|
-
);
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const assertCliPackage = (dir: string): void => {
|
|
155
|
-
const pkg = readJson(dir, 'package.json');
|
|
156
|
-
expect(pkg['name']).toBe(basename(dir));
|
|
157
|
-
|
|
158
|
-
const deps = pkg['dependencies'] as Record<string, string>;
|
|
159
|
-
expect(deps['@ontrails/core']).toBe(ontrailsPackageRange);
|
|
160
|
-
expect(deps['@ontrails/cli']).toBe(ontrailsPackageRange);
|
|
161
|
-
expect(deps['commander']).toBe(scaffoldDependencyVersions.commander);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const assertVerifyPackage = (dir: string): void => {
|
|
165
|
-
const pkg = readJson(dir, 'package.json');
|
|
166
|
-
const devDeps = pkg['devDependencies'] as Record<string, string>;
|
|
167
|
-
expect(devDeps['@ontrails/testing']).toBe(ontrailsPackageRange);
|
|
168
|
-
expect(devDeps['@ontrails/warden']).toBe(ontrailsPackageRange);
|
|
169
|
-
expect(devDeps['lefthook']).toBe(scaffoldDependencyVersions.lefthook);
|
|
170
|
-
expect(readText(dir, 'lefthook.yml')).toContain('bunx trails warden');
|
|
171
|
-
expect(readText(dir, 'lefthook.yml')).not.toContain('--exit-code');
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const assertGeneratedToolingDeps = (dir: string): void => {
|
|
175
|
-
const pkg = readJson(dir, 'package.json');
|
|
176
|
-
const devDeps = pkg['devDependencies'] as Record<string, string>;
|
|
177
|
-
expect(devDeps['@types/bun']).toBe(scaffoldDependencyVersions.bunTypes);
|
|
178
|
-
expect(devDeps['oxlint']).toBe(scaffoldDependencyVersions.oxlint);
|
|
179
|
-
expect(devDeps['typescript']).toBe(scaffoldDependencyVersions.typescript);
|
|
180
|
-
expect(devDeps['ultracite']).toBe(scaffoldDependencyVersions.ultracite);
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const assertHelloApp = (dir: string): void => {
|
|
184
|
-
expectContainsAll(readText(dir, 'src/app.ts'), [
|
|
185
|
-
'topo',
|
|
186
|
-
`'${basename(dir)}'`,
|
|
187
|
-
'hello',
|
|
188
|
-
]);
|
|
189
|
-
expectContainsAll(readText(dir, 'src/trails/hello.ts'), [
|
|
190
|
-
"import { Result, trail } from '@ontrails/core'",
|
|
191
|
-
'return Result.ok({ message:',
|
|
192
|
-
]);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const assertEntityStarter = (dir: string): void => {
|
|
196
|
-
expectPaths(
|
|
197
|
-
dir,
|
|
198
|
-
[
|
|
199
|
-
'src/trails/entity.ts',
|
|
200
|
-
'src/trails/search.ts',
|
|
201
|
-
'src/trails/onboard.ts',
|
|
202
|
-
'src/signals/entity-signals.ts',
|
|
203
|
-
'src/store.ts',
|
|
204
|
-
],
|
|
205
|
-
true
|
|
206
|
-
);
|
|
207
|
-
expectPaths(dir, ['src/trails/hello.ts'], false);
|
|
208
|
-
expectContainsAll(readText(dir, 'src/app.ts'), [
|
|
209
|
-
"import * as entity from './trails/entity.js'",
|
|
210
|
-
"import * as search from './trails/search.js'",
|
|
211
|
-
"import * as onboard from './trails/onboard.js'",
|
|
212
|
-
"import * as entitySignals from './signals/entity-signals.js'",
|
|
213
|
-
]);
|
|
214
|
-
expectContainsAll(readText(dir, 'src/trails/entity.ts'), [
|
|
215
|
-
"import { Result, trail } from '@ontrails/core'",
|
|
216
|
-
'return Result.ok({ id: input.id, name:',
|
|
217
|
-
"return Result.ok({ id: '1', name: input.name })",
|
|
218
|
-
]);
|
|
219
|
-
expectContainsAll(readText(dir, 'src/trails/search.ts'), [
|
|
220
|
-
"import { Result, trail } from '@ontrails/core'",
|
|
221
|
-
'return Result.ok({ results: [] })',
|
|
222
|
-
]);
|
|
223
|
-
expectContainsAll(readText(dir, 'src/trails/onboard.ts'), [
|
|
224
|
-
"import { Result, trail } from '@ontrails/core'",
|
|
225
|
-
'return Result.ok({ onboarded: true })',
|
|
226
|
-
]);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const assertMcpSurface = (dir: string): void => {
|
|
230
|
-
expectPaths(dir, ['src/mcp.ts'], true);
|
|
231
|
-
expectPaths(dir, ['src/cli.ts'], false);
|
|
232
|
-
expectContainsAll(readText(dir, 'src/mcp.ts'), [
|
|
233
|
-
"import { surface } from '@ontrails/mcp'",
|
|
234
|
-
'await surface(app)',
|
|
235
|
-
]);
|
|
236
|
-
|
|
237
|
-
const deps = readJson(dir, 'package.json')['dependencies'] as Record<
|
|
238
|
-
string,
|
|
239
|
-
string
|
|
240
|
-
>;
|
|
241
|
-
expect(deps['@ontrails/mcp']).toBe(ontrailsPackageRange);
|
|
242
|
-
expect(deps['@ontrails/cli']).toBeUndefined();
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const assertHttpSurface = (dir: string): void => {
|
|
246
|
-
expectPaths(dir, ['src/http.ts'], true);
|
|
247
|
-
expectContainsAll(readText(dir, 'src/http.ts'), [
|
|
248
|
-
"import { surface } from '@ontrails/hono'",
|
|
249
|
-
'await surface(app, { port: 3000 })',
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
|
-
const deps = readJson(dir, 'package.json')['dependencies'] as Record<
|
|
253
|
-
string,
|
|
254
|
-
string
|
|
255
|
-
>;
|
|
256
|
-
expect(deps['@ontrails/hono']).toBe(ontrailsPackageRange);
|
|
257
|
-
expect(deps['@ontrails/http']).toBe(ontrailsPackageRange);
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const assertVerifySkipped = (dir: string): void => {
|
|
261
|
-
expectPaths(dir, ['__tests__/examples.test.ts', 'lefthook.yml'], false);
|
|
262
|
-
const devDeps = readJson(dir, 'package.json')['devDependencies'] as Record<
|
|
263
|
-
string,
|
|
264
|
-
string
|
|
265
|
-
>;
|
|
266
|
-
expect(devDeps['@ontrails/testing']).toBeUndefined();
|
|
267
|
-
expect(devDeps['@ontrails/warden']).toBeUndefined();
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const assertEmptyStarter = (dir: string): void => {
|
|
271
|
-
expectPaths(dir, ['src/trails/.gitkeep'], true);
|
|
272
|
-
expectPaths(dir, ['src/trails/hello.ts'], false);
|
|
273
|
-
const appContent = readText(dir, 'src/app.ts');
|
|
274
|
-
expect(appContent).toContain(`topo('${basename(dir)}')`);
|
|
275
|
-
expect(appContent).not.toContain('import * as');
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const withTempProject = async (
|
|
279
|
-
assertion: (dir: string) => Promise<void>
|
|
280
|
-
): Promise<void> => {
|
|
281
|
-
const dir = makeTempProject();
|
|
282
|
-
try {
|
|
283
|
-
await assertion(dir);
|
|
284
|
-
} finally {
|
|
285
|
-
rmSync(dir, { force: true, recursive: true });
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
describe('trails create', () => {
|
|
290
|
-
describe('create mode', () => {
|
|
291
|
-
test('generates project structure with defaults', async () => {
|
|
292
|
-
await withTempProject(async (dir) => {
|
|
293
|
-
expectOk(await runCreate(dir));
|
|
294
|
-
assertDefaultProjectFiles(dir);
|
|
295
|
-
assertCliPackage(dir);
|
|
296
|
-
assertVerifyPackage(dir);
|
|
297
|
-
assertGeneratedToolingDeps(dir);
|
|
298
|
-
assertHelloApp(dir);
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test('generates with entity starter', async () => {
|
|
303
|
-
await withTempProject(async (dir) => {
|
|
304
|
-
expectOk(await runCreate(dir, { starter: 'entity' }));
|
|
305
|
-
assertEntityStarter(dir);
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test('generates with MCP surface', async () => {
|
|
310
|
-
await withTempProject(async (dir) => {
|
|
311
|
-
expectOk(await runCreate(dir, { surfaces: ['mcp'] }));
|
|
312
|
-
assertMcpSurface(dir);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test('generates with HTTP surface', async () => {
|
|
317
|
-
await withTempProject(async (dir) => {
|
|
318
|
-
expectOk(await runCreate(dir, { surfaces: ['http'] }));
|
|
319
|
-
assertHttpSurface(dir);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test('skips verification when verify is false', async () => {
|
|
324
|
-
await withTempProject(async (dir) => {
|
|
325
|
-
expectOk(await runCreate(dir, { verify: false }));
|
|
326
|
-
assertVerifySkipped(dir);
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test('generates with empty starter', async () => {
|
|
331
|
-
await withTempProject(async (dir) => {
|
|
332
|
-
expectOk(await runCreate(dir, { starter: 'empty' }));
|
|
333
|
-
assertEmptyStarter(dir);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
describe('add-surface mode', () => {
|
|
339
|
-
test('adds MCP to existing project', async () => {
|
|
340
|
-
await withTempProject(async (dir) => {
|
|
341
|
-
setupMinimalProject(dir);
|
|
342
|
-
const result = expectOk(
|
|
343
|
-
await addSurface.blaze({ dir, surface: 'mcp' }, {} as never)
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
expect(result.created).toBe('src/mcp.ts');
|
|
347
|
-
expect(result.dependency).toBe('@ontrails/mcp');
|
|
348
|
-
expectPaths(dir, ['src/mcp.ts'], true);
|
|
349
|
-
expectContainsAll(readText(dir, 'src/mcp.ts'), [
|
|
350
|
-
"import { surface } from '@ontrails/mcp'",
|
|
351
|
-
]);
|
|
352
|
-
const deps = readJson(dir, 'package.json')['dependencies'] as Record<
|
|
353
|
-
string,
|
|
354
|
-
string
|
|
355
|
-
>;
|
|
356
|
-
expect(deps['@ontrails/mcp']).toBe(ontrailsPackageRange);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test('adds HTTP to existing project', async () => {
|
|
361
|
-
await withTempProject(async (dir) => {
|
|
362
|
-
setupMinimalProject(dir);
|
|
363
|
-
const result = expectOk(
|
|
364
|
-
await addSurface.blaze({ dir, surface: 'http' }, {} as never)
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
expect(result.created).toBe('src/http.ts');
|
|
368
|
-
expect(result.dependency).toBe('@ontrails/hono');
|
|
369
|
-
assertHttpSurface(dir);
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test('detects existing surface entrypoint', async () => {
|
|
374
|
-
await withTempProject(async (dir) => {
|
|
375
|
-
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
376
|
-
mkdirSync(join(dir, '.trails'), { recursive: true });
|
|
377
|
-
writeFileSync(join(dir, 'src', 'mcp.ts'), 'existing content');
|
|
378
|
-
|
|
379
|
-
const error = expectErr(
|
|
380
|
-
await addSurface.blaze({ dir, surface: 'mcp' }, {} as never)
|
|
381
|
-
);
|
|
382
|
-
expect(error.message).toBe(
|
|
383
|
-
'MCP surface already exists. Nothing to do.'
|
|
384
|
-
);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
describe('isInsideProject', () => {
|
|
391
|
-
test('detects .trails directory', async () => {
|
|
392
|
-
await withTempProject(async (dir) => {
|
|
393
|
-
mkdirSync(join(dir, '.trails'), { recursive: true });
|
|
394
|
-
expect(await isInsideProject(dir)).toBe(true);
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test('detects topo in src/', async () => {
|
|
399
|
-
await withTempProject(async (dir) => {
|
|
400
|
-
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
401
|
-
writeFileSync(
|
|
402
|
-
join(dir, 'src', 'app.ts'),
|
|
403
|
-
"import { topo } from '@ontrails/core';\nexport const app = topo('app');\n"
|
|
404
|
-
);
|
|
405
|
-
expect(await isInsideProject(dir)).toBe(true);
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
test('returns false for empty directory', async () => {
|
|
410
|
-
await withTempProject(async (dir) => {
|
|
411
|
-
mkdirSync(dir, { recursive: true });
|
|
412
|
-
expect(await isInsideProject(dir)).toBe(false);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from 'node:fs';
|
|
9
|
-
import { join, resolve } from 'node:path';
|
|
10
|
-
|
|
11
|
-
import type { Result } from '@ontrails/core';
|
|
12
|
-
import { ValidationError } from '@ontrails/core';
|
|
13
|
-
|
|
14
|
-
import { draftPromoteTrail } from '../trails/draft-promote.js';
|
|
15
|
-
|
|
16
|
-
const repoTempDir = (): string =>
|
|
17
|
-
join(
|
|
18
|
-
resolve(import.meta.dir, '../..'),
|
|
19
|
-
'.tmp-tests',
|
|
20
|
-
`draft-promote-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const expectOk = <T>(result: Result<T, Error>): T => {
|
|
24
|
-
if (result.isErr()) {
|
|
25
|
-
throw result.error;
|
|
26
|
-
}
|
|
27
|
-
return result.value;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const expectErr = <E extends Error>(result: Result<unknown, E>): E => {
|
|
31
|
-
if (result.isOk()) {
|
|
32
|
-
throw new Error('expected result to be an error');
|
|
33
|
-
}
|
|
34
|
-
return result.error;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const writeDraftPromoteFixture = (dir: string): void => {
|
|
38
|
-
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
39
|
-
|
|
40
|
-
writeFileSync(
|
|
41
|
-
join(dir, 'src', 'app.ts'),
|
|
42
|
-
`import { topo } from '@ontrails/core';
|
|
43
|
-
import { draftPrepare } from './_draft.prepare.js';
|
|
44
|
-
import { exportTrail } from './export.js';
|
|
45
|
-
|
|
46
|
-
export const app = topo('draft-test', { draftPrepare, exportTrail });
|
|
47
|
-
`
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
writeFileSync(
|
|
51
|
-
join(dir, 'src', '_draft.prepare.ts'),
|
|
52
|
-
`import { Result, trail } from '@ontrails/core';
|
|
53
|
-
import { z } from 'zod';
|
|
54
|
-
|
|
55
|
-
export const draftPrepare = trail('_draft.entity.prepare', {
|
|
56
|
-
blaze: async () => Result.ok({ ready: true }),
|
|
57
|
-
input: z.object({}),
|
|
58
|
-
output: z.object({ ready: z.boolean() }),
|
|
59
|
-
});
|
|
60
|
-
`
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
writeFileSync(
|
|
64
|
-
join(dir, 'src', 'export.ts'),
|
|
65
|
-
`import { Result, trail } from '@ontrails/core';
|
|
66
|
-
import { z } from 'zod';
|
|
67
|
-
|
|
68
|
-
export const exportTrail = trail('entity.export', {
|
|
69
|
-
blaze: async () => Result.ok({ exported: true }),
|
|
70
|
-
crosses: ['_draft.entity.prepare'],
|
|
71
|
-
input: z.object({}),
|
|
72
|
-
output: z.object({ exported: z.boolean() }),
|
|
73
|
-
});
|
|
74
|
-
`
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const expectDraftPromoteResults = (dir: string): void => {
|
|
79
|
-
expect(existsSync(join(dir, 'src', '_draft.prepare.ts'))).toBe(false);
|
|
80
|
-
expect(existsSync(join(dir, 'src', 'prepare.ts'))).toBe(true);
|
|
81
|
-
expect(readFileSync(join(dir, 'src', 'prepare.ts'), 'utf8')).toContain(
|
|
82
|
-
"trail('entity.prepare'"
|
|
83
|
-
);
|
|
84
|
-
expect(readFileSync(join(dir, 'src', 'export.ts'), 'utf8')).toContain(
|
|
85
|
-
"crosses: ['entity.prepare']"
|
|
86
|
-
);
|
|
87
|
-
expect(readFileSync(join(dir, 'src', 'app.ts'), 'utf8')).toContain(
|
|
88
|
-
"from './prepare.js'"
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
describe('draft.promote', () => {
|
|
93
|
-
test('promotes draft ids, renames files, and updates imports', async () => {
|
|
94
|
-
const dir = repoTempDir();
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
writeDraftPromoteFixture(dir);
|
|
98
|
-
|
|
99
|
-
const result = expectOk(
|
|
100
|
-
await draftPromoteTrail.blaze(
|
|
101
|
-
{
|
|
102
|
-
fromId: '_draft.entity.prepare',
|
|
103
|
-
renameFiles: true,
|
|
104
|
-
rootDir: dir,
|
|
105
|
-
toId: 'entity.prepare',
|
|
106
|
-
},
|
|
107
|
-
{ cwd: dir } as never
|
|
108
|
-
)
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
expect(result.promotedEstablished).toBe(true);
|
|
112
|
-
expect(result.remainingDraftIds).toEqual([]);
|
|
113
|
-
expect(result.updatedFiles).toEqual(
|
|
114
|
-
expect.arrayContaining(['src/app.ts', 'src/export.ts'])
|
|
115
|
-
);
|
|
116
|
-
expect(result.renamedFiles).toEqual([
|
|
117
|
-
{
|
|
118
|
-
from: 'src/_draft.prepare.ts',
|
|
119
|
-
to: 'src/prepare.ts',
|
|
120
|
-
},
|
|
121
|
-
]);
|
|
122
|
-
expectDraftPromoteResults(dir);
|
|
123
|
-
} finally {
|
|
124
|
-
rmSync(dir, { force: true, recursive: true });
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('returns ValidationError when rootDir does not exist', async () => {
|
|
129
|
-
const error = expectErr(
|
|
130
|
-
await draftPromoteTrail.blaze(
|
|
131
|
-
{
|
|
132
|
-
fromId: '_draft.entity.prepare',
|
|
133
|
-
renameFiles: true,
|
|
134
|
-
rootDir: join(repoTempDir(), 'missing'),
|
|
135
|
-
toId: 'entity.prepare',
|
|
136
|
-
},
|
|
137
|
-
{ cwd: process.cwd() } as never
|
|
138
|
-
)
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
expect(error).toBeInstanceOf(ValidationError);
|
|
142
|
-
expect(error.message).toContain('rootDir does not exist');
|
|
143
|
-
});
|
|
144
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import type { AnyTrail } from '@ontrails/core';
|
|
4
|
-
import { ConflictError, trail, topo, Result } from '@ontrails/core';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Test fixtures
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
const helloTrail = trail('hello', {
|
|
12
|
-
blaze: (input) => {
|
|
13
|
-
const name = input.name ?? 'world';
|
|
14
|
-
return Result.ok({ message: `Hello, ${name}!` });
|
|
15
|
-
},
|
|
16
|
-
description: 'Say hello',
|
|
17
|
-
detours: [
|
|
18
|
-
{
|
|
19
|
-
on: ConflictError,
|
|
20
|
-
/* oxlint-disable-next-line require-await -- test stub */
|
|
21
|
-
recover: async () => Result.ok({ message: 'recovered' }),
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
examples: [
|
|
25
|
-
{
|
|
26
|
-
expected: { message: 'Hello, world!' },
|
|
27
|
-
input: {},
|
|
28
|
-
name: 'Default greeting',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
expected: { message: 'Hello, Trails!' },
|
|
32
|
-
input: { name: 'Trails' },
|
|
33
|
-
name: 'Named greeting',
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
input: z.object({ name: z.string().optional() }),
|
|
37
|
-
intent: 'read',
|
|
38
|
-
output: z.object({ message: z.string() }),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const app = topo('test-app', { hello: helloTrail });
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Tests
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
describe('trails guide', () => {
|
|
48
|
-
test('lists trails with descriptions', () => {
|
|
49
|
-
const items = app.list();
|
|
50
|
-
expect(items.length).toBe(1);
|
|
51
|
-
|
|
52
|
-
const [hello] = items;
|
|
53
|
-
expect(hello).toBeDefined();
|
|
54
|
-
expect(hello?.id).toBe('hello');
|
|
55
|
-
|
|
56
|
-
expect((hello as AnyTrail).description).toBe('Say hello');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('trail detail includes examples', () => {
|
|
60
|
-
const item = app.get('hello') as AnyTrail;
|
|
61
|
-
expect(item).toBeDefined();
|
|
62
|
-
expect(item.examples).toBeDefined();
|
|
63
|
-
expect(item.examples?.length).toBe(2);
|
|
64
|
-
expect(item.examples?.[0]?.name).toBe('Default greeting');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('JSON output for trail is valid', () => {
|
|
68
|
-
const t = app.get('hello') as AnyTrail;
|
|
69
|
-
expect(t).toBeDefined();
|
|
70
|
-
|
|
71
|
-
const json = JSON.stringify({
|
|
72
|
-
description: t.description,
|
|
73
|
-
detours: t.detours,
|
|
74
|
-
examples: t.examples,
|
|
75
|
-
id: t.id,
|
|
76
|
-
kind: t.kind,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const parsed = JSON.parse(json) as Record<string, unknown>;
|
|
80
|
-
expect(parsed['id']).toBe('hello');
|
|
81
|
-
expect(parsed['kind']).toBe('trail');
|
|
82
|
-
expect(parsed['description']).toBe('Say hello');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('non-existent trail returns undefined from topo', () => {
|
|
86
|
-
const item = app.get('does-not-exist');
|
|
87
|
-
expect(item).toBeUndefined();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('detours are accessible on trail', () => {
|
|
91
|
-
const t = app.get('hello') as AnyTrail;
|
|
92
|
-
expect(t).toBeDefined();
|
|
93
|
-
expect(t.detours).toHaveLength(1);
|
|
94
|
-
expect(t.detours[0]?.on).toBe(ConflictError);
|
|
95
|
-
});
|
|
96
|
-
});
|