@intentius/chant-lexicon-gitlab 0.0.16 → 0.0.22

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.
Files changed (65) hide show
  1. package/dist/integrity.json +17 -4
  2. package/dist/manifest.json +1 -1
  3. package/dist/rules/wgl016.ts +82 -0
  4. package/dist/rules/wgl017.ts +54 -0
  5. package/dist/rules/wgl018.ts +39 -0
  6. package/dist/rules/wgl019.ts +44 -0
  7. package/dist/rules/wgl020.ts +56 -0
  8. package/dist/rules/wgl021.ts +62 -0
  9. package/dist/rules/wgl022.ts +44 -0
  10. package/dist/rules/wgl023.ts +51 -0
  11. package/dist/rules/wgl024.ts +46 -0
  12. package/dist/rules/wgl025.ts +49 -0
  13. package/dist/rules/wgl026.ts +67 -0
  14. package/dist/rules/wgl027.ts +54 -0
  15. package/dist/rules/wgl028.ts +67 -0
  16. package/dist/rules/yaml-helpers.ts +82 -0
  17. package/dist/skills/chant-gitlab.md +2 -2
  18. package/package.json +20 -2
  19. package/src/codegen/fetch.test.ts +30 -0
  20. package/src/codegen/generate.test.ts +65 -0
  21. package/src/codegen/idempotency.test.ts +28 -0
  22. package/src/codegen/naming.test.ts +93 -0
  23. package/src/codegen/snapshot.test.ts +28 -19
  24. package/src/composites/composites.test.ts +160 -0
  25. package/src/coverage.test.ts +15 -7
  26. package/src/import/roundtrip.test.ts +132 -0
  27. package/src/lint/post-synth/wgl016.test.ts +72 -0
  28. package/src/lint/post-synth/wgl016.ts +82 -0
  29. package/src/lint/post-synth/wgl017.test.ts +53 -0
  30. package/src/lint/post-synth/wgl017.ts +54 -0
  31. package/src/lint/post-synth/wgl018.test.ts +69 -0
  32. package/src/lint/post-synth/wgl018.ts +39 -0
  33. package/src/lint/post-synth/wgl019.test.ts +76 -0
  34. package/src/lint/post-synth/wgl019.ts +44 -0
  35. package/src/lint/post-synth/wgl020.test.ts +54 -0
  36. package/src/lint/post-synth/wgl020.ts +56 -0
  37. package/src/lint/post-synth/wgl021.test.ts +62 -0
  38. package/src/lint/post-synth/wgl021.ts +62 -0
  39. package/src/lint/post-synth/wgl022.test.ts +86 -0
  40. package/src/lint/post-synth/wgl022.ts +44 -0
  41. package/src/lint/post-synth/wgl023.test.ts +88 -0
  42. package/src/lint/post-synth/wgl023.ts +51 -0
  43. package/src/lint/post-synth/wgl024.test.ts +77 -0
  44. package/src/lint/post-synth/wgl024.ts +46 -0
  45. package/src/lint/post-synth/wgl025.test.ts +85 -0
  46. package/src/lint/post-synth/wgl025.ts +49 -0
  47. package/src/lint/post-synth/wgl026.test.ts +87 -0
  48. package/src/lint/post-synth/wgl026.ts +67 -0
  49. package/src/lint/post-synth/wgl027.test.ts +84 -0
  50. package/src/lint/post-synth/wgl027.ts +54 -0
  51. package/src/lint/post-synth/wgl028.test.ts +95 -0
  52. package/src/lint/post-synth/wgl028.ts +67 -0
  53. package/src/lint/post-synth/yaml-helpers.ts +82 -0
  54. package/src/lsp/completions.test.ts +16 -6
  55. package/src/lsp/hover.test.ts +18 -7
  56. package/src/plugin.test.ts +14 -1
  57. package/src/plugin.ts +20 -3
  58. package/src/testdata/pipelines/deploy-envs.gitlab-ci.yml +60 -0
  59. package/src/testdata/pipelines/docker-build.gitlab-ci.yml +41 -0
  60. package/src/testdata/pipelines/includes-templates.gitlab-ci.yml +52 -0
  61. package/src/testdata/pipelines/monorepo.gitlab-ci.yml +51 -0
  62. package/src/testdata/pipelines/multi-stage.gitlab-ci.yml +56 -0
  63. package/src/testdata/pipelines/simple.gitlab-ci.yml +9 -0
  64. package/src/validate.test.ts +12 -6
  65. package/src/variables.test.ts +58 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WGL027: Empty Script
