@o-lang/resolver-tests 1.0.3 → 1.0.5
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/R-006-resolver-runtime-shape/test.json +22 -0
- package/R-007-resolver-failure-contract/test.json +22 -0
- package/R-008-resolver-input-validation/test.json +22 -0
- package/R-009-retry-semantics/test.json +22 -0
- package/R-010-output-contract/test.json +28 -0
- package/R-011-determinism/test.json +23 -0
- package/R-012-side-effects/test.json +23 -0
- package/badges/certified.svg +18 -0
- package/lib/runner.js +121 -174
- package/package.json +1 -1
- package/run.js +8 -4
- package/R-001-allowlist/README.md +0 -21
- package/R-001-allowlist/test.json +0 -55
- package/R-001-allowlist/workflow.ol +0 -7
- package/R-002-io-contract/README.md +0 -26
- package/R-002-io-contract/test.json +0 -76
- package/R-002-io-contract/workflow.ol +0 -13
- package/R-003-failure-modes/README +0 -27
- package/R-003-failure-modes/test.json +0 -78
- package/R-003-failure-modes/workflow.ol +0 -13
- package/R-004-invalid-syntax/README.md +0 -5
- package/R-004-invalid-syntax/test.json +0 -57
- package/R-004-invalid-syntax/workflow.ol +0 -16
- package/certified.svg +0 -6
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-006-runtime-shape",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures resolver exports a callable function.",
|
|
6
|
+
"spec_ref": ["§4.1 Resolver Runtime Interface"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "resolver_is_function",
|
|
17
|
+
"type": "resolver_is_callable",
|
|
18
|
+
"severity": "fatal",
|
|
19
|
+
"description": "Resolver must be a callable function"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-007-failure-contract",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures any thrown failure code is declared in the resolver's failure contract.",
|
|
6
|
+
"spec_ref": ["§4.4 Resolver Failure Modes"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "failure_code_declared",
|
|
17
|
+
"type": "resolver_failure_declared",
|
|
18
|
+
"severity": "fatal",
|
|
19
|
+
"description": "Any error code thrown by the resolver must be listed in its failures[] declaration"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-008-input-validation",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures resolver rejects invocations missing required inputs.",
|
|
6
|
+
"spec_ref": ["§4.2 Resolver Input Contracts"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "missing_required_input_rejected",
|
|
17
|
+
"type": "rejects_missing_required_input",
|
|
18
|
+
"severity": "fatal",
|
|
19
|
+
"description": "Resolver must fail when a required input is omitted"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-009-retry-semantics",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures resolver retry behavior does not exceed declared retry limits when failures occur.",
|
|
6
|
+
"spec_ref": ["§4.4 Resolver Failure Modes"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "retry_limit_respected",
|
|
17
|
+
"type": "retry_count_within_declared_limit",
|
|
18
|
+
"severity": "fatal",
|
|
19
|
+
"description": "Resolver must not retry more times than declared in its failure specification"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-010-output-contract",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures resolver output conforms to its declared output contract.",
|
|
6
|
+
"spec_ref": ["§4.3 Resolver Output Contracts"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "output_is_object",
|
|
17
|
+
"type": "output_is_object",
|
|
18
|
+
"severity": "fatal",
|
|
19
|
+
"description": "Resolver must return an object"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "output_fields_match_contract",
|
|
23
|
+
"type": "output_fields_match_contract",
|
|
24
|
+
"severity": "fatal",
|
|
25
|
+
"description": "Returned object must contain all fields declared in outputs[]"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-011-determinism",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures resolver produces deterministic output for identical inputs.",
|
|
6
|
+
"spec_ref": ["§5.1 Deterministic Execution"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "same_input_same_output",
|
|
17
|
+
"type": "deterministic_output",
|
|
18
|
+
"severity": "warning",
|
|
19
|
+
"description": "Identical invocations should produce identical outputs"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"test_id": "R-012-side-effects",
|
|
3
|
+
"protocol_version": "1.1",
|
|
4
|
+
"category": "resolver-runtime",
|
|
5
|
+
"description": "Ensures resolver does not mutate global state or environment unexpectedly.",
|
|
6
|
+
"spec_ref": ["§5.3 Resolver Isolation"],
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"inputs": [
|
|
9
|
+
{
|
|
10
|
+
"invoke": {}
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"assertions": [
|
|
15
|
+
{
|
|
16
|
+
"id": "no_global_mutation",
|
|
17
|
+
"type": "no_global_state_mutation",
|
|
18
|
+
"severity": "warning",
|
|
19
|
+
"description": "Resolver should not mutate global objects, process.env, or module-level state"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg xmlns='http://www.w3.org/2000/svg' width='120' height='20'>
|
|
2
|
+
<linearGradient id='b' x2='0' y2='100%'>
|
|
3
|
+
<stop offset='0' stop-color='#bbb' stop-opacity='.1'/>
|
|
4
|
+
<stop offset='1' stop-opacity='.1'/>
|
|
5
|
+
</linearGradient>
|
|
6
|
+
<mask id='a'>
|
|
7
|
+
<rect width='120' height='20' rx='3' fill='#fff'/>
|
|
8
|
+
</mask>
|
|
9
|
+
<g mask='url(#a)'>
|
|
10
|
+
<rect width='70' height='20' fill='#555'/>
|
|
11
|
+
<rect x='70' width='50' height='20' fill='red'/>
|
|
12
|
+
<rect width='120' height='20' fill='url(#b)'/>
|
|
13
|
+
</g>
|
|
14
|
+
<g fill='#fff' text-anchor='middle' font-family='Verdana' font-size='11'>
|
|
15
|
+
<text x='35' y='14'>Certified</text>
|
|
16
|
+
<text x='95' y='14'>fail</text>
|
|
17
|
+
</g>
|
|
18
|
+
</svg>
|
package/lib/runner.js
CHANGED
|
@@ -1,96 +1,6 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
|
|
4
|
-
// ----------------------
|
|
5
|
-
// Helper: Deep get by path (e.g., "steps[0].saveAs")
|
|
6
|
-
// ----------------------
|
|
7
|
-
function getNestedValue(obj, path) {
|
|
8
|
-
if (!path) return obj;
|
|
9
|
-
return path
|
|
10
|
-
.split(/\.(?![^\[]*\])|[\[\]]/)
|
|
11
|
-
.filter(Boolean)
|
|
12
|
-
.reduce((cur, prop) => cur?.[prop], obj);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// ----------------------
|
|
16
|
-
// Validator functions for WORKFLOW AST
|
|
17
|
-
// ----------------------
|
|
18
|
-
function checkAllowlist(ast, expected) {
|
|
19
|
-
const allowed = ast.allowedResolvers || [];
|
|
20
|
-
return (
|
|
21
|
-
Array.isArray(allowed) &&
|
|
22
|
-
allowed.length === expected.length &&
|
|
23
|
-
expected.every(r => allowed.includes(r))
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function checkResolverNameNormalization(ast) {
|
|
28
|
-
const allowed = ast.allowedResolvers || [];
|
|
29
|
-
const pattern = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
30
|
-
return Array.isArray(allowed) && allowed.every(name => pattern.test(name));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function checkWorkflowName(ast, expected) {
|
|
34
|
-
return ast.name === expected;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function checkReturnValues(ast, expected) {
|
|
38
|
-
const returns = ast.returnValues || [];
|
|
39
|
-
return (
|
|
40
|
-
Array.isArray(returns) &&
|
|
41
|
-
returns.length === expected.length &&
|
|
42
|
-
expected.every(v => returns.includes(v))
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function checkNoWarnings(status, expectedCount = 0) {
|
|
47
|
-
const warnings = status?.__warnings || [];
|
|
48
|
-
return warnings.length === expectedCount;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function checkStepType(ast, assertion) {
|
|
52
|
-
const steps = ast.steps || [];
|
|
53
|
-
const step = steps[assertion.stepIndex];
|
|
54
|
-
return step && step.type === assertion.expected;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function checkStepSaveAs(ast, assertion) {
|
|
58
|
-
const steps = ast.steps || [];
|
|
59
|
-
const step = steps[assertion.stepIndex];
|
|
60
|
-
return step && step.saveAs === assertion.expected;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function checkStepFailurePolicies(ast, assertion) {
|
|
64
|
-
const steps = ast.steps || [];
|
|
65
|
-
const step = steps[assertion.stepIndex];
|
|
66
|
-
if (!step || !step.failurePolicies) return false;
|
|
67
|
-
|
|
68
|
-
const expected = assertion.expected;
|
|
69
|
-
return Object.keys(expected).every(code => {
|
|
70
|
-
const policy = step.failurePolicies[code];
|
|
71
|
-
return (
|
|
72
|
-
policy &&
|
|
73
|
-
policy.action === expected[code].action &&
|
|
74
|
-
policy.count === expected[code].count
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function checkContainsWarning(status, assertion) {
|
|
80
|
-
const warnings = status?.__warnings || [];
|
|
81
|
-
const needle = assertion.expected_substring.toLowerCase();
|
|
82
|
-
return warnings.some(w =>
|
|
83
|
-
(typeof w === "string" ? w : w.message || "")
|
|
84
|
-
.toLowerCase()
|
|
85
|
-
.includes(needle)
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function checkStatusGreaterThan(status, assertion) {
|
|
90
|
-
const value = getNestedValue(status, assertion.path);
|
|
91
|
-
return typeof value === "number" && value > assertion.expected;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
4
|
// ----------------------
|
|
95
5
|
// Validator functions for RESOLVER METADATA (R-005)
|
|
96
6
|
// ----------------------
|
|
@@ -147,59 +57,71 @@ function checkResolverFailuresValid(resolverMeta) {
|
|
|
147
57
|
}
|
|
148
58
|
|
|
149
59
|
// ----------------------
|
|
150
|
-
//
|
|
60
|
+
// Validator functions for RESOLVER RUNTIME (R-006 → R-012)
|
|
151
61
|
// ----------------------
|
|
152
|
-
const assertionHandlers = {
|
|
153
|
-
// Workflow AST assertions
|
|
154
|
-
allowed_resolvers_listed: (ast, assertion) =>
|
|
155
|
-
checkAllowlist(ast, assertion.expected),
|
|
156
|
-
|
|
157
|
-
resolver_names_normalized: ast =>
|
|
158
|
-
checkResolverNameNormalization(ast),
|
|
159
62
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
workflow_return_values: (ast, assertion) =>
|
|
164
|
-
checkReturnValues(ast, assertion.expected),
|
|
63
|
+
function checkResolverIsCallable(resolver) {
|
|
64
|
+
return typeof resolver === 'function';
|
|
65
|
+
}
|
|
165
66
|
|
|
166
|
-
|
|
167
|
-
|
|
67
|
+
function checkFailureCodeDeclared(observedError, resolverMeta) {
|
|
68
|
+
if (!observedError?.code) return true; // no error = pass
|
|
69
|
+
const declaredCodes = (resolverMeta.failures || []).map(f => f.code);
|
|
70
|
+
return declaredCodes.includes(observedError.code);
|
|
71
|
+
}
|
|
168
72
|
|
|
169
|
-
|
|
170
|
-
|
|
73
|
+
function checkRejectsMissingRequiredInput(invocationResult) {
|
|
74
|
+
return invocationResult.threw; // must throw when required input missing
|
|
75
|
+
}
|
|
171
76
|
|
|
172
|
-
|
|
173
|
-
|
|
77
|
+
function checkRetryCountWithinLimit(observedRetries, resolverMeta, errorCode) {
|
|
78
|
+
const failure = (resolverMeta.failures || []).find(f => f.code === errorCode);
|
|
79
|
+
if (!failure) return true;
|
|
80
|
+
return observedRetries <= failure.retries;
|
|
81
|
+
}
|
|
174
82
|
|
|
175
|
-
|
|
176
|
-
|
|
83
|
+
function checkOutputIsObject(output) {
|
|
84
|
+
return output !== null && typeof output === 'object' && !Array.isArray(output);
|
|
85
|
+
}
|
|
177
86
|
|
|
178
|
-
|
|
179
|
-
|
|
87
|
+
function checkOutputFieldsMatchContract(output, resolverMeta) {
|
|
88
|
+
const declaredNames = (resolverMeta.outputs || []).map(o => o.name);
|
|
89
|
+
return declaredNames.every(name => name in output);
|
|
90
|
+
}
|
|
180
91
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
92
|
+
function checkDeterministicOutput(results) {
|
|
93
|
+
if (results.length < 2) return true;
|
|
94
|
+
const first = JSON.stringify(results[0]);
|
|
95
|
+
return results.slice(1).every(r => JSON.stringify(r) === first);
|
|
96
|
+
}
|
|
184
97
|
|
|
185
|
-
|
|
186
|
-
|
|
98
|
+
function checkNoGlobalMutation() {
|
|
99
|
+
// Placeholder: real impl would compare global state snapshots
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
187
102
|
|
|
188
|
-
|
|
103
|
+
// ----------------------
|
|
104
|
+
// Assertion handler registry.
|
|
105
|
+
// ----------------------
|
|
106
|
+
const assertionHandlers = {
|
|
107
|
+
// R-005: Metadata
|
|
189
108
|
resolver_has_field: (resolverMeta, assertion) =>
|
|
190
109
|
checkResolverHasField(resolverMeta, assertion),
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
110
|
+
resolver_inputs_valid: checkResolverInputsValid,
|
|
111
|
+
resolver_outputs_valid: checkResolverOutputsValid,
|
|
112
|
+
field_names_normalized: checkFieldNamesNormalized,
|
|
113
|
+
resolver_failures_valid: checkResolverFailuresValid,
|
|
114
|
+
|
|
115
|
+
// R-006–R-012: Runtime
|
|
116
|
+
resolver_is_callable: (ctx) => checkResolverIsCallable(ctx.resolver),
|
|
117
|
+
resolver_failure_declared: (ctx) => checkFailureCodeDeclared(ctx.error, ctx.resolverMeta),
|
|
118
|
+
rejects_missing_required_input: (ctx) => checkRejectsMissingRequiredInput(ctx),
|
|
119
|
+
retry_count_within_declared_limit: (ctx) =>
|
|
120
|
+
checkRetryCountWithinLimit(ctx.retryCount, ctx.resolverMeta, ctx.error?.code),
|
|
121
|
+
output_is_object: (ctx) => checkOutputIsObject(ctx.output),
|
|
122
|
+
output_fields_match_contract: (ctx) => checkOutputFieldsMatchContract(ctx.output, ctx.resolverMeta),
|
|
123
|
+
deterministic_output: (ctx) => checkDeterministicOutput(ctx.outputs),
|
|
124
|
+
no_global_state_mutation: () => checkNoGlobalMutation(),
|
|
203
125
|
};
|
|
204
126
|
|
|
205
127
|
// ----------------------
|
|
@@ -241,20 +163,58 @@ function runAssertions(testSpec, target, status = {}) {
|
|
|
241
163
|
message:
|
|
242
164
|
failures.length === 0
|
|
243
165
|
? "All assertions passed"
|
|
244
|
-
: failures
|
|
245
|
-
.map(f => `[${f.severity}] ${f.id}: ${f.message}`)
|
|
246
|
-
.join("; "),
|
|
166
|
+
: failures.map(f => `[${f.severity}] ${f.id}: ${f.message}`).join("; "),
|
|
247
167
|
failures,
|
|
248
168
|
};
|
|
249
169
|
}
|
|
250
170
|
|
|
171
|
+
// ----------------------
|
|
172
|
+
// Runtime resolver invoker with observation
|
|
173
|
+
// ----------------------
|
|
174
|
+
async function invokeResolverWithObservation(resolver, resolverMeta, testSpec) {
|
|
175
|
+
const ctx = {
|
|
176
|
+
resolver,
|
|
177
|
+
resolverMeta,
|
|
178
|
+
output: null,
|
|
179
|
+
outputs: [],
|
|
180
|
+
error: null,
|
|
181
|
+
threw: false,
|
|
182
|
+
retryCount: 0,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Use empty input — resolver must handle its own validation
|
|
186
|
+
const input = {};
|
|
187
|
+
|
|
188
|
+
// Run multiple times for determinism test
|
|
189
|
+
const runs = testSpec.test_id === 'R-011-determinism' ? 3 : 1;
|
|
190
|
+
|
|
191
|
+
for (let i = 0; i < runs; i++) {
|
|
192
|
+
try {
|
|
193
|
+
const result = await Promise.resolve(resolver(input));
|
|
194
|
+
ctx.output = result;
|
|
195
|
+
ctx.outputs.push(result);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
ctx.threw = true;
|
|
198
|
+
ctx.error = err;
|
|
199
|
+
// Simple retry count inference (real impl would use retry loop)
|
|
200
|
+
if (err.code) {
|
|
201
|
+
const decl = resolverMeta.failures?.find(f => f.code === err.code);
|
|
202
|
+
if (decl) ctx.retryCount = decl.retries;
|
|
203
|
+
}
|
|
204
|
+
break; // stop on first error for non-determinism tests
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return ctx;
|
|
209
|
+
}
|
|
210
|
+
|
|
251
211
|
// ----------------------
|
|
252
212
|
// Test suite executor
|
|
253
213
|
// ----------------------
|
|
254
214
|
async function runAllTests({ suites, resolver }) {
|
|
255
215
|
let failed = 0;
|
|
256
|
-
// Resolve test suites from the package root (where R-001-..., R-005-... live)
|
|
257
216
|
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
217
|
+
const resolverMeta = resolver.resolverDeclaration || resolver;
|
|
258
218
|
|
|
259
219
|
for (const suite of suites) {
|
|
260
220
|
const suiteDir = path.join(PACKAGE_ROOT, suite);
|
|
@@ -267,65 +227,52 @@ async function runAllTests({ suites, resolver }) {
|
|
|
267
227
|
}
|
|
268
228
|
|
|
269
229
|
const testSpec = JSON.parse(fs.readFileSync(testSpecPath, "utf8"));
|
|
270
|
-
let target;
|
|
271
|
-
let status = {};
|
|
272
|
-
|
|
273
230
|
const fixture = testSpec.fixtures.inputs[0];
|
|
274
231
|
|
|
275
|
-
if (fixture.
|
|
276
|
-
//
|
|
277
|
-
const
|
|
278
|
-
if (!fs.existsSync(
|
|
279
|
-
console.error(`❌
|
|
232
|
+
if (fixture.resolver_contract) {
|
|
233
|
+
// R-005: Validate metadata structure
|
|
234
|
+
const contractPath = path.join(suiteDir, fixture.resolver_contract);
|
|
235
|
+
if (!fs.existsSync(contractPath)) {
|
|
236
|
+
console.error(`❌ Resolver contract missing: ${contractPath}`);
|
|
280
237
|
failed++;
|
|
281
238
|
continue;
|
|
282
239
|
}
|
|
283
240
|
|
|
284
|
-
|
|
241
|
+
let target;
|
|
285
242
|
try {
|
|
286
|
-
|
|
287
|
-
? resolver.parse(workflowSource)
|
|
288
|
-
: { ast: resolver };
|
|
289
|
-
|
|
290
|
-
target = parseResult.ast;
|
|
291
|
-
status = { __warnings: parseResult.__warnings || [] };
|
|
243
|
+
target = require(contractPath);
|
|
292
244
|
} catch (err) {
|
|
293
|
-
console.error(`❌
|
|
245
|
+
console.error(`❌ Failed to load resolver contract ${suite}:`, err.message);
|
|
294
246
|
failed++;
|
|
295
247
|
continue;
|
|
296
248
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
console.error(`❌ Resolver contract missing: ${contractPath}`);
|
|
249
|
+
|
|
250
|
+
const result = runAssertions(testSpec, target);
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
console.error(`❌ ${suite} failed: ${result.message}`);
|
|
302
253
|
failed++;
|
|
303
|
-
|
|
254
|
+
} else {
|
|
255
|
+
console.log(`✅ ${suite} passed`);
|
|
304
256
|
}
|
|
305
|
-
|
|
257
|
+
} else if (testSpec.category === "resolver-runtime") {
|
|
258
|
+
// R-006 → R-012: Observe real resolver behavior
|
|
306
259
|
try {
|
|
307
|
-
|
|
260
|
+
const runtimeContext = await invokeResolverWithObservation(resolver, resolverMeta, testSpec);
|
|
261
|
+
const result = runAssertions(testSpec, runtimeContext);
|
|
262
|
+
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
console.error(`❌ ${suite} failed: ${result.message}`);
|
|
265
|
+
failed++;
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`✅ ${suite} passed`);
|
|
268
|
+
}
|
|
308
269
|
} catch (err) {
|
|
309
|
-
console.error(
|
|
310
|
-
`❌ Failed to load resolver contract ${suite}:`,
|
|
311
|
-
err.message
|
|
312
|
-
);
|
|
270
|
+
console.error(`🔥 Runtime test ${suite} crashed:`, err.message);
|
|
313
271
|
failed++;
|
|
314
|
-
continue;
|
|
315
272
|
}
|
|
316
273
|
} else {
|
|
317
274
|
console.error(`❌ Unrecognized fixture in ${suite}`);
|
|
318
275
|
failed++;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const result = runAssertions(testSpec, target, status);
|
|
323
|
-
|
|
324
|
-
if (!result.ok) {
|
|
325
|
-
console.error(`❌ ${suite} failed: ${result.message}`);
|
|
326
|
-
failed++;
|
|
327
|
-
} else {
|
|
328
|
-
console.log(`✅ ${suite} passed`);
|
|
329
276
|
}
|
|
330
277
|
}
|
|
331
278
|
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -72,10 +72,14 @@ const { generateBadge } = require("./lib/badge");
|
|
|
72
72
|
opts.suites.length > 0
|
|
73
73
|
? opts.suites
|
|
74
74
|
: [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
"R-005-resolver-metadata-contract",
|
|
76
|
+
"R-006-runtime-shape",
|
|
77
|
+
"R-007-failure-contract",
|
|
78
|
+
"R-008-input-validation",
|
|
79
|
+
"R-009-retry-semantics",
|
|
80
|
+
"R-010-output-contract",
|
|
81
|
+
"R-011-determinism",
|
|
82
|
+
"R-012-side-effects"
|
|
79
83
|
];
|
|
80
84
|
|
|
81
85
|
const result = await runAllTests({
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# R-001 Allowlist Resolver Test
|
|
2
|
-
|
|
3
|
-
**Purpose:**
|
|
4
|
-
This test verifies that the O-Lang parser correctly captures and normalizes the list of allowed resolvers declared in a workflow.
|
|
5
|
-
|
|
6
|
-
**Workflow:**
|
|
7
|
-
- Workflow Name: `TestResolverAllowlist`
|
|
8
|
-
- Allowed Resolvers:
|
|
9
|
-
- `SimpleResolver`
|
|
10
|
-
- `AdvancedResolver`
|
|
11
|
-
|
|
12
|
-
**Assertions:**
|
|
13
|
-
1. All allowed resolvers are captured correctly.
|
|
14
|
-
2. Resolver names are normalized (no spaces, invalid characters).
|
|
15
|
-
3. Workflow name is correctly parsed.
|
|
16
|
-
4. No parser warnings are generated for valid syntax.
|
|
17
|
-
|
|
18
|
-
**Spec References:**
|
|
19
|
-
- §4.2 Allowed Resolvers
|
|
20
|
-
- §3.1 Workflow Parsing
|
|
21
|
-
- §2.3 Symbol Normalization
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"test_id": "R-001-allowlist",
|
|
3
|
-
"protocol_version": "1.1",
|
|
4
|
-
"category": "resolver",
|
|
5
|
-
"description": "Tests that the parser correctly captures and normalizes allowed resolvers.",
|
|
6
|
-
"spec_ref": [
|
|
7
|
-
"§4.2 Allowed Resolvers",
|
|
8
|
-
"§3.1 Workflow Parsing",
|
|
9
|
-
"§2.3 Symbol Normalization"
|
|
10
|
-
],
|
|
11
|
-
"fixtures": {
|
|
12
|
-
"inputs": [
|
|
13
|
-
{
|
|
14
|
-
"workflow": "workflow.ol",
|
|
15
|
-
"values": {}
|
|
16
|
-
}
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
"assertions": [
|
|
20
|
-
{
|
|
21
|
-
"id": "allowed_resolvers_listed",
|
|
22
|
-
"type": "allowed_resolvers_listed",
|
|
23
|
-
"expected": ["SimpleResolver", "AdvancedResolver"],
|
|
24
|
-
"severity": "fatal",
|
|
25
|
-
"description": "Parser must capture all allowed resolvers"
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"id": "resolver_names_normalized",
|
|
29
|
-
"type": "resolver_names_normalized",
|
|
30
|
-
"severity": "fatal",
|
|
31
|
-
"description": "All resolver names must conform to symbol normalization rules (start with letter, alphanumeric only)"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"id": "workflow_name_present",
|
|
35
|
-
"type": "workflow_name_present",
|
|
36
|
-
"expected": "TestResolverAllowlist",
|
|
37
|
-
"severity": "fatal",
|
|
38
|
-
"description": "Workflow name must be parsed correctly"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "workflow_return_values_empty",
|
|
42
|
-
"type": "workflow_return_values_empty",
|
|
43
|
-
"expected": [],
|
|
44
|
-
"severity": "fatal",
|
|
45
|
-
"description": "Return values must be empty as per workflow"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"id": "no_parse_warnings",
|
|
49
|
-
"type": "no_parse_warnings",
|
|
50
|
-
"expected": 0,
|
|
51
|
-
"severity": "warning",
|
|
52
|
-
"description": "No warnings should be issued for valid syntax"
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# R-002 IO Contract Resolver Test
|
|
2
|
-
|
|
3
|
-
**Purpose:**
|
|
4
|
-
This test validates that resolver input/output contracts are correctly recognized, including the proper handling of `Ask` and `Use` steps, and that `saveAs` values are normalized.
|
|
5
|
-
|
|
6
|
-
**Workflow:**
|
|
7
|
-
- Workflow Name: `TestResolverIOContract`
|
|
8
|
-
- Parameters: `userInput`
|
|
9
|
-
- Allowed Resolvers:
|
|
10
|
-
- `Calculator`
|
|
11
|
-
- `WeatherService`
|
|
12
|
-
- Steps:
|
|
13
|
-
1. `Ask Calculator` → `saveAs: calcResult`
|
|
14
|
-
2. `Use WeatherService` → `saveAs: weatherInfo`
|
|
15
|
-
- Return Values: `calcResult, weatherInfo`
|
|
16
|
-
|
|
17
|
-
**Assertions:**
|
|
18
|
-
1. Step 1 is parsed as an `ask` step with normalized `saveAs`.
|
|
19
|
-
2. Step 2 is parsed as a `use` step with normalized `saveAs`.
|
|
20
|
-
3. Return statement lists all resolver outputs.
|
|
21
|
-
4. No parser warnings are generated.
|
|
22
|
-
|
|
23
|
-
**Spec References:**
|
|
24
|
-
- §4.3 Resolver Input/Output Contracts
|
|
25
|
-
- §3.2 Step Parsing
|
|
26
|
-
- §2.3 Symbol Normalization
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"test_id": "R-002-io-contract",
|
|
3
|
-
"protocol_version": "1.1",
|
|
4
|
-
"category": "resolver",
|
|
5
|
-
"description": "Tests that resolver I/O contracts are correctly recognized and saveAs fields are applied.",
|
|
6
|
-
"spec_ref": [
|
|
7
|
-
"§4.3 Resolver Input/Output Contracts",
|
|
8
|
-
"§3.2 Step Parsing",
|
|
9
|
-
"§2.3 Symbol Normalization"
|
|
10
|
-
],
|
|
11
|
-
"fixtures": {
|
|
12
|
-
"inputs": [
|
|
13
|
-
{
|
|
14
|
-
"workflow": "workflow.ol",
|
|
15
|
-
"values": {
|
|
16
|
-
"userInput": "42"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"assertions": [
|
|
22
|
-
{
|
|
23
|
-
"id": "allowed_resolvers_listed",
|
|
24
|
-
"type": "allowed_resolvers_listed",
|
|
25
|
-
"expected": ["Calculator", "WeatherService"],
|
|
26
|
-
"severity": "fatal",
|
|
27
|
-
"description": "Parser must capture all allowed resolvers"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"id": "ask_step_type",
|
|
31
|
-
"type": "step_type",
|
|
32
|
-
"stepIndex": 0,
|
|
33
|
-
"expected": "ask",
|
|
34
|
-
"severity": "fatal",
|
|
35
|
-
"description": "Step 1 must be parsed as 'ask'"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"id": "ask_step_saveas",
|
|
39
|
-
"type": "step_saveas",
|
|
40
|
-
"stepIndex": 0,
|
|
41
|
-
"expected": "calcResult",
|
|
42
|
-
"severity": "fatal",
|
|
43
|
-
"description": "Step 1 saveAs must be 'calcResult'"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"id": "use_step_type",
|
|
47
|
-
"type": "step_type",
|
|
48
|
-
"stepIndex": 1,
|
|
49
|
-
"expected": "use",
|
|
50
|
-
"severity": "fatal",
|
|
51
|
-
"description": "Step 2 must be parsed as 'use'"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"id": "use_step_saveas",
|
|
55
|
-
"type": "step_saveas",
|
|
56
|
-
"stepIndex": 1,
|
|
57
|
-
"expected": "weatherInfo",
|
|
58
|
-
"severity": "fatal",
|
|
59
|
-
"description": "Step 2 saveAs must be 'weatherInfo'"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"id": "workflow_return_values",
|
|
63
|
-
"type": "workflow_return_values",
|
|
64
|
-
"expected": ["calcResult", "weatherInfo"],
|
|
65
|
-
"severity": "fatal",
|
|
66
|
-
"description": "Return statement must list all saved outputs"
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"id": "no_parse_warnings",
|
|
70
|
-
"type": "no_parse_warnings",
|
|
71
|
-
"expected": 0,
|
|
72
|
-
"severity": "warning",
|
|
73
|
-
"description": "No warnings for valid I/O contracts"
|
|
74
|
-
}
|
|
75
|
-
]
|
|
76
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# R-003 Failure Modes Resolver Test
|
|
2
|
-
|
|
3
|
-
**Purpose:**
|
|
4
|
-
This test validates that resolver failures, missing resolvers, and runtime errors are correctly handled and surfaced by the parser/runtime.
|
|
5
|
-
|
|
6
|
-
**Workflow:**
|
|
7
|
-
- Workflow Name: `TestResolverFailureModes`
|
|
8
|
-
- Parameters: `userInput`
|
|
9
|
-
- Allowed Resolvers:
|
|
10
|
-
- `ReliableResolver`
|
|
11
|
-
- `UnstableResolver`
|
|
12
|
-
- Steps:
|
|
13
|
-
1. `Ask ReliableResolver` → `saveAs: reliableResult`
|
|
14
|
-
2. `Ask UnstableResolver` → `saveAs: unstableResult`
|
|
15
|
-
- Return Values: `reliableResult, unstableResult`
|
|
16
|
-
|
|
17
|
-
**Assertions:**
|
|
18
|
-
1. Steps 1 & 2 are correctly parsed as `ask` resolver steps.
|
|
19
|
-
2. `saveAs` values are normalized.
|
|
20
|
-
3. Return statement lists all resolver outputs.
|
|
21
|
-
4. Errors from failing resolvers (e.g., `UnstableResolver`) are surfaced.
|
|
22
|
-
5. Missing resolvers (e.g., `MissingResolver`) trigger proper errors.
|
|
23
|
-
|
|
24
|
-
**Spec References:**
|
|
25
|
-
- §4.4 Resolver Failure Modes
|
|
26
|
-
- §3.2 Step Parsing
|
|
27
|
-
- §5.1 Error Handling
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"test_id": "R-003-failure-modes",
|
|
3
|
-
"protocol_version": "1.1",
|
|
4
|
-
"category": "resolver",
|
|
5
|
-
"description": "Tests that resolver failure modes and retry policies are parsed and exposed.",
|
|
6
|
-
"spec_ref": [
|
|
7
|
-
"§4.4 Resolver Failure Modes",
|
|
8
|
-
"§3.3 Step Parsing",
|
|
9
|
-
"§2.3 Symbol Normalization"
|
|
10
|
-
],
|
|
11
|
-
"fixtures": {
|
|
12
|
-
"inputs": [
|
|
13
|
-
{
|
|
14
|
-
"workflow": "workflow.ol",
|
|
15
|
-
"values": {
|
|
16
|
-
"userInput": "someInput"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"assertions": [
|
|
22
|
-
{
|
|
23
|
-
"id": "allowed_resolvers_listed",
|
|
24
|
-
"type": "allowed_resolvers_listed",
|
|
25
|
-
"expected": ["ReliableResolver", "UnstableResolver"],
|
|
26
|
-
"severity": "fatal",
|
|
27
|
-
"description": "Allowed resolvers must be captured"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"id": "step_0_saveas",
|
|
31
|
-
"type": "step_saveas",
|
|
32
|
-
"stepIndex": 0,
|
|
33
|
-
"expected": "reliableResult",
|
|
34
|
-
"severity": "fatal",
|
|
35
|
-
"description": "Step 0 saveAs"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"id": "step_1_saveas",
|
|
39
|
-
"type": "step_saveas",
|
|
40
|
-
"stepIndex": 1,
|
|
41
|
-
"expected": "unstableResult",
|
|
42
|
-
"severity": "fatal",
|
|
43
|
-
"description": "Step 1 saveAs"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"id": "workflow_name_present",
|
|
47
|
-
"type": "workflow_name_present",
|
|
48
|
-
"expected": "TestResolverFailureModes",
|
|
49
|
-
"severity": "fatal",
|
|
50
|
-
"description": "Workflow name parsed"
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
"id": "workflow_return_values",
|
|
54
|
-
"type": "workflow_return_values",
|
|
55
|
-
"expected": ["reliableResult", "unstableResult"],
|
|
56
|
-
"severity": "fatal",
|
|
57
|
-
"description": "Return values listed"
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"id": "unstable_resolver_failure_policies",
|
|
61
|
-
"type": "step_failure_policies",
|
|
62
|
-
"stepIndex": 1,
|
|
63
|
-
"expected": {
|
|
64
|
-
"TIMEOUT": { "action": "retry", "count": 2 },
|
|
65
|
-
"NETWORK_ERROR": { "action": "fail", "count": 0 }
|
|
66
|
-
},
|
|
67
|
-
"severity": "fatal",
|
|
68
|
-
"description": "Failure modes and retry counts must be parsed"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"id": "no_parse_warnings",
|
|
72
|
-
"type": "no_parse_warnings",
|
|
73
|
-
"expected": 0,
|
|
74
|
-
"severity": "warning",
|
|
75
|
-
"description": "No warnings for valid failure mode syntax"
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Workflow "TestResolverFailureModes" with userInput
|
|
2
|
-
|
|
3
|
-
Allow resolvers:
|
|
4
|
-
- ReliableResolver
|
|
5
|
-
- UnstableResolver
|
|
6
|
-
|
|
7
|
-
Step 1: Ask ReliableResolver
|
|
8
|
-
Save as reliableResult
|
|
9
|
-
|
|
10
|
-
Step 2: Ask UnstableResolver
|
|
11
|
-
Save as unstableResult
|
|
12
|
-
|
|
13
|
-
Return reliableResult, unstableResult
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# R-005: Resolver Metadata Contract
|
|
2
|
-
|
|
3
|
-
Validates that resolver packages declare valid **inputs**, **outputs**, **resolverName**, and **failure modes**.
|
|
4
|
-
|
|
5
|
-
This test ensures that static resolver contracts (e.g., `resolver.js`) are well-formed and align with O-lang’s governance model—enabling tooling, linting, and safe composition.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"test_id": "R-004-invalid-syntax",
|
|
3
|
-
"protocol_version": "1.1",
|
|
4
|
-
"category": "resolver",
|
|
5
|
-
"description": "Tests that parser emits warnings or errors for invalid syntax.",
|
|
6
|
-
"spec_ref": [
|
|
7
|
-
"§2.3 Symbol Normalization",
|
|
8
|
-
"§3.1 Workflow Parsing",
|
|
9
|
-
"§4.2 Allowed Resolvers"
|
|
10
|
-
],
|
|
11
|
-
"fixtures": {
|
|
12
|
-
"inputs": [
|
|
13
|
-
{
|
|
14
|
-
"workflow": "workflow.ol",
|
|
15
|
-
"values": {}
|
|
16
|
-
}
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
"assertions": [
|
|
20
|
-
{
|
|
21
|
-
"id": "warns_on_invalid_resolver_name",
|
|
22
|
-
"type": "contains_warning",
|
|
23
|
-
"expected_substring": "invalid resolver name",
|
|
24
|
-
"severity": "fatal",
|
|
25
|
-
"description": "Must warn about resolver name with space"
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"id": "warns_on_invalid_saveas",
|
|
29
|
-
"type": "contains_warning",
|
|
30
|
-
"expected_substring": "invalid identifier",
|
|
31
|
-
"severity": "fatal",
|
|
32
|
-
"description": "Must warn about malformed Save as"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"id": "warns_on_unrecognized_step",
|
|
36
|
-
"type": "contains_warning",
|
|
37
|
-
"expected_substring": "unrecognized step",
|
|
38
|
-
"severity": "fatal",
|
|
39
|
-
"description": "Must warn about unknown step type"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"id": "warns_on_duplicate_saveas",
|
|
43
|
-
"type": "contains_warning",
|
|
44
|
-
"expected_substring": "duplicate saveAs",
|
|
45
|
-
"severity": "fatal",
|
|
46
|
-
"description": "Must warn about duplicate Save as names"
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"id": "parse_status_has_warnings",
|
|
50
|
-
"type": "status_greater_than",
|
|
51
|
-
"path": "__warnings.length",
|
|
52
|
-
"expected": 0,
|
|
53
|
-
"severity": "fatal",
|
|
54
|
-
"description": "Parser must emit at least one warning"
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// resolver.ol (treated as a JS module in test)
|
|
2
|
-
module.exports = {
|
|
3
|
-
resolverName: "RiskAssessment",
|
|
4
|
-
inputs: [
|
|
5
|
-
{ name: "transaction_id", type: "string", required: true },
|
|
6
|
-
{ name: "user_id", type: "string", required: true }
|
|
7
|
-
],
|
|
8
|
-
outputs: [
|
|
9
|
-
{ name: "risk_score", type: "number" },
|
|
10
|
-
{ name: "confidence", type: "number" }
|
|
11
|
-
],
|
|
12
|
-
failures: [
|
|
13
|
-
{ code: "DATA_UNAVAILABLE", retries: 1 },
|
|
14
|
-
{ code: "MODEL_ERROR", retries: 0 }
|
|
15
|
-
]
|
|
16
|
-
};
|
package/certified.svg
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="28">
|
|
2
|
-
<rect rx="4" width="160" height="28" fill="#2d2d2d"/>
|
|
3
|
-
<rect rx="4" x="80" width="80" height="28" fill="#4cbb17"/>
|
|
4
|
-
<text x="40" y="18" fill="#fff" font-size="13" font-family="Arial" text-anchor="middle">O-lang</text>
|
|
5
|
-
<text x="120" y="18" fill="#fff" font-size="13" font-family="Arial" text-anchor="middle">CERTIFIED</text>
|
|
6
|
-
</svg>
|