Files
pikasTech-unidesk/scripts/native/cicd/feature-config-schema-warning.test.ts
T

249 lines
10 KiB
TypeScript

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<string, unknown> = {
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<string, string> = {}) {
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<string, unknown>) {
return JSON.stringify({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties,
additionalProperties: false,
});
}
function feature(name: string, value: Record<string, unknown>) {
return { ...value, "x-unidesk-feature": name };
}
function deployment(env: Record<string, string>) {
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`;
}