3
+ *
4
+ * Detects jobs with `script: []` or scripts containing only empty strings.
5
+ * GitLab rejects jobs with empty scripts at pipeline validation time.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
10
+
11
+ export const wgl027: PostSynthCheck = {
12
+ id: "WGL027",
13
+ description: "Empty script — jobs with empty or blank script entries",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ const diagnostics: PostSynthDiagnostic[] = [];
17
+
18
+ for (const [entityName, entity] of ctx.entities) {
19
+ if (isPropertyDeclarable(entity)) continue;
20
+ const entityType = (entity as Record<string, unknown>).entityType as string;
21
+ if (entityType !== "GitLab::CI::Job") continue;
22
+
23
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
24
+ if (!props) continue;
25
+
26
+ const script = props.script;
27
+ if (script === undefined || script === null) continue;
28
+
29
+ let isEmpty = false;
30
+
31
+ if (Array.isArray(script)) {
32
+ if (script.length === 0) {
33
+ isEmpty = true;
34
+ } else if (script.every((s) => typeof s === "string" && s.trim() === "")) {
35
+ isEmpty = true;
36
+ }
37
+ } else if (typeof script === "string" && script.trim() === "") {
38
+ isEmpty = true;
39
+ }
40
+
41
+ if (isEmpty) {
42
+ diagnostics.push({
43
+ checkId: "WGL027",
44
+ severity: "error",
45
+ message: `Job "${entityName}" has an empty script — GitLab will reject this pipeline`,
46
+ entity: entityName,
47
+ lexicon: "gitlab",
48
+ });
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ },
54
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * WGL028: Redundant Needs
3
+ *
4
+ * Detects `needs:` entries that list jobs already implied by stage ordering.
5
+ * While not incorrect, redundant needs add noise and make the pipeline
6
+ * harder to maintain. This is informational only.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { getPrimaryOutput, extractStages, extractJobs } from "./yaml-helpers";
11
+
12
+ export function checkRedundantNeeds(yaml: string): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ const stages = extractStages(yaml);
16
+ if (stages.length === 0) return diagnostics;
17
+
18
+ const stageIndex = new Map<string, number>();
19
+ for (let i = 0; i < stages.length; i++) {
20
+ stageIndex.set(stages[i], i);
21
+ }
22
+
23
+ const jobs = extractJobs(yaml);
24
+
25
+ for (const [jobName, job] of jobs) {
26
+ if (!job.needs || !job.stage) continue;
27
+
28
+ const jobStageIdx = stageIndex.get(job.stage);
29
+ if (jobStageIdx === undefined) continue;
30
+
31
+ for (const need of job.needs) {
32
+ const neededJob = jobs.get(need);
33
+ if (!neededJob?.stage) continue;
34
+
35
+ const needStageIdx = stageIndex.get(neededJob.stage);
36
+ if (needStageIdx === undefined) continue;
37
+
38
+ // If the needed job is in an earlier stage, it's already implied
39
+ // by GitLab's default stage-based ordering
40
+ if (needStageIdx < jobStageIdx) {
41
+ diagnostics.push({
42
+ checkId: "WGL028",
43
+ severity: "info",
44
+ message: `Job "${jobName}" lists "${need}" in needs: but it's already in an earlier stage (${neededJob.stage} → ${job.stage})`,
45
+ entity: jobName,
46
+ lexicon: "gitlab",
47
+ });
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ }
54
+
55
+ export const wgl028: PostSynthCheck = {
56
+ id: "WGL028",
57
+ description: "Redundant needs — needs listing jobs already implied by stage ordering",
58
+
59
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
60
+ const diagnostics: PostSynthDiagnostic[] = [];
61
+ for (const [, output] of ctx.outputs) {
62
+ const yaml = getPrimaryOutput(output);
63
+ diagnostics.push(...checkRedundantNeeds(yaml));
64
+ }
65
+ return diagnostics;
66
+ },
67
+ };
@@ -141,3 +141,85 @@ export function extractJobs(yaml: string): Map<string, ParsedJob> {
141
141
  export function hasInclude(yaml: string): boolean {
142
142
  return /^include:/m.test(yaml);
143
143
  }
144
+
145
+ /**
146
+ * Extract global variables from serialized YAML.
147
+ */
148
+ export function extractGlobalVariables(yaml: string): Map<string, string> {
149
+ const vars = new Map<string, string>();
150
+ const match = yaml.match(/^variables:\n((?:\s+.+\n?)+)/m);
151
+ if (!match) return vars;
152
+
153
+ for (const line of match[1].split("\n")) {
154
+ const kv = line.match(/^\s+(\w+):\s+(.+)$/);
155
+ if (kv) {
156
+ vars.set(kv[1], kv[2].trim().replace(/^['"]|['"]$/g, ""));
157
+ }
158
+ }
159
+ return vars;
160
+ }
161
+
162
+ /**
163
+ * Extract the full section text for a given job name.
164
+ */
165
+ export function extractJobSection(yaml: string, jobName: string): string | null {
166
+ const sections = yaml.split("\n\n");
167
+ for (const section of sections) {
168
+ const lines = section.split("\n");
169
+ if (lines.length > 0 && lines[0].startsWith(`${jobName}:`)) {
170
+ return section;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Extract rules from a job section.
178
+ */
179
+ export function extractJobRules(section: string): ParsedRule[] {
180
+ const rules: ParsedRule[] = [];
181
+ const lines = section.split("\n");
182
+
183
+ let inRules = false;
184
+ let currentRule: ParsedRule = {};
185
+
186
+ for (const line of lines) {
187
+ if (line.match(/^\s+rules:$/)) {
188
+ inRules = true;
189
+ continue;
190
+ }
191
+
192
+ if (inRules) {
193
+ const ruleStart = line.match(/^\s+- (if|when|changes):\s*(.*)$/);
194
+ if (ruleStart) {
195
+ if (Object.keys(currentRule).length > 0) {
196
+ rules.push(currentRule);
197
+ }
198
+ currentRule = {};
199
+ if (ruleStart[1] === "if") currentRule.if = ruleStart[2].trim();
200
+ if (ruleStart[1] === "when") currentRule.when = ruleStart[2].trim();
201
+ continue;
202
+ }
203
+
204
+ const whenMatch = line.match(/^\s+when:\s+(.+)$/);
205
+ if (whenMatch) {
206
+ currentRule.when = whenMatch[1].trim();
207
+ continue;
208
+ }
209
+
210
+ // End of rules block
211
+ if (!line.match(/^\s+\s/) || line.match(/^\s+[a-z_]+:/) && !line.match(/^\s+when:/)) {
212
+ if (Object.keys(currentRule).length > 0) {
213
+ rules.push(currentRule);
214
+ }
215
+ inRules = false;
216
+ }
217
+ }
218
+ }
219
+
220
+ if (inRules && Object.keys(currentRule).length > 0) {
221
+ rules.push(currentRule);
222
+ }
223
+
224
+ return rules;
225
+ }
@@ -8,7 +8,7 @@ user-invocable: true
8
8
 
9
9
  ## How chant and GitLab CI relate
10
10
 
11
- chant is a **synthesis-only** tool — it compiles TypeScript source files into `.gitlab-ci.yml` (YAML). chant does NOT call GitLab APIs. Your job as an agent is to bridge the two:
11
+ chant is a **synthesis compiler** — it compiles TypeScript source files into `.gitlab-ci.yml` (YAML). `chant build` does not call GitLab APIs; synthesis is pure and deterministic. Your job as an agent is to bridge synthesis and deployment:
12
12
 
13
13
  - Use **chant** for: build, lint, diff (local YAML comparison)
14
14
  - Use **git + GitLab API** for: push, merge requests, pipeline monitoring, job logs, rollback, and all deployment operations
@@ -147,7 +147,7 @@ Add `"dry_run": true, "include_merged_yaml": true` for full expansion with inclu
147
147
  | Step | Catches | When to run |
148
148
  |------|---------|-------------|
149
149
  | `chant lint` | Deprecated only/except (WGL001), missing script (WGL002), missing stage (WGL003), artifacts without expiry (WGL004) | Every edit |
150
- | `chant build` | Post-synth checks: undefined stages (WGL010), unreachable jobs (WGL011), deprecated properties (WGL012), invalid needs targets (WGL013), invalid extends targets (WGL014), circular needs chains (WGL015) | Before push |
150
+ | `chant build` | Post-synth checks: undefined stages (WGL010), unreachable jobs (WGL011), deprecated properties (WGL012), invalid needs/extends targets (WGL013-014), circular needs (WGL015), secrets in variables (WGL016), insecure registry (WGL017), missing timeout/retry (WGL018-019), duplicate jobs (WGL020), unused variables (WGL021), missing artifacts expiry (WGL022), overly broad rules (WGL023), manual without allow_failure (WGL024), missing cache key (WGL025), DinD without TLS (WGL026), empty script (WGL027), redundant needs (WGL028) | Before push |
151
151
  | CI Lint API | GitLab-specific validation: include resolution, variable expansion, YAML schema errors | Before merge to default branch |
152
152
 
153
153
  Always run all three before deploying. Lint catches things the API cannot (and vice versa).
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gitlab",
3
- "version": "0.0.16",
3
+ "version": "0.0.22",
4
+ "description": "GitLab CI lexicon for chant — declarative IaC in TypeScript",
4
5
  "license": "Apache-2.0",
6
+ "homepage": "https://intentius.io/chant",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/intentius/chant.git",
10
+ "directory": "lexicons/gitlab"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/intentius/chant/issues"
14
+ },
15
+ "keywords": [
16
+ "infrastructure-as-code",
17
+ "iac",
18
+ "typescript",
19
+ "gitlab",
20
+ "gitlab-ci",
21
+ "chant"
22
+ ],
5
23
  "type": "module",
6
24
  "files": [
7
25
  "src/",
@@ -25,7 +43,7 @@
25
43
  "prepack": "bun run generate && bun run bundle && bun run validate"
26
44
  },
27
45
  "dependencies": {
28
- "@intentius/chant": "0.0.15"
46
+ "@intentius/chant": "0.0.22"
29
47
  },
30
48
  "devDependencies": {
31
49
  "typescript": "^5.9.3"
@@ -0,0 +1,30 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { fetchCISchema, fetchSchemas, getCachePath, GITLAB_SCHEMA_VERSION } from "./fetch";
3
+
4
+ describe("fetch", () => {
5
+ test("fetchCISchema function exists", () => {
6
+ expect(typeof fetchCISchema).toBe("function");
7
+ });
8
+
9
+ test("fetchSchemas function exists", () => {
10
+ expect(typeof fetchSchemas).toBe("function");
11
+ });
12
+
13
+ test("GITLAB_SCHEMA_VERSION is defined", () => {
14
+ expect(typeof GITLAB_SCHEMA_VERSION).toBe("string");
15
+ expect(GITLAB_SCHEMA_VERSION).toMatch(/^v\d+\.\d+\.\d+-ee$/);
16
+ });
17
+
18
+ test("getCachePath returns a path", () => {
19
+ const cachePath = getCachePath();
20
+ expect(typeof cachePath).toBe("string");
21
+ expect(cachePath).toContain("gitlab-ci-schema.json");
22
+ });
23
+
24
+ test.skip("integration: fetchCISchema returns Buffer (requires network)", async () => {
25
+ const data = await fetchCISchema();
26
+ expect(data).toBeInstanceOf(Buffer);
27
+ const parsed = JSON.parse(data.toString("utf-8"));
28
+ expect(parsed.properties).toBeDefined();
29
+ });
30
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generate, writeGeneratedFiles } from "./generate";
3
+ import { loadSchemaFixtureMap } from "../testdata/load-fixtures";
4
+
5
+ describe("generate pipeline components", () => {
6
+ test("exports generate function", () => {
7
+ expect(typeof generate).toBe("function");
8
+ });
9
+
10
+ test("exports writeGeneratedFiles function", () => {
11
+ expect(typeof writeGeneratedFiles).toBe("function");
12
+ });
13
+
14
+ // Integration test — requires network, skip by default
15
+ test.skip("generates full pipeline (integration)", async () => {
16
+ const result = await generate({ verbose: false });
17
+
18
+ expect(result.resources).toBeGreaterThan(0);
19
+ expect(result.properties).toBeGreaterThan(0);
20
+ expect(result.lexiconJSON).toBeTruthy();
21
+ expect(result.typesDTS).toBeTruthy();
22
+ expect(result.indexTS).toBeTruthy();
23
+ });
24
+ });
25
+
26
+ describe("offline fixture pipeline", () => {
27
+ test("generates from fixtures", async () => {
28
+ const fixtures = loadSchemaFixtureMap();
29
+ const result = await generate({ schemaSource: fixtures });
30
+
31
+ expect(result.resources).toBeGreaterThanOrEqual(1);
32
+ expect(result.properties).toBeGreaterThanOrEqual(0);
33
+ expect(result.lexiconJSON).toBeTruthy();
34
+ expect(result.typesDTS).toBeTruthy();
35
+ expect(result.indexTS).toBeTruthy();
36
+ });
37
+
38
+ test("output contains expected entities", async () => {
39
+ const fixtures = loadSchemaFixtureMap();
40
+ const result = await generate({ schemaSource: fixtures });
41
+
42
+ const lexicon = JSON.parse(result.lexiconJSON);
43
+ // Core CI entities should be present
44
+ expect(lexicon["Job"]).toBeDefined();
45
+ expect(lexicon["Default"]).toBeDefined();
46
+ expect(lexicon["Workflow"]).toBeDefined();
47
+ });
48
+
49
+ test("resource and property counts are non-zero", async () => {
50
+ const fixtures = loadSchemaFixtureMap();
51
+ const result = await generate({ schemaSource: fixtures });
52
+
53
+ expect(result.resources).toBeGreaterThan(0);
54
+ // Properties are optional in a minimal fixture but count should be defined
55
+ expect(typeof result.properties).toBe("number");
56
+ expect(typeof result.enums).toBe("number");
57
+ });
58
+
59
+ test("produces no warnings from fixtures", async () => {
60
+ const fixtures = loadSchemaFixtureMap();
61
+ const result = await generate({ schemaSource: fixtures });
62
+
63
+ expect(result.warnings).toHaveLength(0);
64
+ });
65
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generate } from "./generate";
3
+ import { loadSchemaFixtureMap } from "../testdata/load-fixtures";
4
+
5
+ describe("generation idempotency", () => {
6
+ test("two generations from same fixtures produce identical content", async () => {
7
+ const fixtures = loadSchemaFixtureMap();
8
+
9
+ const result1 = await generate({ schemaSource: fixtures });
10
+ const result2 = await generate({ schemaSource: fixtures });
11
+
12
+ // Content should be byte-identical
13
+ expect(result1.lexiconJSON).toBe(result2.lexiconJSON);
14
+ expect(result1.typesDTS).toBe(result2.typesDTS);
15
+ expect(result1.indexTS).toBe(result2.indexTS);
16
+ });
17
+
18
+ test("resource counts match between runs", async () => {
19
+ const fixtures = loadSchemaFixtureMap();
20
+
21
+ const result1 = await generate({ schemaSource: fixtures });
22
+ const result2 = await generate({ schemaSource: fixtures });
23
+
24
+ expect(result1.resources).toBe(result2.resources);
25
+ expect(result1.properties).toBe(result2.properties);
26
+ expect(result1.enums).toBe(result2.enums);
27
+ });
28
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { gitlabShortName, type GitLabParseResult } from "./parse";
3
+ import { NamingStrategy } from "./naming";
4
+
5
+ /**
6
+ * Chant's naming philosophy for GitLab: simple, flat names.
7
+ * Every priorityName should be the short name from the GitLab::CI:: namespace.
8
+ * No "CI" prefix should leak into class names.
9
+ */
10
+
11
+ /** Build a minimal GitLabParseResult for a given typeName. */
12
+ function stubResult(typeName: string, isProperty?: boolean): GitLabParseResult {
13
+ return {
14
+ resource: {
15
+ typeName,
16
+ properties: [],
17
+ attributes: [],
18
+ deprecatedProperties: [],
19
+ },
20
+ propertyTypes: [],
21
+ enums: [],
22
+ isProperty,
23
+ };
24
+ }
25
+
26
+ describe("naming spec fidelity", () => {
27
+ const typeNames = [
28
+ "GitLab::CI::Job",
29
+ "GitLab::CI::Default",
30
+ "GitLab::CI::Workflow",
31
+ "GitLab::CI::Artifacts",
32
+ "GitLab::CI::Cache",
33
+ "GitLab::CI::Image",
34
+ "GitLab::CI::Rule",
35
+ "GitLab::CI::Retry",
36
+ "GitLab::CI::AllowFailure",
37
+ "GitLab::CI::Parallel",
38
+ "GitLab::CI::Include",
39
+ "GitLab::CI::Release",
40
+ "GitLab::CI::Environment",
41
+ "GitLab::CI::Trigger",
42
+ "GitLab::CI::AutoCancel",
43
+ "GitLab::CI::WorkflowRule",
44
+ "GitLab::CI::Need",
45
+ "GitLab::CI::Inherit",
46
+ ];
47
+
48
+ const results = typeNames.map((t) => stubResult(t));
49
+ const strategy = new NamingStrategy(results);
50
+
51
+ test("all priority names resolve to short name", () => {
52
+ for (const typeName of typeNames) {
53
+ const resolved = strategy.resolve(typeName);
54
+ const shortName = gitlabShortName(typeName);
55
+ expect(resolved).toBe(shortName);
56
+ }
57
+ });
58
+
59
+ test("no priority name has CI prefix", () => {
60
+ for (const typeName of typeNames) {
61
+ const resolved = strategy.resolve(typeName)!;
62
+ // "CI" as a prefix (e.g. "CIJob") would be wrong; "Cache" starting with "C" is fine
63
+ expect(resolved.startsWith("CI")).toBe(false);
64
+ }
65
+ });
66
+
67
+ test("no priority name has GitLab prefix", () => {
68
+ for (const typeName of typeNames) {
69
+ const resolved = strategy.resolve(typeName)!;
70
+ expect(resolved.startsWith("GitLab")).toBe(false);
71
+ }
72
+ });
73
+
74
+ test("all 18 priority names are mapped", () => {
75
+ expect(typeNames).toHaveLength(18);
76
+ for (const typeName of typeNames) {
77
+ expect(strategy.resolve(typeName)).toBeDefined();
78
+ }
79
+ });
80
+ });
81
+
82
+ describe("property type naming", () => {
83
+ test("resolves property entities correctly", () => {
84
+ const results = [
85
+ stubResult("GitLab::CI::Artifacts", true),
86
+ stubResult("GitLab::CI::Cache", true),
87
+ ];
88
+ const strategy = new NamingStrategy(results);
89
+
90
+ expect(strategy.resolve("GitLab::CI::Artifacts")).toBe("Artifacts");
91
+ expect(strategy.resolve("GitLab::CI::Cache")).toBe("Cache");
92
+ });
93
+ });
@@ -1,19 +1,21 @@
1
1
  import { describe, test, expect } from "bun:test";
2
- import { readFileSync } from "fs";
2
+ import { existsSync, readFileSync } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
 
6
6
  const generatedDir = join(dirname(dirname(fileURLToPath(import.meta.url))), "generated");
7
+ const hasGenerated = existsSync(join(generatedDir, "lexicon-gitlab.json"));
7
8
 
8
9
  describe("generated lexicon-gitlab.json", () => {
9
- const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
10
- const registry = JSON.parse(content);
11
-
12
- test("is valid JSON with expected entries", () => {
10
+ test.skipIf(!hasGenerated)("is valid JSON with expected entries", () => {
11
+ const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
12
+ const registry = JSON.parse(content);
13
13
  expect(Object.keys(registry)).toHaveLength(19);
14
14
  });
15
15
 
16
- test("contains all resource entities", () => {
16
+ test.skipIf(!hasGenerated)("contains all resource entities", () => {
17
+ const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
18
+ const registry = JSON.parse(content);
17
19
  expect(registry.Job).toBeDefined();
18
20
  expect(registry.Job.kind).toBe("resource");
19
21
  expect(registry.Job.resourceType).toBe("GitLab::CI::Job");
@@ -26,7 +28,9 @@ describe("generated lexicon-gitlab.json", () => {
26
28
  expect(registry.Workflow.kind).toBe("resource");
27
29
  });
28
30
 
29
- test("contains all property entities", () => {
31
+ test.skipIf(!hasGenerated)("contains all property entities", () => {
32
+ const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
33
+ const registry = JSON.parse(content);
30
34
  const propertyNames = [
31
35
  "AllowFailure", "Artifacts", "AutoCancel", "Cache",
32
36
  "Environment", "Image", "Include", "Inherit", "Parallel",
@@ -40,7 +44,9 @@ describe("generated lexicon-gitlab.json", () => {
40
44
  }
41
45
  });
42
46
 
43
- test("entries match snapshot", () => {
47
+ test.skipIf(!hasGenerated)("entries match snapshot", () => {
48
+ const content = readFileSync(join(generatedDir, "lexicon-gitlab.json"), "utf-8");
49
+ const registry = JSON.parse(content);
44
50
  expect(registry.Job).toMatchSnapshot();
45
51
  expect(registry.Artifacts).toMatchSnapshot();
46
52
  expect(registry.Cache).toMatchSnapshot();
@@ -49,9 +55,8 @@ describe("generated lexicon-gitlab.json", () => {
49
55
  });
50
56
 
51
57
  describe("generated index.d.ts", () => {
52
- const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
53
-
54
- test("contains all class declarations", () => {
58
+ test.skipIf(!hasGenerated)("contains all class declarations", () => {
59
+ const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
55
60
  const expectedClasses = [
56
61
  "Job", "Default", "Workflow",
57
62
  "AllowFailure", "Artifacts", "AutoCancel", "Cache",
@@ -64,14 +69,16 @@ describe("generated index.d.ts", () => {
64
69
  }
65
70
  });
66
71
 
67
- test("contains CI variables declaration", () => {
72
+ test.skipIf(!hasGenerated)("contains CI variables declaration", () => {
73
+ const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
68
74
  expect(content).toContain("export declare const CI");
69
75
  expect(content).toContain("readonly CommitBranch: string");
70
76
  expect(content).toContain("readonly CommitSha: string");
71
77
  expect(content).toContain("readonly PipelineId: string");
72
78
  });
73
79
 
74
- test("Job class has key properties in constructor", () => {
80
+ test.skipIf(!hasGenerated)("Job class has key properties in constructor", () => {
81
+ const content = readFileSync(join(generatedDir, "index.d.ts"), "utf-8");
75
82
  // Extract the Job class declaration
76
83
  const jobMatch = content.match(/export declare class Job \{[\s\S]*?\n\}/);
77
84
  expect(jobMatch).toBeDefined();
@@ -85,27 +92,29 @@ describe("generated index.d.ts", () => {
85
92
  });
86
93
 
87
94
  describe("generated index.ts", () => {
88
- const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
89
-
90
- test("has correct resource createResource calls", () => {
95
+ test.skipIf(!hasGenerated)("has correct resource createResource calls", () => {
96
+ const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
91
97
  expect(content).toContain('createResource("GitLab::CI::Default"');
92
98
  expect(content).toContain('createResource("GitLab::CI::Job"');
93
99
  expect(content).toContain('createResource("GitLab::CI::Workflow"');
94
100
  });
95
101
 
96
- test("has correct property createProperty calls", () => {
102
+ test.skipIf(!hasGenerated)("has correct property createProperty calls", () => {
103
+ const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
97
104
  expect(content).toContain('createProperty("GitLab::CI::Artifacts"');
98
105
  expect(content).toContain('createProperty("GitLab::CI::Cache"');
99
106
  expect(content).toContain('createProperty("GitLab::CI::Image"');
100
107
  expect(content).toContain('createProperty("GitLab::CI::Rule"');
101
108
  });
102
109
 
103
- test("re-exports reference and CI", () => {
110
+ test.skipIf(!hasGenerated)("re-exports reference and CI", () => {
111
+ const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
104
112
  expect(content).toContain('export { reference } from "../intrinsics"');
105
113
  expect(content).toContain('export { CI } from "../variables"');
106
114
  });
107
115
 
108
- test("imports from runtime", () => {
116
+ test.skipIf(!hasGenerated)("imports from runtime", () => {
117
+ const content = readFileSync(join(generatedDir, "index.ts"), "utf-8");
109
118
  expect(content).toContain('from "./runtime"');
110
119
  });
111
120
  });