@ontrails/config 1.0.0-beta.14 → 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 +41 -0
- package/README.md +20 -28
- package/package.json +10 -2
- package/src/app-config.ts +23 -12
- package/src/compose.ts +16 -22
- package/src/{config-provision.ts → config-resource.ts} +5 -5
- package/src/define-config.ts +18 -18
- package/src/{generate → derive}/env.ts +3 -3
- package/src/{generate → derive}/example.ts +1 -1
- package/src/{generate → derive}/helpers.ts +1 -1
- package/src/derive/index.ts +3 -0
- package/src/{generate → derive}/json-schema.ts +1 -1
- package/src/{describe.ts → derive-fields.ts} +1 -1
- package/src/{explain.ts → derive-provenance.ts} +6 -6
- package/src/extensions.ts +1 -1
- package/src/index.ts +12 -19
- package/src/registry.ts +2 -2
- package/src/resolve.ts +17 -15
- package/src/trails/config-check.ts +3 -3
- package/src/trails/config-describe.ts +5 -5
- package/src/trails/config-explain.ts +13 -13
- package/src/trails/config-init.ts +10 -10
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/app-config.d.ts +0 -65
- package/dist/app-config.d.ts.map +0 -1
- package/dist/app-config.js +0 -172
- package/dist/app-config.js.map +0 -1
- package/dist/collect.d.ts +0 -11
- package/dist/collect.d.ts.map +0 -1
- package/dist/collect.js +0 -81
- package/dist/collect.js.map +0 -1
- package/dist/compose.d.ts +0 -30
- package/dist/compose.d.ts.map +0 -1
- package/dist/compose.js +0 -21
- package/dist/compose.js.map +0 -1
- package/dist/config-gate.d.ts +0 -11
- package/dist/config-gate.d.ts.map +0 -1
- package/dist/config-gate.js +0 -6
- package/dist/config-gate.js.map +0 -1
- package/dist/config-layer.d.ts +0 -11
- package/dist/config-layer.d.ts.map +0 -1
- package/dist/config-layer.js +0 -6
- package/dist/config-layer.js.map +0 -1
- package/dist/config-provision.d.ts +0 -3
- package/dist/config-provision.d.ts.map +0 -1
- package/dist/config-provision.js +0 -26
- package/dist/config-provision.js.map +0 -1
- package/dist/config-service.d.ts +0 -3
- package/dist/config-service.d.ts.map +0 -1
- package/dist/config-service.js +0 -26
- package/dist/config-service.js.map +0 -1
- package/dist/define-config.d.ts +0 -61
- package/dist/define-config.d.ts.map +0 -1
- package/dist/define-config.js +0 -90
- package/dist/define-config.js.map +0 -1
- package/dist/describe.d.ts +0 -25
- package/dist/describe.d.ts.map +0 -1
- package/dist/describe.js +0 -147
- package/dist/describe.js.map +0 -1
- package/dist/doctor.d.ts +0 -27
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js +0 -131
- package/dist/doctor.js.map +0 -1
- package/dist/explain.d.ts +0 -30
- package/dist/explain.d.ts.map +0 -1
- package/dist/explain.js +0 -101
- package/dist/explain.js.map +0 -1
- package/dist/extensions.d.ts +0 -38
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js +0 -35
- package/dist/extensions.js.map +0 -1
- package/dist/generate/env.d.ts +0 -15
- package/dist/generate/env.d.ts.map +0 -1
- package/dist/generate/env.js +0 -65
- package/dist/generate/env.js.map +0 -1
- package/dist/generate/example.d.ts +0 -16
- package/dist/generate/example.d.ts.map +0 -1
- package/dist/generate/example.js +0 -138
- package/dist/generate/example.js.map +0 -1
- package/dist/generate/helpers.d.ts +0 -35
- package/dist/generate/helpers.d.ts.map +0 -1
- package/dist/generate/helpers.js +0 -116
- package/dist/generate/helpers.js.map +0 -1
- package/dist/generate/index.d.ts +0 -4
- package/dist/generate/index.d.ts.map +0 -1
- package/dist/generate/index.js +0 -4
- package/dist/generate/index.js.map +0 -1
- package/dist/generate/json-schema.d.ts +0 -18
- package/dist/generate/json-schema.d.ts.map +0 -1
- package/dist/generate/json-schema.js +0 -97
- package/dist/generate/json-schema.js.map +0 -1
- package/dist/index.d.ts +0 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -21
- package/dist/index.js.map +0 -1
- package/dist/merge.d.ts +0 -16
- package/dist/merge.d.ts.map +0 -1
- package/dist/merge.js +0 -34
- package/dist/merge.js.map +0 -1
- package/dist/ref.d.ts +0 -29
- package/dist/ref.d.ts.map +0 -1
- package/dist/ref.js +0 -30
- package/dist/ref.js.map +0 -1
- package/dist/registry.d.ts +0 -24
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -12
- package/dist/registry.js.map +0 -1
- package/dist/resolve.d.ts +0 -21
- package/dist/resolve.d.ts.map +0 -1
- package/dist/resolve.js +0 -177
- package/dist/resolve.js.map +0 -1
- package/dist/secret-heuristics.d.ts +0 -10
- package/dist/secret-heuristics.d.ts.map +0 -1
- package/dist/secret-heuristics.js +0 -11
- package/dist/secret-heuristics.js.map +0 -1
- package/dist/trails/config-check.d.ts +0 -11
- package/dist/trails/config-check.d.ts.map +0 -1
- package/dist/trails/config-check.js +0 -53
- package/dist/trails/config-check.js.map +0 -1
- package/dist/trails/config-describe.d.ts +0 -12
- package/dist/trails/config-describe.d.ts.map +0 -1
- package/dist/trails/config-describe.js +0 -41
- package/dist/trails/config-describe.js.map +0 -1
- package/dist/trails/config-explain.d.ts +0 -8
- package/dist/trails/config-explain.d.ts.map +0 -1
- package/dist/trails/config-explain.js +0 -74
- package/dist/trails/config-explain.js.map +0 -1
- package/dist/trails/config-init.d.ts +0 -9
- package/dist/trails/config-init.d.ts.map +0 -1
- package/dist/trails/config-init.js +0 -78
- package/dist/trails/config-init.js.map +0 -1
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -49
- package/dist/workspace.js.map +0 -1
- package/dist/zod-utils.d.ts +0 -14
- package/dist/zod-utils.d.ts.map +0 -1
- package/dist/zod-utils.js +0 -41
- package/dist/zod-utils.js.map +0 -1
- package/src/__tests__/app-config.test.ts +0 -329
- package/src/__tests__/compose.test.ts +0 -59
- package/src/__tests__/config-check.test.ts +0 -171
- package/src/__tests__/config-describe.test.ts +0 -154
- package/src/__tests__/config-explain.test.ts +0 -167
- package/src/__tests__/config-gate.test.ts +0 -53
- package/src/__tests__/config-init.test.ts +0 -210
- package/src/__tests__/config-provision.test.ts +0 -87
- package/src/__tests__/define-config.test.ts +0 -263
- package/src/__tests__/describe.test.ts +0 -158
- package/src/__tests__/doctor.test.ts +0 -172
- package/src/__tests__/explain.test.ts +0 -139
- package/src/__tests__/extensions.test.ts +0 -134
- package/src/__tests__/generate.test.ts +0 -269
- package/src/__tests__/ref.test.ts +0 -35
- package/src/__tests__/resolve.test.ts +0 -246
- package/src/__tests__/workspace.test.ts +0 -64
- package/src/config-gate.ts +0 -15
- package/src/generate/index.ts +0 -3
- package/src/workspace.ts +0 -56
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { createProvisionLookup } from '@ontrails/core';
|
|
3
|
-
import type { TrailContext } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { configDescribe } from '../trails/config-describe.js';
|
|
7
|
-
import { env, secret } from '../extensions.js';
|
|
8
|
-
import type { ConfigState } from '../registry.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Build a TrailContext with configProvision resolved in extensions.
|
|
12
|
-
*/
|
|
13
|
-
const buildCtx = (state: ConfigState): TrailContext => {
|
|
14
|
-
const extensions = { config: state };
|
|
15
|
-
const ctx: TrailContext = {
|
|
16
|
-
abortSignal: AbortSignal.timeout(5000),
|
|
17
|
-
cwd: '/tmp',
|
|
18
|
-
env: {},
|
|
19
|
-
extensions,
|
|
20
|
-
provision: undefined as unknown as TrailContext['provision'],
|
|
21
|
-
requestId: 'test',
|
|
22
|
-
workspaceRoot: '/tmp',
|
|
23
|
-
};
|
|
24
|
-
const withLookup = {
|
|
25
|
-
...ctx,
|
|
26
|
-
provision: createProvisionLookup(() => withLookup),
|
|
27
|
-
};
|
|
28
|
-
return withLookup;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe('config.describe trail', () => {
|
|
32
|
-
describe('identity', () => {
|
|
33
|
-
test('has id "config.describe"', () => {
|
|
34
|
-
expect(configDescribe.id).toBe('config.describe');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('has kind "trail"', () => {
|
|
38
|
-
expect(configDescribe.kind).toBe('trail');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('has intent "read"', () => {
|
|
42
|
-
expect(configDescribe.intent).toBe('read');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('has infrastructure meta', () => {
|
|
46
|
-
expect(configDescribe.meta).toEqual({ category: 'infrastructure' });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('has output schema', () => {
|
|
50
|
-
expect(configDescribe.output).toBeDefined();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('declares configProvision dependency', () => {
|
|
54
|
-
expect(configDescribe.provisions).toBeDefined();
|
|
55
|
-
expect(configDescribe.provisions?.length).toBe(1);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('examples', () => {
|
|
60
|
-
test('has at least one example', () => {
|
|
61
|
-
expect(configDescribe.examples?.length).toBeGreaterThanOrEqual(1);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('wired behavior', () => {
|
|
66
|
-
test('returns one field description per schema field', async () => {
|
|
67
|
-
const schema = z.object({
|
|
68
|
-
host: z.string().default('localhost'),
|
|
69
|
-
port: z.number().default(3000),
|
|
70
|
-
});
|
|
71
|
-
const state: ConfigState = {
|
|
72
|
-
resolved: { host: 'localhost', port: 3000 },
|
|
73
|
-
schema,
|
|
74
|
-
};
|
|
75
|
-
const ctx = buildCtx(state);
|
|
76
|
-
const result = await configDescribe.blaze({}, ctx);
|
|
77
|
-
|
|
78
|
-
expect(result.isOk()).toBe(true);
|
|
79
|
-
expect(result.unwrap().fields.length).toBe(2);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('includes path and type for each field', async () => {
|
|
83
|
-
const schema = z.object({
|
|
84
|
-
host: z.string().default('localhost'),
|
|
85
|
-
port: z.number().default(3000),
|
|
86
|
-
});
|
|
87
|
-
const state: ConfigState = {
|
|
88
|
-
resolved: { host: 'localhost', port: 3000 },
|
|
89
|
-
schema,
|
|
90
|
-
};
|
|
91
|
-
const ctx = buildCtx(state);
|
|
92
|
-
const result = await configDescribe.blaze({}, ctx);
|
|
93
|
-
const { fields } = result.unwrap();
|
|
94
|
-
|
|
95
|
-
expect(fields[0]?.path).toBe('host');
|
|
96
|
-
expect(fields[0]?.type).toBe('string');
|
|
97
|
-
expect(fields[1]?.path).toBe('port');
|
|
98
|
-
expect(fields[1]?.type).toBe('number');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('includes env annotation when present', async () => {
|
|
102
|
-
const schema = z.object({
|
|
103
|
-
port: env(z.number(), 'PORT').default(3000),
|
|
104
|
-
});
|
|
105
|
-
const state: ConfigState = {
|
|
106
|
-
resolved: { port: 3000 },
|
|
107
|
-
schema,
|
|
108
|
-
};
|
|
109
|
-
const ctx = buildCtx(state);
|
|
110
|
-
const result = await configDescribe.blaze({}, ctx);
|
|
111
|
-
|
|
112
|
-
expect(result.isOk()).toBe(true);
|
|
113
|
-
const [field] = result.unwrap().fields;
|
|
114
|
-
expect(field?.env).toBe('PORT');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('includes secret annotation when present', async () => {
|
|
118
|
-
const schema = z.object({
|
|
119
|
-
token: secret(z.string()).default('tok'),
|
|
120
|
-
});
|
|
121
|
-
const state: ConfigState = {
|
|
122
|
-
resolved: { token: 'tok' },
|
|
123
|
-
schema,
|
|
124
|
-
};
|
|
125
|
-
const ctx = buildCtx(state);
|
|
126
|
-
const result = await configDescribe.blaze({}, ctx);
|
|
127
|
-
|
|
128
|
-
expect(result.isOk()).toBe(true);
|
|
129
|
-
const [field] = result.unwrap().fields;
|
|
130
|
-
expect(field?.secret).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('reports required status correctly', async () => {
|
|
134
|
-
const schema = z.object({
|
|
135
|
-
optional: z.string().optional(),
|
|
136
|
-
required: z.string(),
|
|
137
|
-
withDefault: z.string().default('val'),
|
|
138
|
-
});
|
|
139
|
-
const state: ConfigState = {
|
|
140
|
-
resolved: { required: 'x', withDefault: 'val' },
|
|
141
|
-
schema,
|
|
142
|
-
};
|
|
143
|
-
const ctx = buildCtx(state);
|
|
144
|
-
const result = await configDescribe.blaze({}, ctx);
|
|
145
|
-
|
|
146
|
-
expect(result.isOk()).toBe(true);
|
|
147
|
-
const { fields } = result.unwrap();
|
|
148
|
-
const findField = (path: string) => fields.find((f) => f.path === path);
|
|
149
|
-
expect(findField('required')?.required).toBe(true);
|
|
150
|
-
expect(findField('optional')?.required).toBe(false);
|
|
151
|
-
expect(findField('withDefault')?.required).toBe(false);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { createProvisionLookup } from '@ontrails/core';
|
|
3
|
-
import type { TrailContext } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { configExplain } from '../trails/config-explain.js';
|
|
7
|
-
import { env } from '../extensions.js';
|
|
8
|
-
import type { ConfigState } from '../registry.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Build a TrailContext with configProvision resolved in extensions.
|
|
12
|
-
*/
|
|
13
|
-
const buildCtx = (state: ConfigState): TrailContext => {
|
|
14
|
-
const extensions = { config: state };
|
|
15
|
-
const ctx: TrailContext = {
|
|
16
|
-
abortSignal: AbortSignal.timeout(5000),
|
|
17
|
-
cwd: '/tmp',
|
|
18
|
-
env: {},
|
|
19
|
-
extensions,
|
|
20
|
-
provision: undefined as unknown as TrailContext['provision'],
|
|
21
|
-
requestId: 'test',
|
|
22
|
-
workspaceRoot: '/tmp',
|
|
23
|
-
};
|
|
24
|
-
const withLookup = {
|
|
25
|
-
...ctx,
|
|
26
|
-
provision: createProvisionLookup(() => withLookup),
|
|
27
|
-
};
|
|
28
|
-
return withLookup;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe('config.explain trail', () => {
|
|
32
|
-
describe('identity', () => {
|
|
33
|
-
test('has id "config.explain"', () => {
|
|
34
|
-
expect(configExplain.id).toBe('config.explain');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('has kind "trail"', () => {
|
|
38
|
-
expect(configExplain.kind).toBe('trail');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('has intent "read"', () => {
|
|
42
|
-
expect(configExplain.intent).toBe('read');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('has infrastructure meta', () => {
|
|
46
|
-
expect(configExplain.meta).toEqual({ category: 'infrastructure' });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('has output schema', () => {
|
|
50
|
-
expect(configExplain.output).toBeDefined();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('declares configProvision dependency', () => {
|
|
54
|
-
expect(configExplain.provisions).toBeDefined();
|
|
55
|
-
expect(configExplain.provisions?.length).toBe(1);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('examples', () => {
|
|
60
|
-
test('has at least one example', () => {
|
|
61
|
-
expect(configExplain.examples?.length).toBeGreaterThanOrEqual(1);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('wired behavior', () => {
|
|
66
|
-
test('returns provenance entries for all fields', async () => {
|
|
67
|
-
const schema = z.object({
|
|
68
|
-
host: z.string().default('localhost'),
|
|
69
|
-
port: z.number().default(3000),
|
|
70
|
-
});
|
|
71
|
-
const state: ConfigState = {
|
|
72
|
-
resolved: { host: 'localhost', port: 3000 },
|
|
73
|
-
schema,
|
|
74
|
-
};
|
|
75
|
-
const ctx = buildCtx(state);
|
|
76
|
-
const result = await configExplain.blaze({ path: '' }, ctx);
|
|
77
|
-
|
|
78
|
-
expect(result.isOk()).toBe(true);
|
|
79
|
-
const value = result.unwrap();
|
|
80
|
-
expect(value.entries.length).toBe(2);
|
|
81
|
-
expect(value.entries[0]?.path).toBe('host');
|
|
82
|
-
expect(value.entries[0]?.source).toBe('default');
|
|
83
|
-
expect(value.entries[1]?.path).toBe('port');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('filters entries by path prefix', async () => {
|
|
87
|
-
const schema = z.object({
|
|
88
|
-
db: z.object({
|
|
89
|
-
host: z.string().default('localhost'),
|
|
90
|
-
port: z.number().default(5432),
|
|
91
|
-
}),
|
|
92
|
-
name: z.string().default('app'),
|
|
93
|
-
});
|
|
94
|
-
const state: ConfigState = {
|
|
95
|
-
resolved: { db: { host: 'localhost', port: 5432 }, name: 'app' },
|
|
96
|
-
schema,
|
|
97
|
-
};
|
|
98
|
-
const ctx = buildCtx(state);
|
|
99
|
-
const result = await configExplain.blaze({ path: 'db' }, ctx);
|
|
100
|
-
|
|
101
|
-
expect(result.isOk()).toBe(true);
|
|
102
|
-
const value = result.unwrap();
|
|
103
|
-
expect(value.entries.length).toBe(2);
|
|
104
|
-
expect(value.entries[0]?.path).toBe('db.host');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('does not match sibling roots that only share a prefix', async () => {
|
|
108
|
-
const schema = z.object({
|
|
109
|
-
db: z.object({
|
|
110
|
-
host: z.string().default('localhost'),
|
|
111
|
-
}),
|
|
112
|
-
dbReplica: z.object({
|
|
113
|
-
host: z.string().default('replica.local'),
|
|
114
|
-
}),
|
|
115
|
-
});
|
|
116
|
-
const state: ConfigState = {
|
|
117
|
-
resolved: {
|
|
118
|
-
db: { host: 'localhost' },
|
|
119
|
-
dbReplica: { host: 'replica.local' },
|
|
120
|
-
},
|
|
121
|
-
schema,
|
|
122
|
-
};
|
|
123
|
-
const ctx = buildCtx(state);
|
|
124
|
-
const result = await configExplain.blaze({ path: 'db' }, ctx);
|
|
125
|
-
|
|
126
|
-
expect(result.isOk()).toBe(true);
|
|
127
|
-
expect(result.unwrap().entries).toEqual([
|
|
128
|
-
expect.objectContaining({ path: 'db.host' }),
|
|
129
|
-
]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('shows base layer as source when base provides value', async () => {
|
|
133
|
-
const schema = z.object({
|
|
134
|
-
port: z.number().default(3000),
|
|
135
|
-
});
|
|
136
|
-
const state: ConfigState = {
|
|
137
|
-
base: { port: 8080 },
|
|
138
|
-
resolved: { port: 8080 },
|
|
139
|
-
schema,
|
|
140
|
-
};
|
|
141
|
-
const ctx = buildCtx(state);
|
|
142
|
-
const result = await configExplain.blaze({ path: '' }, ctx);
|
|
143
|
-
|
|
144
|
-
expect(result.isOk()).toBe(true);
|
|
145
|
-
const [entry] = result.unwrap().entries;
|
|
146
|
-
expect(entry?.source).toBe('base');
|
|
147
|
-
expect(entry?.value).toBe(8080);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('shows env as source when env provides value', async () => {
|
|
151
|
-
const schema = z.object({
|
|
152
|
-
token: env(z.string(), 'TOKEN').default('fallback'),
|
|
153
|
-
});
|
|
154
|
-
const state: ConfigState = {
|
|
155
|
-
env: { TOKEN: 'real-secret' },
|
|
156
|
-
resolved: { token: 'real-secret' },
|
|
157
|
-
schema,
|
|
158
|
-
};
|
|
159
|
-
const ctx = buildCtx(state);
|
|
160
|
-
const result = await configExplain.blaze({ path: '' }, ctx);
|
|
161
|
-
|
|
162
|
-
expect(result.isOk()).toBe(true);
|
|
163
|
-
const [entry] = result.unwrap().entries;
|
|
164
|
-
expect(entry?.source).toBe('env');
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { Result } from '@ontrails/core';
|
|
3
|
-
import type { TrailContext } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { configGate } from '../config-gate.js';
|
|
7
|
-
|
|
8
|
-
const stubTrail = {
|
|
9
|
-
blaze: (_input: unknown, _ctx: TrailContext) => Result.ok({}),
|
|
10
|
-
crosses: [],
|
|
11
|
-
description: undefined,
|
|
12
|
-
detours: undefined,
|
|
13
|
-
examples: undefined,
|
|
14
|
-
fields: undefined,
|
|
15
|
-
id: 'test.stub',
|
|
16
|
-
idempotent: undefined,
|
|
17
|
-
input: z.object({}),
|
|
18
|
-
intent: 'read' as const,
|
|
19
|
-
kind: 'trail' as const,
|
|
20
|
-
meta: undefined,
|
|
21
|
-
output: undefined,
|
|
22
|
-
provisions: [],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
describe('configGate', () => {
|
|
26
|
-
describe('identity', () => {
|
|
27
|
-
test('has name "config"', () => {
|
|
28
|
-
expect(configGate.name).toBe('config');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('has a description', () => {
|
|
32
|
-
expect(configGate.description).toBeDefined();
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('wrap', () => {
|
|
37
|
-
test('passes through to the base implementation', async () => {
|
|
38
|
-
const impl = (_input: unknown, _ctx: TrailContext) =>
|
|
39
|
-
Result.ok({ called: true });
|
|
40
|
-
|
|
41
|
-
const wrapped = configGate.wrap(stubTrail, impl);
|
|
42
|
-
const ctx = {
|
|
43
|
-
cwd: '/tmp',
|
|
44
|
-
env: {},
|
|
45
|
-
workspaceRoot: '/tmp',
|
|
46
|
-
} as TrailContext;
|
|
47
|
-
const result = await wrapped({}, ctx);
|
|
48
|
-
|
|
49
|
-
expect(result.isOk()).toBe(true);
|
|
50
|
-
expect(result.unwrap()).toEqual({ called: true });
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { createProvisionLookup } from '@ontrails/core';
|
|
6
|
-
import type { TrailContext } from '@ontrails/core';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
|
|
9
|
-
import { env } from '../extensions.js';
|
|
10
|
-
import type { ConfigState } from '../registry.js';
|
|
11
|
-
import { configInit } from '../trails/config-init.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Build a TrailContext with configProvision resolved in extensions.
|
|
15
|
-
*/
|
|
16
|
-
const buildCtx = (state: ConfigState): TrailContext => {
|
|
17
|
-
const extensions = { config: state };
|
|
18
|
-
const ctx: TrailContext = {
|
|
19
|
-
abortSignal: AbortSignal.timeout(5000),
|
|
20
|
-
cwd: '/tmp',
|
|
21
|
-
env: {},
|
|
22
|
-
extensions,
|
|
23
|
-
provision: undefined as unknown as TrailContext['provision'],
|
|
24
|
-
requestId: 'test',
|
|
25
|
-
workspaceRoot: '/tmp',
|
|
26
|
-
};
|
|
27
|
-
const withLookup = {
|
|
28
|
-
...ctx,
|
|
29
|
-
provision: createProvisionLookup(() => withLookup),
|
|
30
|
-
};
|
|
31
|
-
return withLookup;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const testSchema = z.object({
|
|
35
|
-
host: z.string().default('localhost'),
|
|
36
|
-
port: z.number().default(3000),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const testState: ConfigState = {
|
|
40
|
-
resolved: { host: 'localhost', port: 3000 },
|
|
41
|
-
schema: testSchema,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
describe('config.init trail', () => {
|
|
45
|
-
describe('identity', () => {
|
|
46
|
-
test('has id "config.init"', () => {
|
|
47
|
-
expect(configInit.id).toBe('config.init');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('has kind "trail"', () => {
|
|
51
|
-
expect(configInit.kind).toBe('trail');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('has intent "write"', () => {
|
|
55
|
-
expect(configInit.intent).toBe('write');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('has infrastructure meta', () => {
|
|
59
|
-
expect(configInit.meta).toEqual({ category: 'infrastructure' });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('has output schema', () => {
|
|
63
|
-
expect(configInit.output).toBeDefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('declares configProvision dependency', () => {
|
|
67
|
-
expect(configInit.provisions).toBeDefined();
|
|
68
|
-
expect(configInit.provisions?.length).toBe(1);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('examples', () => {
|
|
73
|
-
test('has at least one example', () => {
|
|
74
|
-
expect(configInit.examples?.length).toBeGreaterThanOrEqual(1);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('wired behavior', () => {
|
|
79
|
-
test('generates TOML output by default', async () => {
|
|
80
|
-
const ctx = buildCtx(testState);
|
|
81
|
-
const result = await configInit.blaze({ format: 'toml' }, ctx);
|
|
82
|
-
|
|
83
|
-
expect(result.isOk()).toBe(true);
|
|
84
|
-
const value = result.unwrap();
|
|
85
|
-
expect(value.format).toBe('toml');
|
|
86
|
-
expect(value.content).toContain('host');
|
|
87
|
-
expect(value.content).toContain('port');
|
|
88
|
-
expect(value.content.length).toBeGreaterThan(0);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('generates JSON output when requested', async () => {
|
|
92
|
-
const ctx = buildCtx(testState);
|
|
93
|
-
const result = await configInit.blaze({ format: 'json' }, ctx);
|
|
94
|
-
|
|
95
|
-
expect(result.isOk()).toBe(true);
|
|
96
|
-
const value = result.unwrap();
|
|
97
|
-
expect(value.format).toBe('json');
|
|
98
|
-
expect(value.content).toContain('"host"');
|
|
99
|
-
expect(value.content).toContain('"port"');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('generates YAML output when requested', async () => {
|
|
103
|
-
const ctx = buildCtx(testState);
|
|
104
|
-
const result = await configInit.blaze({ format: 'yaml' }, ctx);
|
|
105
|
-
|
|
106
|
-
expect(result.isOk()).toBe(true);
|
|
107
|
-
const value = result.unwrap();
|
|
108
|
-
expect(value.format).toBe('yaml');
|
|
109
|
-
expect(value.content).toContain('host:');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('generates JSONC output when requested', async () => {
|
|
113
|
-
const ctx = buildCtx(testState);
|
|
114
|
-
const result = await configInit.blaze({ format: 'jsonc' }, ctx);
|
|
115
|
-
|
|
116
|
-
expect(result.isOk()).toBe(true);
|
|
117
|
-
const value = result.unwrap();
|
|
118
|
-
expect(value.format).toBe('jsonc');
|
|
119
|
-
expect(value.content).toContain('"host"');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('output content is non-empty for schema with fields', async () => {
|
|
123
|
-
const ctx = buildCtx(testState);
|
|
124
|
-
const result = await configInit.blaze({ format: 'toml' }, ctx);
|
|
125
|
-
|
|
126
|
-
expect(result.isOk()).toBe(true);
|
|
127
|
-
expect(result.unwrap().content.trim().length).toBeGreaterThan(0);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('returns content without writtenFiles when dir is not provided', async () => {
|
|
131
|
-
const ctx = buildCtx(testState);
|
|
132
|
-
const result = await configInit.blaze({ format: 'toml' }, ctx);
|
|
133
|
-
|
|
134
|
-
expect(result.isOk()).toBe(true);
|
|
135
|
-
expect(result.unwrap().writtenFiles).toBeUndefined();
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('artifact generation', () => {
|
|
140
|
-
let tempDir: string;
|
|
141
|
-
|
|
142
|
-
const envSchema = z.object({
|
|
143
|
-
host: env(z.string(), 'APP_HOST').default('localhost'),
|
|
144
|
-
port: z.number().default(3000),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const envState: ConfigState = {
|
|
148
|
-
resolved: { host: 'localhost', port: 3000 },
|
|
149
|
-
schema: envSchema,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
beforeEach(async () => {
|
|
153
|
-
tempDir = await mkdtemp(join(tmpdir(), 'trails-config-init-'));
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
afterEach(async () => {
|
|
157
|
-
await rm(tempDir, { force: true, recursive: true });
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test('writes .schema.json when dir is provided', async () => {
|
|
161
|
-
const ctx = buildCtx(envState);
|
|
162
|
-
const result = await configInit.blaze(
|
|
163
|
-
{ dir: tempDir, format: 'toml' },
|
|
164
|
-
ctx
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
expect(result.isOk()).toBe(true);
|
|
168
|
-
const value = result.unwrap();
|
|
169
|
-
expect(value.writtenFiles).toBeDefined();
|
|
170
|
-
expect(value.writtenFiles).toContainEqual(join(tempDir, '.schema.json'));
|
|
171
|
-
|
|
172
|
-
const schemaContent = await readFile(
|
|
173
|
-
join(tempDir, '.schema.json'),
|
|
174
|
-
'utf8'
|
|
175
|
-
);
|
|
176
|
-
const parsed = JSON.parse(schemaContent);
|
|
177
|
-
expect(parsed['$schema']).toBe(
|
|
178
|
-
'https://json-schema.org/draft/2020-12/schema'
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('writes .env.example when schema has env bindings', async () => {
|
|
183
|
-
const ctx = buildCtx(envState);
|
|
184
|
-
const result = await configInit.blaze(
|
|
185
|
-
{ dir: tempDir, format: 'toml' },
|
|
186
|
-
ctx
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
expect(result.isOk()).toBe(true);
|
|
190
|
-
const value = result.unwrap();
|
|
191
|
-
expect(value.writtenFiles).toContainEqual(join(tempDir, '.env.example'));
|
|
192
|
-
|
|
193
|
-
const envContent = await readFile(join(tempDir, '.env.example'), 'utf8');
|
|
194
|
-
expect(envContent).toContain('APP_HOST');
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test('still returns content alongside written files', async () => {
|
|
198
|
-
const ctx = buildCtx(envState);
|
|
199
|
-
const result = await configInit.blaze(
|
|
200
|
-
{ dir: tempDir, format: 'json' },
|
|
201
|
-
ctx
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
expect(result.isOk()).toBe(true);
|
|
205
|
-
const value = result.unwrap();
|
|
206
|
-
expect(value.content.length).toBeGreaterThan(0);
|
|
207
|
-
expect(value.format).toBe('json');
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
import { configProvision } from '../config-provision.js';
|
|
5
|
-
import type { ConfigState } from '../registry.js';
|
|
6
|
-
import { clearConfigState, registerConfigState } from '../registry.js';
|
|
7
|
-
|
|
8
|
-
/** Stub ProvisionContext for create calls. */
|
|
9
|
-
const stubSvcCtx = {
|
|
10
|
-
config: undefined,
|
|
11
|
-
cwd: '/tmp',
|
|
12
|
-
env: {},
|
|
13
|
-
workspaceRoot: '/tmp',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
describe('configProvision', () => {
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
clearConfigState();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('identity', () => {
|
|
22
|
-
test('has id "config"', () => {
|
|
23
|
-
expect(configProvision.id).toBe('config');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('has kind "provision"', () => {
|
|
27
|
-
expect(configProvision.kind).toBe('provision');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('has infrastructure meta', () => {
|
|
31
|
-
expect(configProvision.meta).toEqual({ category: 'infrastructure' });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('has description', () => {
|
|
35
|
-
expect(configProvision.description).toBeDefined();
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('mock', () => {
|
|
40
|
-
test('returns a ConfigState with empty schema and resolved', () => {
|
|
41
|
-
const value = configProvision.mock?.() as ConfigState;
|
|
42
|
-
expect(value).toBeDefined();
|
|
43
|
-
expect(value.resolved).toEqual({});
|
|
44
|
-
expect(value.schema).toBeDefined();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('create', () => {
|
|
49
|
-
test('returns Result.ok with registered ConfigState', async () => {
|
|
50
|
-
const schema = z.object({ port: z.number().default(3000) });
|
|
51
|
-
const state: ConfigState = { resolved: { port: 3000 }, schema };
|
|
52
|
-
registerConfigState(state);
|
|
53
|
-
|
|
54
|
-
const result = await configProvision.create(stubSvcCtx);
|
|
55
|
-
|
|
56
|
-
expect(result.isOk()).toBe(true);
|
|
57
|
-
const value = result.unwrap() as ConfigState;
|
|
58
|
-
expect(value.resolved).toEqual({ port: 3000 });
|
|
59
|
-
expect(value.schema).toBe(schema);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('includes optional layer data when present', async () => {
|
|
63
|
-
const schema = z.object({ port: z.number() });
|
|
64
|
-
const state: ConfigState = {
|
|
65
|
-
base: { port: 8080 },
|
|
66
|
-
local: { port: 3000 },
|
|
67
|
-
resolved: { port: 3000 },
|
|
68
|
-
schema,
|
|
69
|
-
};
|
|
70
|
-
registerConfigState(state);
|
|
71
|
-
|
|
72
|
-
const result = await configProvision.create(stubSvcCtx);
|
|
73
|
-
|
|
74
|
-
expect(result.isOk()).toBe(true);
|
|
75
|
-
const value = result.unwrap() as ConfigState;
|
|
76
|
-
expect(value.base).toEqual({ port: 8080 });
|
|
77
|
-
expect(value.local).toEqual({ port: 3000 });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('returns Result.err when no state is registered', async () => {
|
|
81
|
-
const result = await configProvision.create(stubSvcCtx);
|
|
82
|
-
|
|
83
|
-
expect(result.isErr()).toBe(true);
|
|
84
|
-
expect(result.error.message).toContain('Config state not registered');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|