import { afterEach, expect, test } from "bun:test"; import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { featureConfigOtelPayload, runFeatureConfigSchemaValidation, validateFeatureConfigSchema, } from "./feature-config-schema-warning.mjs"; const roots: string[] = []; const validatorScript = resolve(import.meta.dir, "feature-config-schema-warning.mjs"); const vendorBundle = resolve(import.meta.dir, "../../vendor/ajv-dist/8.17.1/ajv2020.min.js"); afterEach(() => { while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); }); test("valid nested Draft 2020-12 candidate remains warning-free", () => { const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "object", required: ["enabled", "views"], properties: { enabled: { type: "boolean" }, views: { type: "array", minItems: 1, items: { type: "string", pattern: "^[a-z-]+$" } }, }, additionalProperties: false, }), }), { tracePanel: '{"enabled":true,"views":["timeline"]}' }); expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({ warning: false, blocking: false, code: "feature-config-schema-valid", errorCount: 0, mutation: false, candidate: { source: "rendered-workload-env", matchedValueCount: 1 }, }); }); test("repo-owned Ajv 2020 bundle performs real validation without workspace dependencies", () => { const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }) }), { tracePanel: "true" }); const run = runValidator(root, vendorBundle); expect(run.status).toBe(0); expect(run.stderr).toContain('"code":"feature-config-schema-valid"'); expect(run.stderr).not.toContain("feature-config-validator-unavailable"); }); test("missing or corrupt Ajv bundle is a typed warning and exits zero", () => { const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }) }), { tracePanel: "true" }); const missing = runValidator(root, join(root, "missing-ajv2020.min.js")); expect(missing.status).toBe(0); expect(missing.stderr).toContain('"code":"feature-config-validator-unavailable"'); const corruptBundle = join(root, "corrupt-ajv2020.min.js"); writeFileSync(corruptBundle, "this is not JavaScript"); const corrupt = runValidator(root, corruptBundle); expect(corrupt.status).toBe(0); expect(corrupt.stderr).toContain('"code":"feature-config-validator-unavailable"'); }); test("missing schema is a typed non-blocking warning", () => { const root = fixture(); expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({ warning: true, blocking: false, code: "feature-config-schema-missing", }); }); test("invalid JSON and unsupported Draft 2020-12 schema are typed warnings", () => { const invalidJsonRoot = fixture("{"); expect(validateFeatureConfigSchema({ repoDir: invalidJsonRoot, renderedRoot: join(invalidJsonRoot, "rendered") }).code).toBe("feature-config-schema-invalid"); const invalidSchemaRoot = fixture(JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { tracePanel: { type: "not-a-json-schema-type", "x-unidesk-feature": "trace-panel" } } })); expect(validateFeatureConfigSchema({ repoDir: invalidSchemaRoot, renderedRoot: join(invalidSchemaRoot, "rendered") }).code).toBe("feature-config-schema-invalid"); }); test("missing rendered candidate is distinct from schema failure", () => { const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }) })); expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "missing") })).toMatchObject({ warning: true, blocking: false, code: "feature-config-input-missing", }); }); test("nested value mismatch is bounded and does not disclose the value", () => { const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "object", required: ["enabled"], properties: { enabled: { const: true } } }) }), { tracePanel: '{"enabled":false,"secret":"do-not-print"}', }); const result = validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") }); expect(result).toMatchObject({ warning: true, blocking: false, code: "feature-config-value-mismatch", valuesPrinted: false }); expect(result.firstError).not.toContain("do-not-print"); }); test("duplicate feature properties are reported without blocking", () => { const root = fixture(schema({ tracePanel: feature("trace-panel", { type: "boolean" }), tracePanelAlias: feature("trace-panel", { type: "boolean" }), })); expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({ warning: true, blocking: false, code: "feature-config-variable-duplicate", errorCount: 1, }); }); test("actual OTLP span and event carry redacted validation attributes", () => { const root = fixture(); const validation = validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") }); const payload = featureConfigOtelPayload(validation, { serviceName: "unidesk-cicd", consumer: "hwlab-nc01-v03", samplingRatio: 1, nowMs: 1, traceId: "1".repeat(32), spanId: "2".repeat(16), }); const span = payload.resourceSpans[0].scopeSpans[0].spans[0]; expect(span.name).toBe("cicd.feature_config.schema"); expect(span.events[0].name).toBe("feature-config-schema-validation"); expect(JSON.stringify(payload)).not.toContain("do-not-print"); expect(JSON.stringify(payload)).toContain("unidesk.otel.sampling_ratio"); }); test("missing endpoint remains a non-blocking OTel warning", async () => { const root = fixture(); const result = await runFeatureConfigSchemaValidation({ repoDir: root, renderedRoot: join(root, "rendered"), otelEndpoint: "", otelTimeoutMs: 300, }); expect(result.otelWarning).toMatchObject({ event: "cicd.otel.export", warning: true, blocking: false, causeCode: "ENDPOINT_MISSING", timeoutMs: 300, }); }); test("export failure uses configured timeout and remains non-blocking", async () => { const root = fixture(); let requestSignal: AbortSignal | null = null; const result = await runFeatureConfigSchemaValidation({ repoDir: root, renderedRoot: join(root, "rendered"), otelEndpoint: "http://otel.example.test/v1/traces", otelServiceName: "unidesk-cicd", otelSamplingRatio: 1, otelTimeoutMs: 300, fetchImpl: async (_input: URL | RequestInfo, init?: RequestInit) => { requestSignal = init?.signal ?? null; throw new Error("collector unavailable"); }, }); expect(requestSignal).not.toBeNull(); expect(result.otelWarning).toMatchObject({ event: "cicd.otel.export", warning: true, blocking: false, causeCode: "EXPORT_FAILED", timeoutMs: 300, }); }); test("unexpected validator failure is typed and never rejects", async () => { const options: Record = { repoDir: fixture(schema({ webProbeSentinel: feature("web-probe-sentinel", { type: "object" }) })), renderedRoot: "unused", otelEndpoint: "", otelTimeoutMs: 300, }; Object.defineProperty(options, "candidate", { get: () => { throw new Error("unexpected candidate failure"); } }); const result = await runFeatureConfigSchemaValidation(options); expect(result.validation).toMatchObject({ warning: true, blocking: false, code: "feature-config-validator-failure", mutation: false, }); expect(result.otelWarning).toMatchObject({ warning: true, blocking: false, causeCode: "ENDPOINT_MISSING" }); }); test("candidate scan ignores directory symlinks and bounds directory traversal", () => { const root = fixture(schema({ webProbeSentinel: feature("web-probe-sentinel", { type: "boolean" }) }), { webProbeSentinel: "true", }); symlinkSync(join(root, "rendered"), join(root, "rendered", "loop"), "dir"); expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({ warning: false, code: "feature-config-schema-valid", }); for (let index = 0; index <= 2_000; index += 1) mkdirSync(join(root, "rendered", `directory-${index}`)); expect(validateFeatureConfigSchema({ repoDir: root, renderedRoot: join(root, "rendered") })).toMatchObject({ warning: true, blocking: false, code: "feature-config-validator-failure", firstError: "rendered workload directory limit exceeded: 2000", }); }); function fixture(content?: string, env: Record = {}) { const root = mkdtempSync(join(tmpdir(), "feature-config-schema-")); roots.push(root); mkdirSync(join(root, "rendered")); if (content !== undefined) { mkdirSync(join(root, "config")); writeFileSync(join(root, "config", "feature-config.schema.json"), content); } writeFileSync(join(root, "rendered", "deployment.yaml"), deployment(env)); return root; } function runValidator(root: string, bundlePath: string) { return spawnSync(process.execPath, [validatorScript, "--rendered-root", join(root, "rendered")], { cwd: root, encoding: "utf8", env: { ...process.env, UNIDESK_AJV2020_BUNDLE: bundlePath, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "", OTEL_EXPORTER_TIMEOUT_MS: "300", }, }); } function schema(properties: Record) { return JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties, additionalProperties: false, }); } function feature(name: string, value: Record) { return { ...value, "x-unidesk-feature": name }; } function deployment(env: Record) { const rows = Object.entries(env).map(([name, value]) => ` - name: ${name}\n value: ${JSON.stringify(value)}`).join("\n"); return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: fixture\nspec:\n template:\n spec:\n containers:\n - name: fixture\n env:\n${rows || " []"}\n`; }