Skip to content

Testing Your Lexicon

This guide documents what each test file in a lexicon should cover, with patterns and checklists. Follow these to ensure consistent test quality across lexicons.

Two test files are required by name. chant dev check-lexicon fails tier 1 without src/plugin.test.ts and src/serializer.test.ts.

Shared helpers: @intentius/chant-test-utils

Section titled “Shared helpers: @intentius/chant-test-utils”

Before hand-rolling a fixture, check packages/test-utils/src/. 131 test files across the shipped lexicons import from it, and a lexicon that reimplements makeCtx gets a context that drifts from the one the real build path constructs.

HelperModuleWhat it gives you
createPostSynthContext(outputs)fixtures.tsA PostSynthContext from a record of lexicon name to template object, JSON-serialized. Wires ctx.docs exactly as runPostSynthChecks does (chant #975)
makePostSynthCtx(lexicon, output, entities?)post-synth-harness.tsThe same, from an already-serialized string (YAML, JSON, TOML)
makePostSynthCtxFromFiles(lexicon, files, primary?, entities?)post-synth-harness.tsFrom a multi-file SerializerResult, as helm produces
makePostSynthCtxFromJSON(lexicon, template, entities?)post-synth-harness.tsFrom a plain object, for JSON formats
runCheck, expectNoDiagnostics, expectDiagnosticpost-synth-harness.tsRun one check and assert on its diagnostics
describeExample, describeAllExamplesexample-harness.tsRegister lint and build tests for examples/*, which tier 1 requires to build
createMockEntity, createMockSerializer, createMockLintRule, createMockLintContextfixtures.tsCore-level fixtures for lint rule tests
createMockPluginmock-plugin.tsA LexiconPlugin with only the members you pass, for exercising a degradation path
describeObservationConformanceobservation-conformance.tsThe observation contract suite. See Observation Contract
describeApplyConformanceapply-conformance.tsThe apply contract suite. See Apply Conformance Suite
createTestDir, cleanupTestDir, withTestDirfs.tsTemp-directory lifecycle
expectToThrow, FIXTUREassertions.ts, fixture-constants.tsAssertion and constant helpers

The package is @intentius/chant-test-utils, with ./example-harness as a second entry point. It is private: true and unpublished, and resolves through the root workspaces: ["packages/*", "lexicons/*"] entry, so no lexicon declares it as a dependency. A lexicon developed outside this repo has to copy the helpers it needs.

Tier 1 requires every shipped example to build and to pass the lexicon’s own post-synth checks. describeAllExamples registers both tests per example directory:

examples/examples.test.ts
import { describeAllExamples } from "@intentius/chant-test-utils/example-harness";
import { mySerializer } from "@intentius/chant-lexicon-mylexicon";
describeAllExamples(
{
lexicon: "mylexicon",
serializer: mySerializer,
outputKey: "mylexicon",
examplesDir: import.meta.dirname,
},
{
"getting-started": { skipLint: true },
},
);

The second argument overrides per example: checks(output) for custom assertions, skipLint, skipBuild. See lexicons/k8s/examples/examples.test.ts.

The serializer converts declarables to your output format. Cover these 12 cases, which are the same twelve listed in Create a Serializer.

Create mockResource and mockProperty helpers using DECLARABLE_MARKER. Both put the authored values under props, which is where a real generated class puts them:

import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
function mockResource(entityType: string, props: Record<string, unknown>): any {
return {
[DECLARABLE_MARKER]: true,
lexicon: "my-lexicon",
entityType,
kind: "resource",
props,
};
}
function mockProperty(entityType: string, props: Record<string, unknown>): any {
return {
[DECLARABLE_MARKER]: true,
lexicon: "my-lexicon",
entityType,
kind: "property",
props,
};
}
#Test caseWhat it verifies
1Serializer nameserializer.name matches the lexicon
2Rule prefixserializer.rulePrefix matches the declared prefix
3Empty entitiesserialize(new Map()) returns an empty string
4Single resourceProduces valid output in your format (YAML/JSON/text)
5Auto-generated namecamelCase export name becomes your format’s name form
6Explicit name preservedAn author-set name is not overwritten
7Multi-resourceMultiple entities joined correctly (a --- separator for YAML, one file per entity for k3s and k3d)
8Defaults mergedWhatever your format injects when the declaration is silent
9Explicit overrides winA resource-level value beats the default
10Property entities skippedkind: "property" entities are inlined, not emitted as separate documents
11Key orderingOutput keys in canonical order, independent of prop declaration order
12Format-specificWhatever is peculiar to your format. Cedar parses its own output back through cedar-wasm; k3s asserts flag names survive as keys

lexicons/cedar/src/serializer.test.ts is sectioned by these numbers.

Two more that every shipped serializer test covers:

  • Ownership stamping when the build carries a marker, plus the case where an author’s own key with the same name wins (lexicons/k3d/src/serializer.test.ts, lexicons/k8s/src/serializer-ownership.test.ts).
  • Round-trip: the emitted text parses back to the declared values.

Additional lexicon-specific cases:

  • Specless types (K8s: ConfigMap, Secret)
  • Fallback type resolution (GCP: derive GVK from entity type string)

Only for a lexicon that ships label/annotation declaration utilities, as k8s does (lexicons/k8s/src/default-labels.ts). chant dev check-lexicon does not look for this file.

Test the label/annotation declaration utilities:

Test caseWhat it verifies
Correct markersDEFAULT_LABELS_MARKER and DECLARABLE_MARKER are true
Lexicon property.lexicon matches your lexicon name
Entity type.entityType matches convention
Accessible valuesLabels/annotations are readable
Empty alloweddefaultLabels({}) doesn’t throw
Type guardsisDefaultLabels() true for labels, false for annotations/null/undefined
Cross-checksisDefaultAnnotations() false for labels, and vice versa

Use test.skipIf(!hasGenerated) for tests that depend on the generated lexicon registry:

import { existsSync, readFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-{name}.json");
const hasGenerated = existsSync(lexiconPath) && (() => {
try {
const content = JSON.parse(readFileSync(lexiconPath, "utf-8"));
return Object.keys(content).length > 0;
} catch { return false; }
})();
Test casehasGeneratedWhat it verifies
Non-constructor contextNoReturns empty array for const x = 42
Constructor prefixYesnew D returns results including Deployment
Prefix filteringYesnew StatefulS returns StatefulSet
Test casehasGeneratedWhat it verifies
Unknown wordNoReturns undefined for nonexistent resource
Known resourceYesReturns defined hover info
Empty stringYesReturns undefined
Content checkYesHover content is non-empty
Test caseWhat it verifies
Empty YAMLReturns empty resources and parameters
Single resourceCorrect type mapping (apiVersion+kind → type name)
Multi-docMultiple resources from ----separated YAML
Type mappingEach apiVersion/kind combo maps correctly
Non-lexicon filteredResources from other lexicons are ignored
Logical namemetadata.name extracted correctly
PropertiesInclude metadata+spec, exclude apiVersion/kind
Parameters emptyLexicons without parameters return []
Test caseWhat it verifies
Valid TypeScriptOutput contains import + constructor
Correct import sourceUses @intentius/chant-lexicon-{name}
Multiple resourcesMultiple export const declarations
Variable namingkebab-case → camelCase conversion
Empty IRDoesn’t crash on empty input
Nested objectsProper formatting of nested props

Create testdata manifests as instance YAML (not CRD schemas):

src/testdata/manifests/
├── resource-a.yaml # Single resource
├── resource-b.yaml # Single resource
└── full-app.yaml # Multi-doc with 3+ resources
Test caseWhat it verifies
Single resource roundtripParse → generate → contains constructor
Multi-doc roundtripAll resources present in output
Inline YAMLParse+generate works without fixture file
const hasGenerated = existsSync(join(generatedDir, "lexicon-{name}.json"));
test.skipIf(!hasGenerated)("analyze function exists", async () => {
const { analyze } = await import("./coverage");
expect(typeof analyze).toBe("function");
});
test("handles missing files gracefully", async () => {
if (!hasGenerated) {
try { await analyze(); } catch { /* Expected */ }
}
});

