ci.unit.test.ts
Raw
1import { describe, test, expect } from "bun:test";
2import {
3 parseCiConfig,
4 shouldTriggerPush,
5 shouldTriggerTag,
6} from "../src/services/ci.ts";
7
8describe("parseCiConfig", () => {
9 test("parses a minimal valid config", () => {
10 const cfg = parseCiConfig(`
11image = "debian:latest"
12[build]
13run_sh = "make all"
14`);
15 expect(cfg).not.toBeNull();
16 expect(cfg!.image).toBe("debian:latest");
17 expect(cfg!.steps).toHaveLength(1);
18 expect(cfg!.steps[0]!.name).toBe("build");
19 expect(cfg!.steps[0]!.run_sh).toBe("make all");
20 });
21
22 test("returns null when image is missing", () => {
23 expect(parseCiConfig(`[build]\nrun_sh = "make"`)).toBeNull();
24 });
25
26 test("returns null on invalid TOML", () => {
27 expect(parseCiConfig("image = [unclosed")).toBeNull();
28 });
29
30 test("excludes reserved table names from steps", () => {
31 const cfg = parseCiConfig(`
32image = "alpine"
33[on]
34push = ["main"]
35[variables]
36 [variables.FOO]
37 default = "bar"
38[step1]
39run_sh = "echo hi"
40`);
41 expect(cfg).not.toBeNull();
42 expect(cfg!.steps.map((s) => s.name)).toEqual(["step1"]);
43 });
44
45 test("preserves step order", () => {
46 const cfg = parseCiConfig(`
47image = "alpine"
48[setup]
49run_sh = "apt install"
50[compile]
51run_sh = "make"
52[test]
53run_sh = "./test.sh"
54`);
55 expect(cfg!.steps.map((s) => s.name)).toEqual([
56 "setup",
57 "compile",
58 "test",
59 ]);
60 });
61
62 test("parses optional top-level fields", () => {
63 const cfg = parseCiConfig(`
64image = "alpine"
65work_dir = "/ci"
66clone_project_to = "/ci/repo"
67shell_setup = "set -e"
68timeout = 1800
69cpu_limit = 2.0
70memory_limit = "1g"
71cache = ["/root/.npm"]
72`);
73 expect(cfg!.work_dir).toBe("/ci");
74 expect(cfg!.clone_project_to).toBe("/ci/repo");
75 expect(cfg!.shell_setup).toBe("set -e");
76 expect(cfg!.timeout).toBe(1800);
77 expect(cfg!.cpu_limit).toBe(2.0);
78 expect(cfg!.memory_limit).toBe("1g");
79 expect(cfg!.cache).toEqual(["/root/.npm"]);
80 });
81
82 test("parses on.push as array", () => {
83 const cfg = parseCiConfig(`
84image = "alpine"
85[on]
86push = ["main", "develop"]
87`);
88 expect(cfg!.on?.push).toEqual(["main", "develop"]);
89 });
90
91 test("parses on.tag and on.manual", () => {
92 const cfg = parseCiConfig(`
93image = "alpine"
94[on]
95tag = true
96manual = true
97`);
98 expect(cfg!.on?.tag).toBe(true);
99 expect(cfg!.on?.manual).toBe(true);
100 });
101
102 test("parses variables with default and description", () => {
103 const cfg = parseCiConfig(`
104image = "alpine"
105[variables]
106 [variables.DEPLOY_ENV]
107 default = "staging"
108 description = "Target environment"
109 [variables.VERSION]
110 default = "1.0.0"
111`);
112 expect(cfg!.variables?.DEPLOY_ENV).toEqual({
113 default: "staging",
114 description: "Target environment",
115 });
116 expect(cfg!.variables?.VERSION?.default).toBe("1.0.0");
117 });
118
119 test("parses step-level fields", () => {
120 const cfg = parseCiConfig(`
121image = "alpine"
122[test]
123run_sh = "./run_tests"
124run_if = 'test -n "\${CI_COMMIT_TAG}"'
125clear = true
126timeout = 300
127publish_file = ["/dist/binary"]
128publish_gzip = ["/dist/"]
129`);
130 const step = cfg!.steps[0]!;
131 expect(step.run_sh).toBe("./run_tests");
132 expect(step.run_if).toBe('test -n "${CI_COMMIT_TAG}"');
133 expect(step.clear).toBe(true);
134 expect(step.timeout).toBe(300);
135 expect(step.publish_file).toEqual(["/dist/binary"]);
136 expect(step.publish_gzip).toEqual(["/dist/"]);
137 });
138});
139
140describe("shouldTriggerPush", () => {
141 function cfg(push: string[] | boolean) {
142 return parseCiConfig(
143 `image="alpine"\n[on]\npush=${JSON.stringify(push)}`,
144 )!;
145 }
146
147 test("matches exact branch name", () => {
148 expect(shouldTriggerPush(cfg(["main"]), "main")).toBe(true);
149 expect(shouldTriggerPush(cfg(["main"]), "develop")).toBe(false);
150 });
151
152 test("wildcard * matches any branch", () => {
153 expect(shouldTriggerPush(cfg(["*"]), "anything")).toBe(true);
154 expect(shouldTriggerPush(cfg(["*"]), "main")).toBe(true);
155 });
156
157 test("matches one of multiple branches", () => {
158 const c = cfg(["main", "release"]);
159 expect(shouldTriggerPush(c, "main")).toBe(true);
160 expect(shouldTriggerPush(c, "release")).toBe(true);
161 expect(shouldTriggerPush(c, "feature/x")).toBe(false);
162 });
163
164 test("returns false when on.push is absent", () => {
165 const c = parseCiConfig('image="alpine"')!;
166 expect(shouldTriggerPush(c, "main")).toBe(false);
167 });
168
169 test("glob prefix matching", () => {
170 const c = cfg(["release/*"]);
171 expect(shouldTriggerPush(c, "release/1.0")).toBe(true);
172 expect(shouldTriggerPush(c, "main")).toBe(false);
173 });
174});
175
176describe("shouldTriggerTag", () => {
177 test("returns true when on.tag = true", () => {
178 const c = parseCiConfig('image="alpine"\n[on]\ntag=true')!;
179 expect(shouldTriggerTag(c)).toBe(true);
180 });
181
182 test("returns false when on.tag = false", () => {
183 const c = parseCiConfig('image="alpine"\n[on]\ntag=false')!;
184 expect(shouldTriggerTag(c)).toBe(false);
185 });
186
187 test("returns false when on.tag is absent", () => {
188 const c = parseCiConfig('image="alpine"')!;
189 expect(shouldTriggerTag(c)).toBe(false);
190 });
191});
192