@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/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
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ name: "olang-reference-resolver",
3
+
4
+ resolve(input) {
5
+ return {
6
+ ok: true,
7
+ output: input
8
+ };
9
+ }
10
+ };
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
+ }