Pattern: build the context with the shared harness

Section titled “Pattern: build the context with the shared harness”

A hand-rolled { outputs: new Map(...) } is not a PostSynthContext: it has no entities, no buildResult, and no lazy ctx.docs, so a check that reads any of them passes in the test and throws in the build. Use the harness.

import { makePostSynthCtx } from "@intentius/chant-test-utils";
const ctx = makePostSynthCtx("mylexicon", yaml);

For a JSON format, makePostSynthCtxFromJSON("aws", template) serializes the object for you; createPostSynthContext({ azure: template }) does the same keyed by lexicon, which is the form lexicons/azure/src/lint/post-synth/*.test.ts uses. For a multi-file serializer, makePostSynthCtxFromFiles.

Every post-synth check needs both positive and negative tests:

import { makePostSynthCtx, expectDiagnostic, expectNoDiagnostics } from "@intentius/chant-test-utils";
describe("WGCXXX: description", () => {
test("flags bad pattern", () => {
const ctx = makePostSynthCtx("mylexicon", `...bad yaml...`);
expectDiagnostic(wgcXXX, ctx, { checkId: "WGCXXX" });
});
test("no diagnostic when correct", () => {
const ctx = makePostSynthCtx("mylexicon", `...good yaml...`);
expectNoDiagnostics(wgcXXX, ctx);
});
});

The chant YAML parser treats unquoted https://... as key-value pairs. Always quote URLs in test YAML:

# Bad — parsed as { https: "//..." }
- https://www.googleapis.com/auth/cloud-platform
# Good
- "https://www.googleapis.com/auth/cloud-platform"

Run the lexicon’s suite with npx vitest run lexicons/{name}, then chant dev check-lexicon lexicons/{name} for the tier list. See the Completeness Checklist for what each tier row means.