@owlmeans/client-planning 0.1.18-rc.8 → 0.1.18
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/README.md +94 -11
- package/agent-meta/manifest.json +62 -4
- package/agent-meta/skills/client-planning/SKILL.md +60 -78
- package/agent-meta/skills/client-planning/parts/aux-mirrors.md +61 -0
- package/agent-meta/skills/client-planning/parts/mirror.md +67 -0
- package/agent-meta/skills/client-planning/parts/validation.md +60 -0
- package/build/client.d.ts +1 -1
- package/build/client.d.ts.map +1 -1
- package/build/client.js +66 -14
- package/build/client.js.map +1 -1
- package/build/commits.d.ts +2 -13
- package/build/commits.d.ts.map +1 -1
- package/build/commits.js +190 -132
- package/build/commits.js.map +1 -1
- package/build/consts.d.ts +4 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +10 -4
- package/build/consts.js.map +1 -1
- package/build/definitions.d.ts +13 -0
- package/build/definitions.d.ts.map +1 -0
- package/build/definitions.js +70 -0
- package/build/definitions.js.map +1 -0
- package/build/events/types.d.ts +34 -0
- package/build/events/types.d.ts.map +1 -0
- package/build/events/types.js +2 -0
- package/build/events/types.js.map +1 -0
- package/build/events.d.ts +10 -21
- package/build/events.d.ts.map +1 -1
- package/build/events.js +104 -102
- package/build/events.js.map +1 -1
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +86 -49
- package/build/facade.js.map +1 -1
- package/build/feed.d.ts +4 -4
- package/build/feed.d.ts.map +1 -1
- package/build/feed.js +50 -27
- package/build/feed.js.map +1 -1
- package/build/helper/types.d.ts +15 -0
- package/build/helper/types.d.ts.map +1 -0
- package/build/helper/types.js +2 -0
- package/build/helper/types.js.map +1 -0
- package/build/helper.d.ts +7 -7
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +16 -8
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -0
- package/build/index.js.map +1 -1
- package/build/lifecycle/types.d.ts +27 -0
- package/build/lifecycle/types.d.ts.map +1 -0
- package/build/lifecycle/types.js +2 -0
- package/build/lifecycle/types.js.map +1 -0
- package/build/lifecycle.d.ts +3 -0
- package/build/lifecycle.d.ts.map +1 -0
- package/build/lifecycle.js +157 -0
- package/build/lifecycle.js.map +1 -0
- package/build/resource-feed.d.ts +5 -0
- package/build/resource-feed.d.ts.map +1 -0
- package/build/resource-feed.js +97 -0
- package/build/resource-feed.js.map +1 -0
- package/build/resources.d.ts +6 -0
- package/build/resources.d.ts.map +1 -0
- package/build/resources.js +93 -0
- package/build/resources.js.map +1 -0
- package/build/stores.d.ts +4 -11
- package/build/stores.d.ts.map +1 -1
- package/build/stores.js +11 -35
- package/build/stores.js.map +1 -1
- package/build/sync/types.d.ts +18 -0
- package/build/sync/types.d.ts.map +1 -0
- package/build/sync/types.js +2 -0
- package/build/sync/types.js.map +1 -0
- package/build/sync.d.ts +4 -0
- package/build/sync.d.ts.map +1 -0
- package/build/sync.js +26 -0
- package/build/sync.js.map +1 -0
- package/build/types.d.ts +48 -3
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +13 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/record/types.d.ts +28 -0
- package/build/utils/record/types.d.ts.map +1 -0
- package/build/utils/record/types.js +2 -0
- package/build/utils/record/types.js.map +1 -0
- package/build/utils/record.d.ts +3 -24
- package/build/utils/record.d.ts.map +1 -1
- package/build/utils/record.js +64 -70
- package/build/utils/record.js.map +1 -1
- package/build/validation/consts.d.ts +23 -0
- package/build/validation/consts.d.ts.map +1 -0
- package/build/validation/consts.js +289 -0
- package/build/validation/consts.js.map +1 -0
- package/build/validation/types.d.ts +17 -0
- package/build/validation/types.d.ts.map +1 -0
- package/build/validation/types.js +2 -0
- package/build/validation/types.js.map +1 -0
- package/build/validation.d.ts +10 -0
- package/build/validation.d.ts.map +1 -0
- package/build/validation.js +171 -0
- package/build/validation.js.map +1 -0
- package/package.json +20 -18
- package/src/client.ts +58 -15
- package/src/commits.ts +152 -184
- package/src/consts.ts +12 -4
- package/src/definitions.ts +93 -0
- package/src/events/types.ts +35 -0
- package/src/events.ts +118 -108
- package/src/facade.ts +87 -55
- package/src/feed.ts +47 -28
- package/src/helper/types.ts +15 -0
- package/src/helper.ts +27 -10
- package/src/index.ts +12 -0
- package/src/lifecycle/types.ts +28 -0
- package/src/lifecycle.ts +123 -0
- package/src/resource-feed.ts +79 -0
- package/src/resources.ts +84 -0
- package/src/stores.ts +11 -40
- package/src/sync/types.ts +18 -0
- package/src/sync.ts +40 -0
- package/src/types.local.ts +14 -0
- package/src/types.ts +53 -5
- package/src/utils/record/types.ts +27 -0
- package/src/utils/record.ts +68 -71
- package/src/validation/consts.ts +289 -0
- package/src/validation/types.ts +18 -0
- package/src/validation.ts +140 -0
- package/tests/app.spec.ts +72 -0
- package/tests/context.ts +46 -15
- package/tests/csp.spec.ts +119 -0
- package/tests/definitions.spec.ts +74 -0
- package/tests/draft7.spec.ts +13 -0
- package/tests/events.spec.ts +13 -9
- package/tests/facade.spec.ts +2 -1
- package/tests/lifecycle/context.ts +29 -0
- package/tests/lifecycle-close.spec.ts +103 -0
- package/tests/lifecycle-commits.spec.ts +92 -0
- package/tests/lifecycle-definitions.spec.ts +28 -0
- package/tests/lifecycle-feeds.spec.ts +92 -0
- package/tests/lifecycle-sockets.spec.ts +76 -0
- package/tests/replies.spec.ts +81 -0
- package/tests/resources.spec.ts +116 -0
- package/tests/stores.spec.ts +13 -11
- package/tests/validation.spec.ts +127 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnySchema } from 'ajv'
|
|
3
|
+
import { makeAjv, SchemaInvalid } from '@owlmeans/planning'
|
|
4
|
+
import { makePlanningFieldValidator } from '../src/index.js'
|
|
5
|
+
|
|
6
|
+
describe('@owlmeans/client-planning — CSP field validation', () => {
|
|
7
|
+
test('the exact nested fields validate without modifying, coercing, flattening or defaulting them', () => {
|
|
8
|
+
const schema = { type: 'object', properties: {
|
|
9
|
+
estimatedValue: { type: 'object', properties: { amount: { type: 'number', minimum: 0 }, currency: { type: 'string', enum: ['USD', 'EUR'] } },
|
|
10
|
+
required: ['amount', 'currency'], additionalProperties: false },
|
|
11
|
+
nextContactDate: { type: 'string', format: 'date' }, priority: { type: 'string', enum: ['low', 'normal', 'high'] },
|
|
12
|
+
}, required: ['estimatedValue', 'nextContactDate'], additionalProperties: false }
|
|
13
|
+
const copy = structuredClone(schema)
|
|
14
|
+
const validator = makePlanningFieldValidator(schema)
|
|
15
|
+
const valid = { estimatedValue: { amount: 12345, currency: 'USD' }, nextContactDate: '2027-03-15', priority: 'normal' }
|
|
16
|
+
const payload = structuredClone(valid)
|
|
17
|
+
expect(validator.validateFields(valid)).toEqual({ valid: true, errors: [] })
|
|
18
|
+
expect(validator.validateFields({ estimatedValue: valid.estimatedValue, nextContactDate: valid.nextContactDate }).valid).toBe(true)
|
|
19
|
+
const invalid = [ {}, { ...valid, estimatedValue: 12345 }, { ...valid, estimatedValue: { amount: 2 } },
|
|
20
|
+
{ ...valid, estimatedValue: { amount: '12345', currency: 'USD' } }, { ...valid, estimatedValue: { amount: -1, currency: 'USD' } },
|
|
21
|
+
{ ...valid, priority: 'urgent' }, { ...valid, nextContactDate: '2027-02-30' },
|
|
22
|
+
{ ...valid, nextContactDate: '2027-03-15T00:00:00Z' }, { ...valid, extra: true } ]
|
|
23
|
+
const native = makeAjv().compile(schema)
|
|
24
|
+
for (const fields of invalid) {
|
|
25
|
+
const checked = validator.validateFields(fields)
|
|
26
|
+
expect(checked.valid).toBe(native(fields) === true)
|
|
27
|
+
expect(checked.valid).toBe(false)
|
|
28
|
+
expect(checked.errors.length).toBeGreaterThan(0)
|
|
29
|
+
expect(checked.errors.every(error => error.message.length < 100)).toBe(true)
|
|
30
|
+
}
|
|
31
|
+
expect(validator.validateFields({ ...valid, estimatedValue: { amount: 2 } }).errors).toContainEqual({
|
|
32
|
+
path: 'estimatedValue.currency', keyword: 'required', schemaPath: '#/properties/estimatedValue/required', message: 'A required field is missing',
|
|
33
|
+
})
|
|
34
|
+
expect(schema).toEqual(copy)
|
|
35
|
+
expect(valid).toEqual(payload)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('supported Draft 7 predicates agree with native planning AJV, including references, compositions and full formats', () => {
|
|
39
|
+
const cases: Array<{ schema: AnySchema, values: unknown[] }> = [
|
|
40
|
+
{ schema: { type: ['number', 'null'], minimum: 0, maximum: 7, exclusiveMinimum: -1, exclusiveMaximum: 8 }, values: [null, 0, 7, -1, 8, '3'] },
|
|
41
|
+
{ schema: { type: 'integer', minimum: -3, maximum: 3 }, values: [-3, 0, 3, 1.99999999, 6.00000001, 4] },
|
|
42
|
+
{ schema: { type: 'string', minLength: 2, maxLength: 3, pattern: '^.+' }, values: ['ab', 'a😀', '😀', 'abcd', '', 2] },
|
|
43
|
+
{ schema: { type: 'array', items: [{ type: 'string' }, { type: 'integer' }], additionalItems: false }, values: [[], ['a'], ['a', 2], ['a', '2'], ['a', 2, 3]] },
|
|
44
|
+
{ schema: { type: 'array', items: { type: 'number' }, minItems: 1, maxItems: 3, uniqueItems: true, contains: { const: 2 } }, values: [[1, 2], [2], [], [1], [2, 2], [1, 2, 3, 4], ['2']] },
|
|
45
|
+
{ schema: { type: 'object', patternProperties: { '^a': { type: 'number' } }, additionalProperties: false, minProperties: 1, maxProperties: 2 },
|
|
46
|
+
values: [{ abc: 1 }, { abc: '1' }, {}, { abc: 1, other: 2 }, { a: 1, ab: 2, abc: 3 }] },
|
|
47
|
+
{ schema: { type: 'object', propertyNames: { pattern: '^[a-z]+$' }, dependencies: { a: ['b'], c: { required: ['d'] } } },
|
|
48
|
+
values: [{}, { a: 1, b: 2 }, { a: 1 }, { c: 1 }, { c: 1, d: 2 }, { A: 1 }] },
|
|
49
|
+
{ schema: { allOf: [{ type: 'number' }, { minimum: 2 }], not: { const: 3 } }, values: [2, 4, 1, 3, '2'] },
|
|
50
|
+
{ schema: { anyOf: [{ type: 'string', pattern: '^a' }, { type: 'integer' }] }, values: ['abc', 2, 'xyz', 1.5, null] },
|
|
51
|
+
{ schema: { oneOf: [{ type: 'number', minimum: 1 }, { type: 'number', maximum: 3 }] }, values: [0, 4, 2, '2'] },
|
|
52
|
+
{ schema: { if: { type: 'string' }, then: { minLength: 2 }, else: { type: 'integer' } }, values: ['ab', 2, 'a', 1.5, null] },
|
|
53
|
+
{ schema: { $defs: { n: { type: 'number' } }, $ref: '#/$defs/n', minimum: 5 }, values: [7, 3, '7'] },
|
|
54
|
+
{ schema: { $id: 'https://planning.test/fields', definitions: { n: { $id: 'number', type: 'number' } }, properties: { a: { $ref: 'number' } } },
|
|
55
|
+
values: [{ a: 2 }, { a: '2' }, {}] },
|
|
56
|
+
{ schema: { definitions: { blocked: false }, properties: { a: { $ref: '#/definitions/blocked' } } }, values: [{}, { a: 1 }] },
|
|
57
|
+
{ schema: { type: 'object', properties: { value: { type: 'number' }, next: { $ref: '#' } }, required: ['value'] },
|
|
58
|
+
values: [{ value: 1 }, { value: 1, next: { value: 2 } }, { value: 1, next: {} }] },
|
|
59
|
+
{ schema: { type: 'object', properties: { a: { type: 'number', default: 7 } }, required: ['a'] }, values: [{}, { a: 3 }, { a: '3' }] },
|
|
60
|
+
{ schema: { type: 'object', properties: { a: { type: 'string', nullable: true, minLength: 2, format: 'email' } } },
|
|
61
|
+
values: [{}, { a: null }, { a: 'a@example.test' }, { a: 'a' }, { a: 3 }] },
|
|
62
|
+
{ schema: { type: 'string', nullable: true, enum: ['a'] }, values: ['a', null, 'b'] },
|
|
63
|
+
{ schema: { type: 'string', nullable: true, const: 'a' }, values: ['a', null, 'b'] },
|
|
64
|
+
{ schema: { type: ['string', 'null'], nullable: true }, values: ['a', null, 3] },
|
|
65
|
+
{ schema: { type: 'string', nullable: false }, values: ['a', null, 3] },
|
|
66
|
+
{ schema: true, values: [1, 'a', null] }, { schema: false, values: [1, 'a', null] },
|
|
67
|
+
]
|
|
68
|
+
const formats = [
|
|
69
|
+
['date', ['2024-02-29', '2027-03-15', '2027-02-29', '2027-04-31', '2027-03-15T00:00:00Z']],
|
|
70
|
+
['time', ['12:00:00Z', '23:59:60Z', '12:00:00', '12:00:00+99:00', '25:00:00Z']],
|
|
71
|
+
['date-time', ['2027-03-15T12:00:00Z', '2027-03-15 12:00:00Z', '2027-02-30T12:00:00Z', '2027-03-15T12:00:00+99:00']],
|
|
72
|
+
['email', ['a@example.test', 'a@localhost', '.a@example.test', 'a..b@example.test']],
|
|
73
|
+
['ipv4', ['127.0.0.1', '01.2.3.4', '256.0.0.1']], ['uuid', ['123e4567-e89b-12d3-a456-426614174000', 'bad']],
|
|
74
|
+
['uri', ['https://planning.test/a', 'urn:planning:test', '/relative']],
|
|
75
|
+
['iso-time', ['12:00:00', '12:00:00Z', '25:00:00']], ['duration', ['P1D', 'P1W', 'P', 'P1.5D']],
|
|
76
|
+
] as const
|
|
77
|
+
for (const [name, values] of formats) cases.push({ schema: { type: 'string', format: name }, values: [...values] })
|
|
78
|
+
for (const { schema, values } of cases) {
|
|
79
|
+
const native = makeAjv().compile(schema)
|
|
80
|
+
const client = makePlanningFieldValidator(schema)
|
|
81
|
+
for (const value of values) {
|
|
82
|
+
// Untyped JSON can reach a validator with the wrong root kind; it must still refuse it.
|
|
83
|
+
expect(client.validateFields(value as Record<string, unknown>).valid).toBe(native(value) === true)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('malformed, unresolved and unsupported schemas fail closed at preparation, including dormant branches', () => {
|
|
89
|
+
const schemas: AnySchema[] = [
|
|
90
|
+
{ properties: { optional: { $ref: 'https://missing.test/private?credential=never-print' } } },
|
|
91
|
+
{ if: false, then: { $ref: '#/definitions/missing' } }, { $ref: '#' },
|
|
92
|
+
{ $schema: 'https://json-schema.org/draft/2020-12/schema' }, { type: 'number', multipleOf: 0.1 },
|
|
93
|
+
{ type: 'integer', multipleOf: 2 }, { type: 'integer', multipleOf: 1 },
|
|
94
|
+
{ nullable: true }, { type: 'string', nullable: 'true' }, { type: ['string', 'null'], nullable: false },
|
|
95
|
+
{ type: 'string', format: 'made-up' }, { type: 'number', format: 'int32' },
|
|
96
|
+
{ type: 'string', format: 'date', formatMinimum: '2027-01-01' }, { unevaluatedProperties: false },
|
|
97
|
+
{ required: 'field' }, { type: 'object', properties: { a: { minLength: -1 } } },
|
|
98
|
+
{ $defs: { invalid: { type: 'invented' } } }, { $defs: null }, { $defs: [] }, { pattern: '[' }, { enum: [] },
|
|
99
|
+
]
|
|
100
|
+
for (const schema of schemas) expect(() => makePlanningFieldValidator(schema)).toThrow(SchemaInvalid)
|
|
101
|
+
try { makePlanningFieldValidator(schemas[0]!) } catch (error) {
|
|
102
|
+
expect(String(error)).toContain('missing-reference:#/properties/optional/$ref')
|
|
103
|
+
expect(String(error)).not.toContain('credential')
|
|
104
|
+
expect(String(error)).not.toContain('https://missing')
|
|
105
|
+
}
|
|
106
|
+
const schema = { type: 'object', properties: { a: { type: 'number' } }, 'x-nativekit': { source: 'preserved' } }
|
|
107
|
+
expect(makePlanningFieldValidator(schema).validateFields({ a: 3 }).valid).toBe(true)
|
|
108
|
+
expect(schema['x-nativekit']).toEqual({ source: 'preserved' })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('unsafe keys and non-JSON values refuse with bounded field errors, while required names decode correctly', () => {
|
|
112
|
+
const validate = makePlanningFieldValidator({ type: 'object', properties: { nested: { type: 'object', required: ['a/b~c'] } } }).validateFields
|
|
113
|
+
expect(validate({ nested: {} }).errors[0]?.path).toBe('nested.a/b~c')
|
|
114
|
+
for (const value of [NaN, Infinity, undefined, 2n, new Date(), () => 3]) expect(validate({ nested: { value } }).valid).toBe(false)
|
|
115
|
+
expect(validate({ nested: { 'unsafe.key': 3 } }).errors.some(error => error.keyword === 'field-key')).toBe(true)
|
|
116
|
+
const cycle: Record<string, unknown> = {}; cycle.self = cycle
|
|
117
|
+
expect(validate(cycle).valid).toBe(false)
|
|
118
|
+
const longKey = 'field'.repeat(200)
|
|
119
|
+
const unsafe = validate({ [longKey]: undefined, [`${longKey}.nested`]: 3 })
|
|
120
|
+
expect(unsafe.valid).toBe(false)
|
|
121
|
+
expect(unsafe.errors).toHaveLength(2)
|
|
122
|
+
expect(unsafe.errors.every(error => error.path.length <= 512)).toBe(true)
|
|
123
|
+
const errors = makePlanningFieldValidator({ type: 'object', required: Array.from({ length: 100 }, (_, index) => `field${index}`) }).validateFields({})
|
|
124
|
+
expect(errors.valid).toBe(false)
|
|
125
|
+
expect(errors.errors).toHaveLength(64)
|
|
126
|
+
})
|
|
127
|
+
})
|