@ontrails/core 1.0.0-beta.3 → 1.0.0-beta.32
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 +323 -8
- package/README.md +114 -33
- package/package.json +11 -1
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +250 -19
- package/src/detours.ts +8 -0
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +350 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +335 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/glob.ts +81 -0
- package/src/index.ts +526 -35
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-scope.ts +66 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +18 -0
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +92 -55
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +101 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -67
- package/src/tracing.ts +345 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +1168 -30
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +206 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +414 -56
- package/src/validation.ts +152 -4
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +278 -0
- package/src/workspace.ts +162 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -125
- package/dist/derive.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -36
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -40
- package/dist/index.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -153
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -80
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -156
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -134
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -66
- package/src/__tests__/derive.test.ts +0 -203
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/topo.test.ts +0 -220
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -280
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { Result } from '../result';
|
|
6
|
-
import { hike } from '../hike';
|
|
7
|
-
import type { TrailContext } from '../types';
|
|
8
|
-
|
|
9
|
-
const stubCtx: TrailContext = {
|
|
10
|
-
requestId: 'test-123',
|
|
11
|
-
signal: AbortSignal.timeout(5000),
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
describe('hike()', () => {
|
|
15
|
-
const inputSchema = z.object({ userId: z.string() });
|
|
16
|
-
const outputSchema = z.object({ profile: z.string() });
|
|
17
|
-
|
|
18
|
-
const fetchProfile = hike('fetch-profile', {
|
|
19
|
-
description: 'Fetch a user profile',
|
|
20
|
-
follows: ['authenticate', 'validate-session'],
|
|
21
|
-
implementation: (input) =>
|
|
22
|
-
Result.ok({ profile: `Profile for ${input.userId}` }),
|
|
23
|
-
input: inputSchema,
|
|
24
|
-
output: outputSchema,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('basics', () => {
|
|
28
|
-
test("returns kind 'hike'", () => {
|
|
29
|
-
expect(fetchProfile.kind).toBe('hike');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('returns correct id', () => {
|
|
33
|
-
expect(fetchProfile.id).toBe('fetch-profile');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('preserves follows array', () => {
|
|
37
|
-
expect(fetchProfile.follows).toEqual([
|
|
38
|
-
'authenticate',
|
|
39
|
-
'validate-session',
|
|
40
|
-
]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('follows array is frozen', () => {
|
|
44
|
-
expect(Object.isFrozen(fetchProfile.follows)).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('trail compatibility', () => {
|
|
49
|
-
test('extends Trail — has input schema', () => {
|
|
50
|
-
const parsed = fetchProfile.input.safeParse({ userId: 'u-1' });
|
|
51
|
-
expect(parsed.success).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('extends Trail — has output schema', () => {
|
|
55
|
-
expect(fetchProfile.output).toBeDefined();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('extends Trail — implementation is callable', async () => {
|
|
59
|
-
const result = await fetchProfile.implementation(
|
|
60
|
-
{ userId: 'u-1' },
|
|
61
|
-
stubCtx
|
|
62
|
-
);
|
|
63
|
-
expect(result.isOk()).toBe(true);
|
|
64
|
-
expect(result.unwrap()).toEqual({ profile: 'Profile for u-1' });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('preserves description', () => {
|
|
68
|
-
expect(fetchProfile.description).toBe('Fetch a user profile');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('result object is frozen', () => {
|
|
72
|
-
expect(Object.isFrozen(fetchProfile)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('markers are preserved', () => {
|
|
77
|
-
const withMarkers = hike('tagged-hike', {
|
|
78
|
-
follows: ['setup'],
|
|
79
|
-
implementation: () => Result.ok(),
|
|
80
|
-
input: z.object({}),
|
|
81
|
-
markers: { domain: 'auth' },
|
|
82
|
-
});
|
|
83
|
-
expect(withMarkers.markers).toEqual({ domain: 'auth' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('single-object overload', () => {
|
|
87
|
-
test('accepts spec with id property', () => {
|
|
88
|
-
const r = hike({
|
|
89
|
-
follows: ['entity.add', 'entity.relate'],
|
|
90
|
-
id: 'entity.onboard',
|
|
91
|
-
implementation: () => Result.ok(),
|
|
92
|
-
input: z.object({}),
|
|
93
|
-
});
|
|
94
|
-
expect(r.id).toBe('entity.onboard');
|
|
95
|
-
expect(r.kind).toBe('hike');
|
|
96
|
-
expect(r.follows).toEqual(['entity.add', 'entity.relate']);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('sync implementations are normalized to an awaitable runtime function', async () => {
|
|
100
|
-
const r = hike({
|
|
101
|
-
follows: ['entity.add'],
|
|
102
|
-
id: 'entity.check',
|
|
103
|
-
implementation: (input: { userId: string }) =>
|
|
104
|
-
Result.ok({ profile: input.userId }),
|
|
105
|
-
input: inputSchema,
|
|
106
|
-
output: outputSchema,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const promise = r.implementation({ userId: 'u-2' }, stubCtx);
|
|
110
|
-
expect(promise).toBeInstanceOf(Promise);
|
|
111
|
-
|
|
112
|
-
const result = await promise;
|
|
113
|
-
expect(result.isOk()).toBe(true);
|
|
114
|
-
expect(result.unwrap()).toEqual({ profile: 'u-2' });
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { jobOutputSchema } from '../job';
|
|
6
|
-
|
|
7
|
-
describe('jobOutputSchema', () => {
|
|
8
|
-
test('parses a valid completed job output', () => {
|
|
9
|
-
const input = {
|
|
10
|
-
completedAt: '2026-03-25T00:01:00Z',
|
|
11
|
-
current: 10,
|
|
12
|
-
jobId: 'job-123',
|
|
13
|
-
percentage: 100,
|
|
14
|
-
result: { rows: 42 },
|
|
15
|
-
startedAt: '2026-03-25T00:00:00Z',
|
|
16
|
-
status: 'completed',
|
|
17
|
-
total: 10,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const parsed = jobOutputSchema.parse(input);
|
|
21
|
-
|
|
22
|
-
expect(parsed.jobId).toBe('job-123');
|
|
23
|
-
expect(parsed.status).toBe('completed');
|
|
24
|
-
expect(parsed.current).toBe(10);
|
|
25
|
-
expect(parsed.total).toBe(10);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('parses a minimal pending job output', () => {
|
|
29
|
-
const input = {
|
|
30
|
-
current: 0,
|
|
31
|
-
jobId: 'job-456',
|
|
32
|
-
status: 'pending',
|
|
33
|
-
total: 100,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const parsed = jobOutputSchema.parse(input);
|
|
37
|
-
|
|
38
|
-
expect(parsed.jobId).toBe('job-456');
|
|
39
|
-
expect(parsed.status).toBe('pending');
|
|
40
|
-
expect(parsed.result).toBeUndefined();
|
|
41
|
-
expect(parsed.error).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('parses a failed job with error', () => {
|
|
45
|
-
const input = {
|
|
46
|
-
current: 3,
|
|
47
|
-
error: 'connection reset',
|
|
48
|
-
jobId: 'job-789',
|
|
49
|
-
status: 'failed',
|
|
50
|
-
total: 10,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const parsed = jobOutputSchema.parse(input);
|
|
54
|
-
|
|
55
|
-
expect(parsed.status).toBe('failed');
|
|
56
|
-
expect(parsed.error).toBe('connection reset');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('rejects when jobId is missing', () => {
|
|
60
|
-
const input = {
|
|
61
|
-
current: 1,
|
|
62
|
-
status: 'running',
|
|
63
|
-
total: 5,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
expect(() => jobOutputSchema.parse(input)).toThrow();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('rejects an invalid status value', () => {
|
|
70
|
-
const input = {
|
|
71
|
-
current: 0,
|
|
72
|
-
jobId: 'job-bad',
|
|
73
|
-
status: 'unknown',
|
|
74
|
-
total: 0,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
expect(() => jobOutputSchema.parse(input)).toThrow();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('composes statusFields and progressFields correctly', () => {
|
|
81
|
-
const { shape } = jobOutputSchema;
|
|
82
|
-
|
|
83
|
-
// Status field from statusFields()
|
|
84
|
-
expect(shape.status).toBeInstanceOf(z.ZodEnum);
|
|
85
|
-
|
|
86
|
-
// Progress fields from progressFields()
|
|
87
|
-
expect(shape.current).toBeInstanceOf(z.ZodNumber);
|
|
88
|
-
expect(shape.total).toBeInstanceOf(z.ZodNumber);
|
|
89
|
-
expect(shape.percentage).toBeDefined();
|
|
90
|
-
|
|
91
|
-
// Job-specific fields
|
|
92
|
-
expect(shape.jobId).toBeInstanceOf(z.ZodString);
|
|
93
|
-
expect(shape.error).toBeDefined();
|
|
94
|
-
expect(shape.result).toBeDefined();
|
|
95
|
-
expect(shape.startedAt).toBeDefined();
|
|
96
|
-
expect(shape.completedAt).toBeDefined();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/* oxlint-disable require-await -- adapter mocks and layer wrappers satisfy async interfaces without awaiting */
|
|
2
|
-
import { describe, test, expect } from 'bun:test';
|
|
3
|
-
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
IndexAdapter,
|
|
8
|
-
StorageAdapter,
|
|
9
|
-
CacheAdapter,
|
|
10
|
-
SearchOptions,
|
|
11
|
-
SearchResult,
|
|
12
|
-
StorageOptions,
|
|
13
|
-
} from '../adapters';
|
|
14
|
-
import type { HealthStatus, HealthResult, HealthCheck } from '../health';
|
|
15
|
-
import { composeLayers } from '../layer';
|
|
16
|
-
import type { Layer } from '../layer';
|
|
17
|
-
import { Result } from '../result';
|
|
18
|
-
import { trail } from '../trail';
|
|
19
|
-
import type { TrailContext } from '../types';
|
|
20
|
-
|
|
21
|
-
const stubCtx: TrailContext = {
|
|
22
|
-
requestId: 'test-layer',
|
|
23
|
-
signal: AbortSignal.timeout(5000),
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const echoTrail = trail('echo', {
|
|
27
|
-
implementation: (input) => Result.ok({ value: input.value }),
|
|
28
|
-
input: z.object({ value: z.string() }),
|
|
29
|
-
markers: { domain: 'test' },
|
|
30
|
-
output: z.object({ value: z.string() }),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Layer tests
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
describe('Layer', () => {
|
|
38
|
-
test('single layer wraps implementation', async () => {
|
|
39
|
-
const prefixLayer: Layer = {
|
|
40
|
-
name: 'prefix',
|
|
41
|
-
wrap(_trail, impl) {
|
|
42
|
-
return async (input, ctx) => {
|
|
43
|
-
const result = await impl(input, ctx);
|
|
44
|
-
return result.map((out) => ({
|
|
45
|
-
...out,
|
|
46
|
-
value: `prefixed:${(out as { value: string }).value}`,
|
|
47
|
-
}));
|
|
48
|
-
};
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const wrapped = composeLayers(
|
|
53
|
-
[prefixLayer],
|
|
54
|
-
echoTrail,
|
|
55
|
-
echoTrail.implementation
|
|
56
|
-
);
|
|
57
|
-
const result = await wrapped({ value: 'hello' }, stubCtx);
|
|
58
|
-
|
|
59
|
-
expect(result.isOk()).toBe(true);
|
|
60
|
-
expect(result.unwrap()).toEqual({ value: 'prefixed:hello' });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('multiple layers compose in outermost-first order', async () => {
|
|
64
|
-
const log: string[] = [];
|
|
65
|
-
|
|
66
|
-
const outer: Layer = {
|
|
67
|
-
name: 'outer',
|
|
68
|
-
wrap(_trail, impl) {
|
|
69
|
-
return async (input, ctx) => {
|
|
70
|
-
log.push('outer:before');
|
|
71
|
-
const r = await impl(input, ctx);
|
|
72
|
-
log.push('outer:after');
|
|
73
|
-
return r;
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const inner: Layer = {
|
|
79
|
-
name: 'inner',
|
|
80
|
-
wrap(_trail, impl) {
|
|
81
|
-
return async (input, ctx) => {
|
|
82
|
-
log.push('inner:before');
|
|
83
|
-
const r = await impl(input, ctx);
|
|
84
|
-
log.push('inner:after');
|
|
85
|
-
return r;
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const wrapped = composeLayers(
|
|
91
|
-
[outer, inner],
|
|
92
|
-
echoTrail,
|
|
93
|
-
echoTrail.implementation
|
|
94
|
-
);
|
|
95
|
-
await wrapped({ value: 'x' }, stubCtx);
|
|
96
|
-
|
|
97
|
-
expect(log).toEqual([
|
|
98
|
-
'outer:before',
|
|
99
|
-
'inner:before',
|
|
100
|
-
'inner:after',
|
|
101
|
-
'outer:after',
|
|
102
|
-
]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('layer can short-circuit without calling inner implementation', async () => {
|
|
106
|
-
const shortCircuit: Layer = {
|
|
107
|
-
name: 'short-circuit',
|
|
108
|
-
wrap() {
|
|
109
|
-
return async () => Result.err(new Error('blocked'));
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const wrapped = composeLayers(
|
|
114
|
-
[shortCircuit],
|
|
115
|
-
echoTrail,
|
|
116
|
-
echoTrail.implementation
|
|
117
|
-
);
|
|
118
|
-
const result = await wrapped({ value: 'hello' }, stubCtx);
|
|
119
|
-
|
|
120
|
-
expect(result.isErr()).toBe(true);
|
|
121
|
-
const err = result as unknown as { error: Error };
|
|
122
|
-
expect(err.error.message).toBe('blocked');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('layer can inspect trail markers', () => {
|
|
126
|
-
let capturedDomain: unknown;
|
|
127
|
-
|
|
128
|
-
const inspectLayer: Layer = {
|
|
129
|
-
name: 'inspect',
|
|
130
|
-
wrap(t, impl) {
|
|
131
|
-
capturedDomain = t.markers?.['domain'];
|
|
132
|
-
return impl;
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
composeLayers([inspectLayer], echoTrail, echoTrail.implementation);
|
|
137
|
-
|
|
138
|
-
expect(capturedDomain).toBe('test');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test('empty layers array returns implementation unchanged', () => {
|
|
142
|
-
const wrapped = composeLayers([], echoTrail, echoTrail.implementation);
|
|
143
|
-
expect(wrapped).toBe(echoTrail.implementation);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Health types (compile-time verification)
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
describe('health types', () => {
|
|
152
|
-
test('HealthResult satisfies the interface', () => {
|
|
153
|
-
const check: HealthCheck = {
|
|
154
|
-
latency: 12,
|
|
155
|
-
message: 'ok',
|
|
156
|
-
status: 'healthy',
|
|
157
|
-
};
|
|
158
|
-
const result: HealthResult = {
|
|
159
|
-
checks: { db: check },
|
|
160
|
-
status: 'healthy',
|
|
161
|
-
uptime: 3600,
|
|
162
|
-
version: '1.0.0',
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
expect(result.status).toBe('healthy');
|
|
166
|
-
expect(result.checks['db']?.status).toBe('healthy');
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('HealthStatus union is exhaustive', () => {
|
|
170
|
-
const statuses: HealthStatus[] = ['healthy', 'degraded', 'unhealthy'];
|
|
171
|
-
expect(statuses).toHaveLength(3);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// Adapter types (compile-time verification)
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
describe('adapter types', () => {
|
|
180
|
-
test('IndexAdapter mock satisfies the interface', () => {
|
|
181
|
-
const mock: IndexAdapter = {
|
|
182
|
-
index: async () => Result.ok(),
|
|
183
|
-
remove: async () => Result.ok(),
|
|
184
|
-
search: async () => Result.ok([]),
|
|
185
|
-
};
|
|
186
|
-
expect(mock.index).toBeDefined();
|
|
187
|
-
expect(mock.search).toBeDefined();
|
|
188
|
-
expect(mock.remove).toBeDefined();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('StorageAdapter mock satisfies the interface', () => {
|
|
192
|
-
const mock: StorageAdapter = {
|
|
193
|
-
delete: async () => Result.ok(),
|
|
194
|
-
get: async () => Result.ok('value'),
|
|
195
|
-
has: async () => Result.ok(true),
|
|
196
|
-
set: async () => Result.ok(),
|
|
197
|
-
};
|
|
198
|
-
expect(mock.has).toBeDefined();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('CacheAdapter mock satisfies the interface', () => {
|
|
202
|
-
const mock: CacheAdapter = {
|
|
203
|
-
clear: async () => Result.ok(),
|
|
204
|
-
delete: async () => Result.ok(),
|
|
205
|
-
get: async () => Result.ok(),
|
|
206
|
-
set: async () => Result.ok(),
|
|
207
|
-
};
|
|
208
|
-
expect(mock.clear).toBeDefined();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('SearchOptions and SearchResult satisfy their shapes', () => {
|
|
212
|
-
const opts: SearchOptions = { filters: { tag: 'a' }, limit: 10, offset: 0 };
|
|
213
|
-
const hit: SearchResult = {
|
|
214
|
-
document: { title: 'x' },
|
|
215
|
-
id: '1',
|
|
216
|
-
score: 0.95,
|
|
217
|
-
};
|
|
218
|
-
const sopts: StorageOptions = { ttl: 5000 };
|
|
219
|
-
|
|
220
|
-
expect(opts.limit).toBe(10);
|
|
221
|
-
expect(hit.score).toBe(0.95);
|
|
222
|
-
expect(sopts.ttl).toBe(5000);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { PermissionError } from '../errors.js';
|
|
5
|
-
import { securePath, isPathSafe, resolveSafePath } from '../path-security.js';
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// securePath
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
describe('securePath', () => {
|
|
12
|
-
const base = '/project/workspace';
|
|
13
|
-
|
|
14
|
-
test('resolves a simple relative path', () => {
|
|
15
|
-
const result = securePath(base, 'src/index.ts');
|
|
16
|
-
expect(result.isOk()).toBe(true);
|
|
17
|
-
expect(result.unwrap()).toBe(resolve(base, 'src/index.ts'));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('resolves nested paths', () => {
|
|
21
|
-
const result = securePath(base, 'src/../lib/utils.ts');
|
|
22
|
-
expect(result.isOk()).toBe(true);
|
|
23
|
-
expect(result.unwrap()).toBe(resolve(base, 'lib/utils.ts'));
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('accepts the base directory itself', () => {
|
|
27
|
-
const result = securePath(base, '.');
|
|
28
|
-
expect(result.isOk()).toBe(true);
|
|
29
|
-
expect(result.unwrap()).toBe(resolve(base));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('rejects path traversal with ..', () => {
|
|
33
|
-
const result = securePath(base, '../../etc/passwd');
|
|
34
|
-
expect(result.isErr()).toBe(true);
|
|
35
|
-
const err = result as unknown as { error: PermissionError };
|
|
36
|
-
expect(err.error).toBeInstanceOf(PermissionError);
|
|
37
|
-
expect(err.error.message).toContain('Path traversal detected');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('rejects absolute paths outside base', () => {
|
|
41
|
-
const result = securePath(base, '/etc/passwd');
|
|
42
|
-
expect(result.isErr()).toBe(true);
|
|
43
|
-
const err = result as unknown as { error: PermissionError };
|
|
44
|
-
expect(err.error).toBeInstanceOf(PermissionError);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('rejects sneaky traversal (subdir/../../..)', () => {
|
|
48
|
-
const result = securePath(base, 'src/../../..');
|
|
49
|
-
expect(result.isErr()).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('allows paths that mention .. but stay inside', () => {
|
|
53
|
-
const result = securePath(base, 'src/../src/file.ts');
|
|
54
|
-
expect(result.isOk()).toBe(true);
|
|
55
|
-
expect(result.unwrap()).toBe(resolve(base, 'src/file.ts'));
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// isPathSafe
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
describe('isPathSafe', () => {
|
|
64
|
-
const base = '/project/workspace';
|
|
65
|
-
|
|
66
|
-
test('returns true for child paths', () => {
|
|
67
|
-
expect(isPathSafe(base, 'src/index.ts')).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('returns true for the base directory', () => {
|
|
71
|
-
expect(isPathSafe(base, '.')).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('returns false for traversal', () => {
|
|
75
|
-
expect(isPathSafe(base, '../secret')).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('returns false for absolute paths outside base', () => {
|
|
79
|
-
expect(isPathSafe(base, '/tmp/evil')).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// resolveSafePath
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
describe('resolveSafePath', () => {
|
|
88
|
-
const base = '/project/workspace';
|
|
89
|
-
|
|
90
|
-
test('joins multiple segments safely', () => {
|
|
91
|
-
const result = resolveSafePath(base, 'src', 'lib', 'utils.ts');
|
|
92
|
-
expect(result.isOk()).toBe(true);
|
|
93
|
-
expect(result.unwrap()).toBe(join(resolve(base), 'src', 'lib', 'utils.ts'));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('rejects when segments escape the base', () => {
|
|
97
|
-
const result = resolveSafePath(base, 'src', '../../../etc');
|
|
98
|
-
expect(result.isErr()).toBe(true);
|
|
99
|
-
const err = result as unknown as { error: PermissionError };
|
|
100
|
-
expect(err.error).toBeInstanceOf(PermissionError);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('handles single segment', () => {
|
|
104
|
-
const result = resolveSafePath(base, 'file.txt');
|
|
105
|
-
expect(result.isOk()).toBe(true);
|
|
106
|
-
expect(result.unwrap()).toBe(resolve(base, 'file.txt'));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('handles empty segments', () => {
|
|
110
|
-
const result = resolveSafePath(base);
|
|
111
|
-
expect(result.isOk()).toBe(true);
|
|
112
|
-
expect(result.unwrap()).toBe(resolve(base));
|
|
113
|
-
});
|
|
114
|
-
});
|