@onlineapps/conn-orch-validator 2.0.33 → 3.0.0
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/docs/DESIGN.md +64 -118
- package/package.json +2 -2
- package/src/ServiceReadinessValidator.js +35 -153
- package/src/ValidationOrchestrator.js +45 -10
- package/src/config.js +1 -1
- package/src/helpers/createServiceReadinessTests.js +11 -9
- package/src/index.js +13 -19
- package/src/validators/ServiceStructureValidator.js +48 -22
- package/examples/service-wrapper-usage.js +0 -250
- package/examples/three-tier-testing.js +0 -144
- package/src/ServiceTestHarness.js +0 -256
- package/src/ServiceValidator.js +0 -399
- package/src/TestOrchestrator.js +0 -736
package/docs/DESIGN.md
CHANGED
|
@@ -1,134 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# conn-orch-validator — Design
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Validation framework for OA Drive microservices. Drives two flows:
|
|
6
6
|
|
|
7
|
-
1. **
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
1. **Pre-validation** (Tier 1) — runs during `service-wrapper` startup. Verifies
|
|
8
|
+
structure (config files, package.json), readiness (endpoints respond, health
|
|
9
|
+
works) and produces a signed `validation-proof.json` stored under
|
|
10
|
+
`conn-runtime/`.
|
|
11
|
+
2. **Readiness checks** — reusable probes (`createServiceReadinessTests`,
|
|
12
|
+
`createPreValidationTests`) consumable from unit/component test suites
|
|
13
|
+
of individual biz services.
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
The single source of truth for service endpoint metadata is
|
|
16
|
+
[`operations.json`](../../../docs/standards/operations-registry-contract.md).
|
|
17
|
+
OpenAPI iteration (`paths`/`operationId`) is NOT supported — that legacy
|
|
18
|
+
surface was removed together with the now-retired `ServiceValidator` /
|
|
19
|
+
`TestOrchestrator` / `ServiceTestHarness` classes.
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
- Does NOT duplicate tests from individual connectors
|
|
15
|
-
- Uses existing connector functionality where available
|
|
16
|
-
- Focuses on integration and orchestration
|
|
21
|
+
## Key Principles
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
23
|
+
- **No duplication** — does not re-test individual connector logic; focuses on
|
|
24
|
+
integration, readiness and proof generation.
|
|
25
|
+
- **Production-ready** — the exact same code runs locally, in CI and during
|
|
26
|
+
wrapper startup.
|
|
27
|
+
- **Fail-fast** — missing logger, missing serviceUrl, missing operations.json:
|
|
28
|
+
immediate throw.
|
|
22
29
|
|
|
23
30
|
## Components
|
|
24
31
|
|
|
25
|
-
###
|
|
26
|
-
|
|
27
|
-
- `
|
|
28
|
-
- `
|
|
32
|
+
### Mock Infrastructure (for unit tests)
|
|
33
|
+
|
|
34
|
+
- `MockMQClient` — in-memory message queue simulation
|
|
35
|
+
- `MockRegistry` — service registry simulation
|
|
36
|
+
- `MockStorage` — object storage simulation
|
|
29
37
|
|
|
30
|
-
###
|
|
31
|
-
- `ServiceValidator` - OpenAPI compliance and endpoint testing
|
|
32
|
-
- `WorkflowTestRunner` - Cookbook execution testing
|
|
33
|
-
- `CookbookTestUtils` - Cookbook structure validation
|
|
38
|
+
### Production Validation
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
- `
|
|
40
|
+
- `ValidationOrchestrator` — 6-step pre-validation pipeline, emits proof
|
|
41
|
+
- `ServiceReadinessValidator` — score-based readiness checks (operations /
|
|
42
|
+
endpoints / health / cookbook / registry)
|
|
43
|
+
- `ServiceStructureValidator` — config layout checks (config/service/*)
|
|
44
|
+
- `ValidationProofGenerator` — fingerprint + codec helpers
|
|
45
|
+
- `CookbookTestRunner` / `WorkflowTestRunner` — cookbook execution probes
|
|
46
|
+
- `CookbookTestUtils` — static cookbook-structure validators
|
|
37
47
|
|
|
38
|
-
|
|
48
|
+
### Test Suite Helpers
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Developer testing their service
|
|
43
|
-
const harness = new ServiceTestHarness({
|
|
44
|
-
service: myExpressApp,
|
|
45
|
-
serviceName: 'my-service',
|
|
46
|
-
openApiSpec: spec,
|
|
47
|
-
mockInfrastructure: true // Use mocks
|
|
48
|
-
});
|
|
50
|
+
- `createServiceReadinessTests(options)` — Jest suite generator
|
|
51
|
+
- `createPreValidationTests(options)` — Jest suite generator
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
// Run tests...
|
|
52
|
-
await harness.stop();
|
|
53
|
-
```
|
|
53
|
+
## Integration Points
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
throw new Error('Service validation failed');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Testing Strategy
|
|
88
|
-
|
|
89
|
-
### What We Test
|
|
90
|
-
1. **Service Contract** - OpenAPI compliance
|
|
91
|
-
2. **Workflow Capability** - Can process cookbooks
|
|
92
|
-
3. **Infrastructure Integration** - Registry, MQ, Storage connectivity
|
|
93
|
-
4. **Health & Status** - Monitoring endpoints
|
|
94
|
-
|
|
95
|
-
### What We DON'T Test
|
|
96
|
-
1. Individual connector functionality (tested in connector packages)
|
|
97
|
-
2. Business logic correctness (service's responsibility)
|
|
98
|
-
3. Performance metrics (separate concern)
|
|
99
|
-
|
|
100
|
-
## Production Validation Flow
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
New Service Registration
|
|
104
|
-
↓
|
|
105
|
-
[Registry]
|
|
106
|
-
↓
|
|
107
|
-
Uses connector-testing
|
|
108
|
-
↓
|
|
109
|
-
1. Validate OpenAPI
|
|
110
|
-
2. Test all endpoints
|
|
111
|
-
3. Execute test cookbook
|
|
112
|
-
4. Verify health check
|
|
113
|
-
↓
|
|
114
|
-
Pass? → Accept : Reject
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Integration with Existing System
|
|
118
|
-
|
|
119
|
-
### Uses Existing Connectors
|
|
120
|
-
- Leverages real connector implementations where possible
|
|
121
|
-
- Only mocks what's necessary for isolation
|
|
122
|
-
- Validates against actual connector interfaces
|
|
123
|
-
|
|
124
|
-
### Complements Existing Tests
|
|
125
|
-
- Individual connectors have unit tests
|
|
126
|
-
- Services have business logic tests
|
|
127
|
-
- Connector-testing provides integration validation
|
|
128
|
-
|
|
129
|
-
## Future Enhancements
|
|
130
|
-
|
|
131
|
-
1. **Performance Benchmarking** - Baseline performance requirements
|
|
132
|
-
2. **Security Validation** - Authentication/authorization checks
|
|
133
|
-
3. **Chaos Testing** - Fault injection and recovery
|
|
134
|
-
4. **Compliance Checking** - Regulatory requirement validation
|
|
55
|
+
- `@onlineapps/service-wrapper` instantiates `ValidationOrchestrator` during
|
|
56
|
+
wrapper startup. Proof is cached under `conn-runtime/validation-proof.json`
|
|
57
|
+
(30-day validity, invalidated by config fingerprint change — includes
|
|
58
|
+
operations map, see
|
|
59
|
+
[operations-registry-contract.md §3](../../../docs/standards/operations-registry-contract.md)).
|
|
60
|
+
- Biz-service templates import `createPreValidationTests` /
|
|
61
|
+
`createServiceReadinessTests` from this package for their own test suites.
|
|
62
|
+
|
|
63
|
+
## What We Test
|
|
64
|
+
|
|
65
|
+
1. **Service contract** — operations.json structure + endpoint reachability
|
|
66
|
+
2. **Workflow capability** — can process cookbooks
|
|
67
|
+
3. **Infrastructure** — registry / MQ / storage connectivity
|
|
68
|
+
4. **Health** — `/health` returns 200
|
|
69
|
+
|
|
70
|
+
## What We DO NOT Test
|
|
71
|
+
|
|
72
|
+
1. Individual connector behavior (tested in connector packages)
|
|
73
|
+
2. Business logic correctness (service responsibility)
|
|
74
|
+
3. Performance / load (separate concern)
|
|
75
|
+
|
|
76
|
+
## Related Documentation
|
|
77
|
+
|
|
78
|
+
- [operations-registry-contract.md](../../../docs/standards/operations-registry-contract.md) — operations.json schema
|
|
79
|
+
- [validation-probe-contract.md](../../../docs/standards/validation-probe-contract.md) — required `example`/`default` fields for probes
|
|
80
|
+
- [validator.md](../../../docs/architecture/validator.md) — architecture overview
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/conn-orch-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Validation orchestrator for OA Drive microservices - coordinates validation across all layers (base, infra, orch, business)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"author": "OnlineApps",
|
|
22
22
|
"license": "PROPRIETARY",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@onlineapps/service-validator-core": "1.0.
|
|
24
|
+
"@onlineapps/service-validator-core": "1.0.13",
|
|
25
25
|
"@onlineapps/runtime-config": "1.0.2",
|
|
26
26
|
"ajv": "^8.12.0",
|
|
27
27
|
"ajv-formats": "^2.1.1",
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const ServiceValidator = require('./ServiceValidator');
|
|
4
3
|
const CookbookTestUtils = require('./CookbookTestUtils');
|
|
5
|
-
const { resolveHeaders } = require('./utils/resolveHeaders');
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
* ServiceReadinessValidator - Orchestrates complete service validation
|
|
9
|
-
* Used by service-wrapper to verify service is ready for production
|
|
6
|
+
* ServiceReadinessValidator - Orchestrates complete service validation.
|
|
10
7
|
*
|
|
11
|
-
*
|
|
8
|
+
* Used by service-wrapper to verify a service is ready for production.
|
|
9
|
+
* Works exclusively against the v3 operations.json contract — operations
|
|
10
|
+
* are dispatched via the handler registry (RFC §5.3, §5.9), not HTTP
|
|
11
|
+
* per-operation endpoints. The HTTP loopback check was removed when v3
|
|
12
|
+
* adopted in-process handler invocation.
|
|
13
|
+
*
|
|
14
|
+
* @see /api/docs/architecture/biz-service-invocation-model.md §5.3
|
|
15
|
+
* @see /api/docs/standards/validation-probe-contract.md
|
|
12
16
|
*/
|
|
13
17
|
class ServiceReadinessValidator {
|
|
14
18
|
constructor(options = {}) {
|
|
15
19
|
if (!options.logger || typeof options.logger.warn !== 'function') {
|
|
16
20
|
throw new Error('[ServiceReadinessValidator] Logger is required — Expected object with warn() method');
|
|
17
21
|
}
|
|
18
|
-
this.validator = new ServiceValidator(options);
|
|
19
22
|
this.logger = options.logger;
|
|
20
23
|
|
|
21
24
|
// Readiness checks: core (80 points) + optional (20 points) = 100 points max
|
|
22
25
|
// Core checks ALWAYS run, optional checks run if testCookbook/registry provided
|
|
23
26
|
// See: /shared/connector/conn-orch-validator/README.md for usage pattern
|
|
27
|
+
// v3: per-op HTTP endpoint probing is retired. Operations are dispatched in-process
|
|
28
|
+
// via the handler registry. The 30-point weight previously spent on `endpoints`
|
|
29
|
+
// is folded into `operations` so the total stays at 100.
|
|
24
30
|
this.checks = {
|
|
25
|
-
operations: { weight:
|
|
26
|
-
endpoints: { weight: 30, required: true }, // all endpoints respond correctly
|
|
31
|
+
operations: { weight: 60, required: true }, // operations.json v3 structure valid
|
|
27
32
|
health: { weight: 20, required: true }, // health check works
|
|
28
33
|
cookbook: { weight: 15, required: false }, // OPTIONAL - cookbook valid (with mocks)
|
|
29
34
|
registry: { weight: 5, required: false } // OPTIONAL - registry compatible (MockRegistry)
|
|
@@ -69,15 +74,8 @@ class ServiceReadinessValidator {
|
|
|
69
74
|
results.checks.operations = { passed: false, error: 'Missing operations.json' };
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
// 2.
|
|
73
|
-
|
|
74
|
-
results.checks.endpoints = await this.checkOperationsEndpoints(url, operations);
|
|
75
|
-
if (results.checks.endpoints.passed) {
|
|
76
|
-
results.score += this.checks.endpoints.weight;
|
|
77
|
-
} else if (this.checks.endpoints.required) {
|
|
78
|
-
results.errors.push('Endpoint validation failed');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
77
|
+
// 2. (v3) Per-op HTTP endpoint probing retired — operations are dispatched
|
|
78
|
+
// in-process via the handler registry. No network call per operation.
|
|
81
79
|
|
|
82
80
|
// 3. Verify health check
|
|
83
81
|
results.checks.health = await this.checkHealth(url + healthEndpoint);
|
|
@@ -128,14 +126,15 @@ class ServiceReadinessValidator {
|
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
/**
|
|
131
|
-
* Check operations.json compliance
|
|
129
|
+
* Check operations.json compliance (v3 — handler registry).
|
|
130
|
+
* Required per operation: description, handler, bundle_scope, input, output.
|
|
131
|
+
* Forbidden (v2): endpoint, method, path.
|
|
132
132
|
*/
|
|
133
133
|
async checkOperationsCompliance(operations) {
|
|
134
134
|
try {
|
|
135
135
|
const errors = [];
|
|
136
136
|
const warnings = [];
|
|
137
137
|
|
|
138
|
-
// Validate operations structure
|
|
139
138
|
if (!operations || typeof operations !== 'object') {
|
|
140
139
|
return {
|
|
141
140
|
passed: false,
|
|
@@ -151,17 +150,23 @@ class ServiceReadinessValidator {
|
|
|
151
150
|
};
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
|
|
153
|
+
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
154
|
+
const handlerPattern = /^handlers\/[a-zA-Z0-9_\/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
155
|
+
const forbiddenV2Fields = ['endpoint', 'method', 'path'];
|
|
156
|
+
|
|
155
157
|
for (const [name, operation] of Object.entries(operations)) {
|
|
156
|
-
// Required fields
|
|
157
158
|
if (!operation.description) {
|
|
158
|
-
|
|
159
|
+
warnings.push(`Operation '${name}' missing description`);
|
|
159
160
|
}
|
|
160
|
-
if (!operation.
|
|
161
|
-
errors.push(`Operation '${name}' missing
|
|
161
|
+
if (!operation.handler) {
|
|
162
|
+
errors.push(`Operation '${name}' missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
163
|
+
} else if (!handlerPattern.test(operation.handler)) {
|
|
164
|
+
errors.push(`Operation '${name}' has invalid handler ref: '${operation.handler}'`);
|
|
162
165
|
}
|
|
163
|
-
if (!operation.
|
|
164
|
-
errors.push(`Operation '${name}' missing
|
|
166
|
+
if (!operation.bundle_scope) {
|
|
167
|
+
errors.push(`Operation '${name}' missing bundle_scope (platform|tenant|workspace)`);
|
|
168
|
+
} else if (!validScopes.includes(operation.bundle_scope)) {
|
|
169
|
+
errors.push(`Operation '${name}' has invalid bundle_scope: '${operation.bundle_scope}'`);
|
|
165
170
|
}
|
|
166
171
|
if (!operation.input) {
|
|
167
172
|
errors.push(`Operation '${name}' missing input schema`);
|
|
@@ -170,15 +175,10 @@ class ServiceReadinessValidator {
|
|
|
170
175
|
errors.push(`Operation '${name}' missing output schema`);
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Validate endpoint format
|
|
180
|
-
if (operation.endpoint && !operation.endpoint.startsWith('/')) {
|
|
181
|
-
warnings.push(`Operation '${name}' endpoint should start with /`);
|
|
178
|
+
for (const forbidden of forbiddenV2Fields) {
|
|
179
|
+
if (forbidden in operation) {
|
|
180
|
+
errors.push(`Operation '${name}' has retired v2 field '${forbidden}' — remove per RFC §5.3`);
|
|
181
|
+
}
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -196,85 +196,6 @@ class ServiceReadinessValidator {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/**
|
|
200
|
-
* Check all operations endpoints respond correctly
|
|
201
|
-
*/
|
|
202
|
-
async checkOperationsEndpoints(serviceUrl, operations) {
|
|
203
|
-
const axios = require('axios');
|
|
204
|
-
const results = {
|
|
205
|
-
passed: true,
|
|
206
|
-
total: Object.keys(operations).length,
|
|
207
|
-
successful: 0,
|
|
208
|
-
failed: 0,
|
|
209
|
-
details: []
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
for (const [operationName, operation] of Object.entries(operations)) {
|
|
213
|
-
try {
|
|
214
|
-
const endpoint = operation.endpoint;
|
|
215
|
-
const method = operation.method.toLowerCase();
|
|
216
|
-
const url = `${serviceUrl}${endpoint}`;
|
|
217
|
-
|
|
218
|
-
// Generate test input based on schema
|
|
219
|
-
const testInput = this.generateTestInput(operation.input);
|
|
220
|
-
|
|
221
|
-
const validationTenantId = process.env.OA_VALIDATION_TENANT_ID;
|
|
222
|
-
const validationWorkspaceId = process.env.OA_VALIDATION_WORKSPACE_ID;
|
|
223
|
-
if (!validationTenantId || !validationWorkspaceId) {
|
|
224
|
-
throw new Error('[ServiceReadinessValidator] Missing required environment variables OA_VALIDATION_TENANT_ID and/or OA_VALIDATION_WORKSPACE_ID');
|
|
225
|
-
}
|
|
226
|
-
const headers = {
|
|
227
|
-
'x-validation-request': 'true',
|
|
228
|
-
'x-tenant-id': validationTenantId,
|
|
229
|
-
'x-workspace-id': validationWorkspaceId,
|
|
230
|
-
...resolveHeaders(operation.headers)
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Make request
|
|
234
|
-
const response = await axios({
|
|
235
|
-
method,
|
|
236
|
-
url,
|
|
237
|
-
data: method !== 'get' ? testInput : undefined,
|
|
238
|
-
params: method === 'get' ? testInput : undefined,
|
|
239
|
-
headers,
|
|
240
|
-
timeout: 5000,
|
|
241
|
-
validateStatus: () => true // Accept any status for validation
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Check response: 2xx = success, 4xx = endpoint works but rejected test data (valid)
|
|
245
|
-
// Only 5xx (server error) or connection failure = invalid endpoint
|
|
246
|
-
const isValid = response.status < 500;
|
|
247
|
-
|
|
248
|
-
if (isValid) {
|
|
249
|
-
results.successful++;
|
|
250
|
-
} else {
|
|
251
|
-
results.failed++;
|
|
252
|
-
results.passed = false;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
results.details.push({
|
|
256
|
-
operation: operationName,
|
|
257
|
-
endpoint,
|
|
258
|
-
method: method.toUpperCase(),
|
|
259
|
-
status: response.status,
|
|
260
|
-
valid: isValid
|
|
261
|
-
});
|
|
262
|
-
} catch (error) {
|
|
263
|
-
results.failed++;
|
|
264
|
-
results.passed = false;
|
|
265
|
-
results.details.push({
|
|
266
|
-
operation: operationName,
|
|
267
|
-
endpoint: operation.endpoint,
|
|
268
|
-
method: operation.method,
|
|
269
|
-
valid: false,
|
|
270
|
-
error: error.message
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return results;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
199
|
/**
|
|
279
200
|
* Check health endpoint
|
|
280
201
|
*/
|
|
@@ -349,45 +270,6 @@ class ServiceReadinessValidator {
|
|
|
349
270
|
}
|
|
350
271
|
}
|
|
351
272
|
|
|
352
|
-
/**
|
|
353
|
-
* Generate test input based on operation input schema.
|
|
354
|
-
*
|
|
355
|
-
* Resolution order per field: example > default > enum[0]
|
|
356
|
-
* If none found for a required field, throws with actionable message.
|
|
357
|
-
*
|
|
358
|
-
* Contract: every required field in operations.json MUST have 'example' or 'default'.
|
|
359
|
-
* See: docs/standards/validation-probe-contract.md
|
|
360
|
-
*/
|
|
361
|
-
generateTestInput(inputSchema) {
|
|
362
|
-
const testData = {};
|
|
363
|
-
|
|
364
|
-
for (const [fieldName, fieldSpec] of Object.entries(inputSchema)) {
|
|
365
|
-
if (!fieldSpec.required) continue;
|
|
366
|
-
|
|
367
|
-
const value = this._resolveTestValue(fieldName, fieldSpec);
|
|
368
|
-
testData[fieldName] = value;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return testData;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Resolve a single test value for a required input field.
|
|
376
|
-
* Throws if no value can be determined — forces service authors to provide examples.
|
|
377
|
-
* @private
|
|
378
|
-
*/
|
|
379
|
-
_resolveTestValue(fieldName, fieldSpec) {
|
|
380
|
-
if (fieldSpec.example !== undefined) return fieldSpec.example;
|
|
381
|
-
if (fieldSpec.default !== undefined) return fieldSpec.default;
|
|
382
|
-
if (Array.isArray(fieldSpec.enum) && fieldSpec.enum.length > 0) return fieldSpec.enum[0];
|
|
383
|
-
|
|
384
|
-
throw new Error(
|
|
385
|
-
`[ServiceReadinessValidator] No test value for required field '${fieldName}' ` +
|
|
386
|
-
`(type: ${fieldSpec.type}) — Add 'example' or 'default' to operations.json input schema. ` +
|
|
387
|
-
`See: docs/standards/validation-probe-contract.md`
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
273
|
/**
|
|
392
274
|
* Get recommendation based on results
|
|
393
275
|
*/
|
|
@@ -329,7 +329,9 @@ class ValidationOrchestrator {
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
/**
|
|
332
|
-
* Step 3: Validate operations compliance
|
|
332
|
+
* Step 3: Validate operations compliance (v3 — handler registry dispatch).
|
|
333
|
+
* Required per operation: handler ('handlers/<path>#<export>'), bundle_scope, input, output.
|
|
334
|
+
* Forbidden (v2): endpoint, method, path.
|
|
333
335
|
*/
|
|
334
336
|
async validateOperations() {
|
|
335
337
|
try {
|
|
@@ -337,18 +339,21 @@ class ValidationOrchestrator {
|
|
|
337
339
|
const operations = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
338
340
|
const errors = [];
|
|
339
341
|
|
|
340
|
-
|
|
342
|
+
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
343
|
+
const handlerPattern = /^handlers\/[a-zA-Z0-9_\/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
344
|
+
const forbiddenV2Fields = ['endpoint', 'method', 'path'];
|
|
345
|
+
|
|
341
346
|
for (const [opName, opDef] of Object.entries(operations.operations || {})) {
|
|
342
|
-
if (!opDef.
|
|
343
|
-
errors.push(`Operation ${opName}: missing
|
|
344
|
-
} else if (!opDef.
|
|
345
|
-
errors.push(`Operation ${opName}:
|
|
347
|
+
if (!opDef.handler) {
|
|
348
|
+
errors.push(`Operation ${opName}: missing handler (v3 — 'handlers/<path>#<export>')`);
|
|
349
|
+
} else if (!handlerPattern.test(opDef.handler)) {
|
|
350
|
+
errors.push(`Operation ${opName}: invalid handler ref '${opDef.handler}' — expected 'handlers/<path>#<exportName>'`);
|
|
346
351
|
}
|
|
347
352
|
|
|
348
|
-
if (!opDef.
|
|
349
|
-
errors.push(`Operation ${opName}: missing
|
|
350
|
-
} else if (!
|
|
351
|
-
errors.push(`Operation ${opName}: invalid
|
|
353
|
+
if (!opDef.bundle_scope) {
|
|
354
|
+
errors.push(`Operation ${opName}: missing bundle_scope (platform|tenant|workspace)`);
|
|
355
|
+
} else if (!validScopes.includes(opDef.bundle_scope)) {
|
|
356
|
+
errors.push(`Operation ${opName}: invalid bundle_scope '${opDef.bundle_scope}' — expected one of ${validScopes.join(', ')}`);
|
|
352
357
|
}
|
|
353
358
|
|
|
354
359
|
if (!opDef.input) {
|
|
@@ -357,6 +362,12 @@ class ValidationOrchestrator {
|
|
|
357
362
|
if (!opDef.output) {
|
|
358
363
|
errors.push(`Operation ${opName}: missing output schema`);
|
|
359
364
|
}
|
|
365
|
+
|
|
366
|
+
for (const forbidden of forbiddenV2Fields) {
|
|
367
|
+
if (forbidden in opDef) {
|
|
368
|
+
errors.push(`Operation ${opName}: retired v2 field '${forbidden}' present — remove per RFC §5.3`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
360
371
|
}
|
|
361
372
|
|
|
362
373
|
console.log(`[ValidationOrchestrator] ✓ Operations compliance: ${errors.length === 0 ? 'PASS' : 'FAIL'}`);
|
|
@@ -374,6 +385,11 @@ class ValidationOrchestrator {
|
|
|
374
385
|
|
|
375
386
|
/**
|
|
376
387
|
* Step 4: Run cookbook tests
|
|
388
|
+
*
|
|
389
|
+
* v3 note: per-operation HTTP dispatch is retired. If operations.json is v3
|
|
390
|
+
* (schema_version "3.0" or any operation declares `handler`), cookbook runs
|
|
391
|
+
* are skipped here with a warning. Handler-registry-based cookbook dispatch
|
|
392
|
+
* is tracked separately (see RFC §5.3, §5.9).
|
|
377
393
|
*/
|
|
378
394
|
async runCookbookTests() {
|
|
379
395
|
try {
|
|
@@ -390,6 +406,25 @@ class ValidationOrchestrator {
|
|
|
390
406
|
};
|
|
391
407
|
}
|
|
392
408
|
|
|
409
|
+
const operationsFile = path.join(this.configPath, 'operations.json');
|
|
410
|
+
let isV3 = false;
|
|
411
|
+
try {
|
|
412
|
+
const ops = JSON.parse(fs.readFileSync(operationsFile, 'utf8'));
|
|
413
|
+
const opValues = Object.values(ops.operations || {});
|
|
414
|
+
isV3 = ops.schema_version === '3.0' || opValues.some(o => o && typeof o === 'object' && 'handler' in o);
|
|
415
|
+
} catch (_) { /* fall through to runner */ }
|
|
416
|
+
|
|
417
|
+
if (isV3) {
|
|
418
|
+
console.warn('[ValidationOrchestrator] Step 4: cookbook HTTP dispatch is v2-only. Skipping for v3 ops schema.');
|
|
419
|
+
return {
|
|
420
|
+
success: true,
|
|
421
|
+
total: 0,
|
|
422
|
+
passed: 0,
|
|
423
|
+
failed: 0,
|
|
424
|
+
warnings: ['Cookbook tests skipped — v3 handler-registry dispatch not yet implemented in CookbookTestRunner']
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
393
428
|
const result = await this.cookbookRunner.runCookbooks(cookbooksPath);
|
|
394
429
|
|
|
395
430
|
console.log(`[ValidationOrchestrator] ✓ Cookbook tests: ${result.passed}/${result.total} passed`);
|
package/src/config.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Runtime configuration schema for @onlineapps/conn-orch-validator.
|
|
5
5
|
*
|
|
6
6
|
* Uses @onlineapps/runtime-config for unified priority:
|
|
7
|
-
* 1. Explicit config (passed to
|
|
7
|
+
* 1. Explicit config (passed to ValidationOrchestrator/readiness options)
|
|
8
8
|
* 2. Environment variable
|
|
9
9
|
* 3. Module-owned defaults (none for topology)
|
|
10
10
|
*
|
|
@@ -179,14 +179,12 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
179
179
|
expect(result.score).toBe(100);
|
|
180
180
|
expect(result.checks.health?.passed).toBe(true);
|
|
181
181
|
expect(result.checks.operations?.passed).toBe(true);
|
|
182
|
-
expect(result.checks.endpoints?.passed).toBe(true);
|
|
183
182
|
expect(result.checks.cookbook?.passed).toBe(true);
|
|
184
183
|
expect(result.checks.registry?.passed).toBe(true);
|
|
185
184
|
} else {
|
|
186
185
|
expect(result.score).toBeGreaterThanOrEqual(80);
|
|
187
186
|
expect(result.checks.health?.passed).toBe(true);
|
|
188
187
|
expect(result.checks.operations?.passed).toBe(true);
|
|
189
|
-
expect(result.checks.endpoints?.passed).toBe(true);
|
|
190
188
|
}
|
|
191
189
|
}, timeout);
|
|
192
190
|
|
|
@@ -199,7 +197,7 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
199
197
|
expect(data.status).toBe('healthy');
|
|
200
198
|
});
|
|
201
199
|
|
|
202
|
-
test('service has valid operations specification', () => {
|
|
200
|
+
test('service has valid operations specification (v3)', () => {
|
|
203
201
|
expect(operations).toBeDefined();
|
|
204
202
|
expect(operations.operations || operations).toBeDefined();
|
|
205
203
|
|
|
@@ -207,12 +205,16 @@ function createServiceReadinessTests(testsDir, options = {}) {
|
|
|
207
205
|
expect(typeof operationsFlat).toBe('object');
|
|
208
206
|
expect(Object.keys(operationsFlat).length).toBeGreaterThan(0);
|
|
209
207
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
expect(operationSpec
|
|
215
|
-
expect(
|
|
208
|
+
const validScopes = ['platform', 'tenant', 'workspace'];
|
|
209
|
+
const handlerPattern = /^handlers\/[a-zA-Z0-9_/-]+#[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
210
|
+
|
|
211
|
+
for (const [, operationSpec] of Object.entries(operationsFlat)) {
|
|
212
|
+
expect(operationSpec).toHaveProperty('handler');
|
|
213
|
+
expect(operationSpec).toHaveProperty('bundle_scope');
|
|
214
|
+
expect(operationSpec.handler).toMatch(handlerPattern);
|
|
215
|
+
expect(validScopes).toContain(operationSpec.bundle_scope);
|
|
216
|
+
expect(operationSpec).not.toHaveProperty('endpoint');
|
|
217
|
+
expect(operationSpec).not.toHaveProperty('method');
|
|
216
218
|
}
|
|
217
219
|
});
|
|
218
220
|
});
|