@o-lang/resolver-tests 1.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/.github/workflows/certify.yml +88 -0
- package/R-001-allowlist/README.md +21 -0
- package/R-001-allowlist/test.json +55 -0
- package/R-001-allowlist/workflow.ol +7 -0
- package/R-002-io-contract/README.md +26 -0
- package/R-002-io-contract/test.json +76 -0
- package/R-002-io-contract/workflow.ol +13 -0
- package/R-003-failure-modes/README +27 -0
- package/R-003-failure-modes/test.json +84 -0
- package/R-003-failure-modes/workflow.ol +13 -0
- package/R-004-invalid-syntax/README.md +5 -0
- package/R-004-invalid-syntax/test.json +57 -0
- package/R-004-invalid-syntax/workflow.ol +16 -0
- package/R-005-resolver-metadata-contract/README.md +5 -0
- package/R-005-resolver-metadata-contract/resolver.js +16 -0
- package/R-005-resolver-metadata-contract/test.json +60 -0
- package/README.md +46 -0
- package/badges/certified.svg +18 -0
- package/lib/O--Lang v1-Conformant-success.svg +1 -0
- package/lib/badge.js +27 -0
- package/lib/runner.js +337 -0
- package/package.json +29 -0
- package/reference-resolver/index.js +10 -0
- package/run-kernel.js +122 -0
- package/run.js +134 -0
- package/schema/conformance.schema.json +31 -0
package/lib/badge.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function generateBadge({ passed, outputDir }) {
|
|
5
|
+
const badgePath = path.join(outputDir, "certified.svg");
|
|
6
|
+
|
|
7
|
+
const svg = passed
|
|
8
|
+
? `
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="28">
|
|
10
|
+
<rect rx="4" width="160" height="28" fill="#2d2d2d"/>
|
|
11
|
+
<rect rx="4" x="80" width="80" height="28" fill="#4cbb17"/>
|
|
12
|
+
<text x="40" y="18" fill="#fff" font-size="13" font-family="Arial" text-anchor="middle">O-lang</text>
|
|
13
|
+
<text x="120" y="18" fill="#fff" font-size="13" font-family="Arial" text-anchor="middle">CERTIFIED</text>
|
|
14
|
+
</svg>`
|
|
15
|
+
: `
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="28">
|
|
17
|
+
<rect rx="4" width="160" height="28" fill="#2d2d2d"/>
|
|
18
|
+
<rect rx="4" x="80" width="80" height="28" fill="#bb2124"/>
|
|
19
|
+
<text x="40" y="18" fill="#fff" font-size="13" font-family="Arial" text-anchor="middle">O-lang</text>
|
|
20
|
+
<text x="120" y="18" fill="#fff" font-size="13" font-family="Arial" text-anchor="middle">FAILED</text>
|
|
21
|
+
</svg>`;
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(badgePath, svg.trim());
|
|
24
|
+
return badgePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { generateBadge };
|
package/lib/runner.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// lib/runner.js
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
// ----------------------
|
|
6
|
+
// Helper: Deep get by path (e.g., "steps[0].saveAs")
|
|
7
|
+
// ----------------------
|
|
8
|
+
function getNestedValue(obj, path) {
|
|
9
|
+
if (!path) return obj;
|
|
10
|
+
return path
|
|
11
|
+
.split(/\.(?![^\[]*\])|[\[\]]/)
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.reduce((cur, prop) => cur?.[prop], obj);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ----------------------
|
|
17
|
+
// Validator functions for WORKFLOW AST
|
|
18
|
+
// ----------------------
|
|
19
|
+
function checkAllowlist(ast, expected) {
|
|
20
|
+
const allowed = ast.allowedResolvers || [];
|
|
21
|
+
return (
|
|
22
|
+
Array.isArray(allowed) &&
|
|
23
|
+
allowed.length === expected.length &&
|
|
24
|
+
expected.every(r => allowed.includes(r))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function checkResolverNameNormalization(ast) {
|
|
29
|
+
const allowed = ast.allowedResolvers || [];
|
|
30
|
+
const pattern = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
31
|
+
return Array.isArray(allowed) && allowed.every(name => pattern.test(name));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkWorkflowName(ast, expected) {
|
|
35
|
+
return ast.name === expected;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function checkReturnValues(ast, expected) {
|
|
39
|
+
const returns = ast.returnValues || [];
|
|
40
|
+
return (
|
|
41
|
+
Array.isArray(returns) &&
|
|
42
|
+
returns.length === expected.length &&
|
|
43
|
+
expected.every(v => returns.includes(v))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkNoWarnings(status, expectedCount = 0) {
|
|
48
|
+
const warnings = status?.__warnings || [];
|
|
49
|
+
return warnings.length === expectedCount;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkStepType(ast, assertion) {
|
|
53
|
+
const steps = ast.steps || [];
|
|
54
|
+
const step = steps[assertion.stepIndex];
|
|
55
|
+
return step && step.type === assertion.expected;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkStepSaveAs(ast, assertion) {
|
|
59
|
+
const steps = ast.steps || [];
|
|
60
|
+
const step = steps[assertion.stepIndex];
|
|
61
|
+
return step && step.saveAs === assertion.expected;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkStepFailurePolicies(ast, assertion) {
|
|
65
|
+
const steps = ast.steps || [];
|
|
66
|
+
const step = steps[assertion.stepIndex];
|
|
67
|
+
if (!step || !step.failurePolicies) return false;
|
|
68
|
+
|
|
69
|
+
const expected = assertion.expected;
|
|
70
|
+
return Object.keys(expected).every(code => {
|
|
71
|
+
const policy = step.failurePolicies[code];
|
|
72
|
+
return (
|
|
73
|
+
policy &&
|
|
74
|
+
policy.action === expected[code].action &&
|
|
75
|
+
policy.count === expected[code].count
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function checkContainsWarning(status, assertion) {
|
|
81
|
+
const warnings = status?.__warnings || [];
|
|
82
|
+
const needle = assertion.expected_substring.toLowerCase();
|
|
83
|
+
return warnings.some(w =>
|
|
84
|
+
(typeof w === "string" ? w : w.message || "")
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.includes(needle)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkStatusGreaterThan(status, assertion) {
|
|
91
|
+
const value = getNestedValue(status, assertion.path);
|
|
92
|
+
return typeof value === "number" && value > assertion.expected;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ----------------------
|
|
96
|
+
// Validator functions for RESOLVER METADATA (R-005)
|
|
97
|
+
// ----------------------
|
|
98
|
+
function checkResolverHasField(resolverMeta, assertion) {
|
|
99
|
+
return resolverMeta[assertion.field] === assertion.expected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkResolverInputsValid(resolverMeta) {
|
|
103
|
+
const inputs = resolverMeta.inputs;
|
|
104
|
+
return (
|
|
105
|
+
Array.isArray(inputs) &&
|
|
106
|
+
inputs.every(
|
|
107
|
+
i =>
|
|
108
|
+
i &&
|
|
109
|
+
typeof i.name === "string" &&
|
|
110
|
+
typeof i.type === "string" &&
|
|
111
|
+
typeof i.required === "boolean"
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checkResolverOutputsValid(resolverMeta) {
|
|
117
|
+
const outputs = resolverMeta.outputs;
|
|
118
|
+
return (
|
|
119
|
+
Array.isArray(outputs) &&
|
|
120
|
+
outputs.every(
|
|
121
|
+
o =>
|
|
122
|
+
o &&
|
|
123
|
+
typeof o.name === "string" &&
|
|
124
|
+
typeof o.type === "string"
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkFieldNamesNormalized(resolverMeta, assertion) {
|
|
130
|
+
const items = resolverMeta[assertion.field] || [];
|
|
131
|
+
const pattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
132
|
+
return Array.isArray(items) && items.every(item => pattern.test(item.name));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkResolverFailuresValid(resolverMeta) {
|
|
136
|
+
const failures = resolverMeta.failures;
|
|
137
|
+
if (!failures) return false;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
Array.isArray(failures) &&
|
|
141
|
+
failures.every(
|
|
142
|
+
f =>
|
|
143
|
+
f &&
|
|
144
|
+
typeof f.code === "string" &&
|
|
145
|
+
typeof f.retries === "number"
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ----------------------
|
|
151
|
+
// Assertion handler registry
|
|
152
|
+
// ----------------------
|
|
153
|
+
const assertionHandlers = {
|
|
154
|
+
// Workflow AST assertions
|
|
155
|
+
allowed_resolvers_listed: (ast, assertion) =>
|
|
156
|
+
checkAllowlist(ast, assertion.expected),
|
|
157
|
+
|
|
158
|
+
resolver_names_normalized: ast =>
|
|
159
|
+
checkResolverNameNormalization(ast),
|
|
160
|
+
|
|
161
|
+
workflow_name_present: (ast, assertion) =>
|
|
162
|
+
checkWorkflowName(ast, assertion.expected),
|
|
163
|
+
|
|
164
|
+
workflow_return_values: (ast, assertion) =>
|
|
165
|
+
checkReturnValues(ast, assertion.expected),
|
|
166
|
+
|
|
167
|
+
workflow_return_values_empty: (ast, assertion) =>
|
|
168
|
+
checkReturnValues(ast, assertion.expected),
|
|
169
|
+
|
|
170
|
+
no_parse_warnings: (ast, assertion, status) =>
|
|
171
|
+
checkNoWarnings(status, assertion.expected),
|
|
172
|
+
|
|
173
|
+
step_type: (ast, assertion) =>
|
|
174
|
+
checkStepType(ast, assertion),
|
|
175
|
+
|
|
176
|
+
step_saveas: (ast, assertion) =>
|
|
177
|
+
checkStepSaveAs(ast, assertion),
|
|
178
|
+
|
|
179
|
+
step_failure_policies: (ast, assertion) =>
|
|
180
|
+
checkStepFailurePolicies(ast, assertion),
|
|
181
|
+
|
|
182
|
+
// Negative test assertions (R-004)
|
|
183
|
+
contains_warning: (ast, assertion, status) =>
|
|
184
|
+
checkContainsWarning(status, assertion),
|
|
185
|
+
|
|
186
|
+
status_greater_than: (ast, assertion, status) =>
|
|
187
|
+
checkStatusGreaterThan(status, assertion),
|
|
188
|
+
|
|
189
|
+
// Resolver metadata assertions (R-005)
|
|
190
|
+
resolver_has_field: (resolverMeta, assertion) =>
|
|
191
|
+
checkResolverHasField(resolverMeta, assertion),
|
|
192
|
+
|
|
193
|
+
resolver_inputs_valid: resolverMeta =>
|
|
194
|
+
checkResolverInputsValid(resolverMeta),
|
|
195
|
+
|
|
196
|
+
resolver_outputs_valid: resolverMeta =>
|
|
197
|
+
checkResolverOutputsValid(resolverMeta),
|
|
198
|
+
|
|
199
|
+
field_names_normalized: (resolverMeta, assertion) =>
|
|
200
|
+
checkFieldNamesNormalized(resolverMeta, assertion),
|
|
201
|
+
|
|
202
|
+
resolver_failures_valid: resolverMeta =>
|
|
203
|
+
checkResolverFailuresValid(resolverMeta),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ----------------------
|
|
207
|
+
// Main assertion runner
|
|
208
|
+
// ----------------------
|
|
209
|
+
function runAssertions(testSpec, target, status = {}) {
|
|
210
|
+
if (!testSpec.assertions?.length) {
|
|
211
|
+
return { ok: true, message: "No assertions defined" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const failures = [];
|
|
215
|
+
|
|
216
|
+
for (const assertion of testSpec.assertions) {
|
|
217
|
+
const { id, type, severity = "fatal", description } = assertion;
|
|
218
|
+
let passed = false;
|
|
219
|
+
|
|
220
|
+
if (type in assertionHandlers) {
|
|
221
|
+
passed = assertionHandlers[type](target, assertion, status);
|
|
222
|
+
} else {
|
|
223
|
+
failures.push({
|
|
224
|
+
id,
|
|
225
|
+
severity,
|
|
226
|
+
message: `Unknown assertion type: ${type}`,
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!passed) {
|
|
232
|
+
failures.push({
|
|
233
|
+
id,
|
|
234
|
+
severity,
|
|
235
|
+
message: description || `Assertion failed: ${id}`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
ok: failures.length === 0,
|
|
242
|
+
message:
|
|
243
|
+
failures.length === 0
|
|
244
|
+
? "All assertions passed"
|
|
245
|
+
: failures
|
|
246
|
+
.map(f => `[${f.severity}] ${f.id}: ${f.message}`)
|
|
247
|
+
.join("; "),
|
|
248
|
+
failures,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ----------------------
|
|
253
|
+
// Test suite executor
|
|
254
|
+
// ----------------------
|
|
255
|
+
async function runAllTests({ suites, resolver }) {
|
|
256
|
+
let failed = 0;
|
|
257
|
+
|
|
258
|
+
for (const suite of suites) {
|
|
259
|
+
const suiteDir = path.join(process.cwd(), suite);
|
|
260
|
+
const testSpecPath = path.join(suiteDir, "test.json");
|
|
261
|
+
|
|
262
|
+
if (!fs.existsSync(testSpecPath)) {
|
|
263
|
+
console.error(`❌ Test spec not found: ${testSpecPath}`);
|
|
264
|
+
failed++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const testSpec = JSON.parse(fs.readFileSync(testSpecPath, "utf8"));
|
|
269
|
+
let target;
|
|
270
|
+
let status = {};
|
|
271
|
+
|
|
272
|
+
const fixture = testSpec.fixtures.inputs[0];
|
|
273
|
+
|
|
274
|
+
if (fixture.workflow) {
|
|
275
|
+
// Workflow parsing test (R-001 → R-004)
|
|
276
|
+
const workflowPath = path.join(suiteDir, fixture.workflow);
|
|
277
|
+
if (!fs.existsSync(workflowPath)) {
|
|
278
|
+
console.error(`❌ Workflow file missing: ${workflowPath}`);
|
|
279
|
+
failed++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const workflowSource = fs.readFileSync(workflowPath, "utf8");
|
|
284
|
+
try {
|
|
285
|
+
const parseResult = resolver.parse
|
|
286
|
+
? resolver.parse(workflowSource)
|
|
287
|
+
: { ast: resolver };
|
|
288
|
+
|
|
289
|
+
target = parseResult.ast;
|
|
290
|
+
status = { __warnings: parseResult.__warnings || [] };
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(`❌ Parse failed for ${suite}:`, err.message);
|
|
293
|
+
failed++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
} else if (fixture.resolver_contract) {
|
|
297
|
+
// Resolver metadata test (R-005)
|
|
298
|
+
const contractPath = path.join(suiteDir, fixture.resolver_contract);
|
|
299
|
+
if (!fs.existsSync(contractPath)) {
|
|
300
|
+
console.error(`❌ Resolver contract missing: ${contractPath}`);
|
|
301
|
+
failed++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
target = require(contractPath);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(
|
|
309
|
+
`❌ Failed to load resolver contract ${suite}:`,
|
|
310
|
+
err.message
|
|
311
|
+
);
|
|
312
|
+
failed++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
console.error(`❌ Unrecognized fixture in ${suite}`);
|
|
317
|
+
failed++;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = runAssertions(testSpec, target, status);
|
|
322
|
+
|
|
323
|
+
if (!result.ok) {
|
|
324
|
+
console.error(`❌ ${suite} failed: ${result.message}`);
|
|
325
|
+
failed++;
|
|
326
|
+
} else {
|
|
327
|
+
console.log(`✅ ${suite} passed`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { failed };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
runAssertions,
|
|
336
|
+
runAllTests,
|
|
337
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@o-lang/resolver-tests",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official O-Lang Resolver Test Harness — locked single entrypoint",
|
|
5
|
+
"main": "run.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node run.js",
|
|
9
|
+
"test:kernel": "node run-kernel.js",
|
|
10
|
+
"test:kernel:json": "node run-kernel.js --json",
|
|
11
|
+
"test:kernel:badge": "node run-kernel.js --badge",
|
|
12
|
+
"test:kernel:all": "node run-kernel.js --json --badge"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"olang-resolver-test": "run.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"olang",
|
|
19
|
+
"resolver",
|
|
20
|
+
"test",
|
|
21
|
+
"harness",
|
|
22
|
+
"locked-entrypoint"
|
|
23
|
+
],
|
|
24
|
+
"author": "O-Lang Team",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"fs-extra": "^11.1.1"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/run-kernel.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const { writeFileSync } = require("fs");
|
|
6
|
+
|
|
7
|
+
// ----------------------
|
|
8
|
+
// Load kernel from npm
|
|
9
|
+
// ----------------------
|
|
10
|
+
let kernel;
|
|
11
|
+
try {
|
|
12
|
+
kernel = require("@o-lang/olang"); // Published kernel
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error("❌ Failed to load @o-lang/olang kernel from node_modules");
|
|
15
|
+
console.error(err);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ----------------------
|
|
20
|
+
// CLI arg parsing
|
|
21
|
+
// ----------------------
|
|
22
|
+
function parseArgs() {
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const opts = {
|
|
25
|
+
suites: [],
|
|
26
|
+
json: false,
|
|
27
|
+
badge: false
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
if (args[i] === "--suite" && args[i + 1]) {
|
|
32
|
+
opts.suites.push(args[++i]);
|
|
33
|
+
}
|
|
34
|
+
if (args[i] === "--json") {
|
|
35
|
+
opts.json = true;
|
|
36
|
+
}
|
|
37
|
+
if (args[i] === "--badge") {
|
|
38
|
+
opts.badge = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return opts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ----------------------
|
|
46
|
+
// Import resolver-test runner
|
|
47
|
+
// ----------------------
|
|
48
|
+
const { runAllTests } = require("./lib/runner");
|
|
49
|
+
|
|
50
|
+
// ----------------------
|
|
51
|
+
// Badge generator
|
|
52
|
+
// ----------------------
|
|
53
|
+
function generateBadge(passed) {
|
|
54
|
+
const color = passed ? "green" : "red";
|
|
55
|
+
const text = passed ? "certified" : "failed";
|
|
56
|
+
const badgeSvg = `
|
|
57
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20">
|
|
58
|
+
<rect width="120" height="20" fill="${color}" rx="3" ry="3"/>
|
|
59
|
+
<text x="60" y="14" fill="#fff" font-family="Verdana" font-size="12" text-anchor="middle">${text}</text>
|
|
60
|
+
</svg>
|
|
61
|
+
`.trim();
|
|
62
|
+
|
|
63
|
+
return badgeSvg;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ----------------------
|
|
67
|
+
// Main
|
|
68
|
+
// ----------------------
|
|
69
|
+
(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const opts = parseArgs();
|
|
72
|
+
|
|
73
|
+
const suites =
|
|
74
|
+
opts.suites.length > 0
|
|
75
|
+
? opts.suites
|
|
76
|
+
: [
|
|
77
|
+
"R-001-allowlist",
|
|
78
|
+
"R-002-io-contract",
|
|
79
|
+
"R-003-failure-modes",
|
|
80
|
+
"R-004-invalid-syntax",
|
|
81
|
+
"R-005-resolver-metadata-contract"
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const result = await runAllTests({
|
|
85
|
+
suites,
|
|
86
|
+
resolver: kernel
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// JSON output
|
|
90
|
+
if (opts.json) {
|
|
91
|
+
const report = {
|
|
92
|
+
passed: result.failed === 0,
|
|
93
|
+
failedTests: result.failed,
|
|
94
|
+
suites
|
|
95
|
+
};
|
|
96
|
+
console.log(JSON.stringify(report, null, 2));
|
|
97
|
+
writeFileSync("conformance-report.json", JSON.stringify(report, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Badge output
|
|
101
|
+
if (opts.badge) {
|
|
102
|
+
const svg = generateBadge(result.failed === 0);
|
|
103
|
+
const badgePath = path.join("badges", "certified.svg");
|
|
104
|
+
if (!fs.existsSync("badges")) fs.mkdirSync("badges");
|
|
105
|
+
writeFileSync(badgePath, svg, "utf8");
|
|
106
|
+
console.log(`🏷 Badge written to ${badgePath}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result.failed > 0) {
|
|
110
|
+
console.error(`❌ ${result.failed} resolver test(s) failed`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log("✅ All resolver tests passed");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("🔥 Kernel test runner crashed");
|
|
119
|
+
console.error(err);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
})();
|
package/run.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
// ----------------------
|
|
7
|
+
// Load resolver from env
|
|
8
|
+
// ----------------------
|
|
9
|
+
let resolverPath = process.env.OLANG_RESOLVER;
|
|
10
|
+
|
|
11
|
+
if (!resolverPath) {
|
|
12
|
+
console.error("❌ OLANG_RESOLVER environment variable is not set");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Normalize relative paths
|
|
17
|
+
if (resolverPath.startsWith(".")) {
|
|
18
|
+
resolverPath = path.resolve(process.cwd(), resolverPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Verify resolver exists
|
|
22
|
+
if (!fs.existsSync(resolverPath)) {
|
|
23
|
+
console.error(`❌ Resolver path does not exist: ${resolverPath}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let resolver;
|
|
28
|
+
try {
|
|
29
|
+
resolver = require(resolverPath);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`❌ Failed to load resolver from ${resolverPath}`);
|
|
32
|
+
console.error(err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ----------------------
|
|
37
|
+
// CLI arg parsing
|
|
38
|
+
// ----------------------
|
|
39
|
+
function parseArgs() {
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const opts = {
|
|
42
|
+
suites: [],
|
|
43
|
+
json: false
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < args.length; i++) {
|
|
47
|
+
if (args[i] === "--suite" && args[i + 1]) {
|
|
48
|
+
opts.suites.push(args[++i]);
|
|
49
|
+
}
|
|
50
|
+
if (args[i] === "--json") {
|
|
51
|
+
opts.json = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return opts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ----------------------
|
|
59
|
+
// Imports
|
|
60
|
+
// ----------------------
|
|
61
|
+
const { runAllTests } = require("./lib/runner");
|
|
62
|
+
const { generateBadge } = require("./lib/badge");
|
|
63
|
+
|
|
64
|
+
// ----------------------
|
|
65
|
+
// Main
|
|
66
|
+
// ----------------------
|
|
67
|
+
(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const opts = parseArgs();
|
|
70
|
+
|
|
71
|
+
const suites =
|
|
72
|
+
opts.suites.length > 0
|
|
73
|
+
? opts.suites
|
|
74
|
+
: [
|
|
75
|
+
"R-001-allowlist",
|
|
76
|
+
"R-002-io-contract",
|
|
77
|
+
"R-003-failure-modes",
|
|
78
|
+
"R-004-invalid-syntax",
|
|
79
|
+
"R-005-resolver-metadata-contract"
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const result = await runAllTests({
|
|
83
|
+
suites,
|
|
84
|
+
resolver
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ----------------------
|
|
88
|
+
// Generate conformance report
|
|
89
|
+
// ----------------------
|
|
90
|
+
const conformanceReport = {
|
|
91
|
+
resolver: resolver?.resolverName || "unknown",
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
results: suites.map(suite => ({
|
|
94
|
+
suite,
|
|
95
|
+
status: result.failed > 0 ? "fail" : "pass"
|
|
96
|
+
}))
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(
|
|
100
|
+
path.join(process.cwd(), "conformance.json"),
|
|
101
|
+
JSON.stringify(conformanceReport, null, 2)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// ----------------------
|
|
105
|
+
// Generate certification badge
|
|
106
|
+
// ----------------------
|
|
107
|
+
generateBadge({
|
|
108
|
+
passed: result.failed === 0,
|
|
109
|
+
outputDir: process.cwd()
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ----------------------
|
|
113
|
+
// Output handling
|
|
114
|
+
// ----------------------
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
console.log(JSON.stringify(result, null, 2));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result.failed > 0) {
|
|
120
|
+
console.error(`❌ ${result.failed} resolver test(s) failed`);
|
|
121
|
+
console.error("❌ Resolver is NOT certified");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("✅ All resolver tests passed");
|
|
126
|
+
console.log("🏅 Resolver is O-lang CERTIFIED");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error("🔥 Resolver test runner crashed");
|
|
131
|
+
console.error(err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "O-lang Resolver Conformance Report",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["resolver", "timestamp", "results"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"resolver": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Resolver name or package identifier"
|
|
10
|
+
},
|
|
11
|
+
"timestamp": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"format": "date-time"
|
|
14
|
+
},
|
|
15
|
+
"results": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["suite", "status"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"suite": { "type": "string" },
|
|
22
|
+
"status": { "type": "string", "enum": ["pass", "fail"] },
|
|
23
|
+
"failures": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": { "type": "string" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|