|
1 | 1 | import { describe, expect, it } from "vitest"; |
2 | | -import { getCLIWarnings, parseCLIArgs } from "./args"; |
| 2 | +import { parseCLIArgs } from "./args"; |
3 | 3 | import { LogLevel } from "./log"; |
4 | 4 |
|
5 | 5 | describe("parseCLIArgs", () => { |
@@ -155,23 +155,192 @@ describe("parseCLIArgs", () => { |
155 | 155 | expect(result.links).toEqual([{ url: "https://ci.example.com/run/123" }]); |
156 | 156 | }); |
157 | 157 |
|
158 | | - it("throws on unknown flags (strict mode)", () => { |
159 | | - expect(() => parseCLIArgs(["--unknown-flag"])).toThrow(); |
160 | | - }); |
161 | | - |
162 | | - it("returns no warning when --name is used with update", () => { |
163 | | - const result = parseCLIArgs(["update", "--name", "Release 1.2.0"]); |
164 | | - expect(getCLIWarnings(result)).toEqual([]); |
| 158 | + describe("--document / --document-file", () => { |
| 159 | + it("parses --document with Title=content", () => { |
| 160 | + const result = parseCLIArgs(["sync", "--document", "Changelog=# v1.0.0\n\n- first release"]); |
| 161 | + expect(result.documents).toEqual([ |
| 162 | + { title: "Changelog", source: { kind: "inline", content: "# v1.0.0\n\n- first release" } }, |
| 163 | + ]); |
| 164 | + }); |
| 165 | + |
| 166 | + it("parses multiple repeatable --document values", () => { |
| 167 | + const result = parseCLIArgs([ |
| 168 | + "sync", |
| 169 | + "--document", |
| 170 | + "Changelog=# v1.0.0", |
| 171 | + "--document=Deploy=Deployed to production.", |
| 172 | + ]); |
| 173 | + expect(result.documents).toEqual([ |
| 174 | + { title: "Changelog", source: { kind: "inline", content: "# v1.0.0" } }, |
| 175 | + { title: "Deploy", source: { kind: "inline", content: "Deployed to production." } }, |
| 176 | + ]); |
| 177 | + }); |
| 178 | + |
| 179 | + it("preserves whitespace and equals signs in --document content", () => { |
| 180 | + const result = parseCLIArgs(["sync", "--document", "Args=key1=value1\n key2 = value2"]); |
| 181 | + expect(result.documents).toEqual([ |
| 182 | + { title: "Args", source: { kind: "inline", content: "key1=value1\n key2 = value2" } }, |
| 183 | + ]); |
| 184 | + }); |
| 185 | + |
| 186 | + it("parses --document-file with Title=path", () => { |
| 187 | + const result = parseCLIArgs(["sync", "--document-file", "Changelog=./CHANGELOG.md"]); |
| 188 | + expect(result.documents).toEqual([{ title: "Changelog", source: { kind: "file", path: "./CHANGELOG.md" } }]); |
| 189 | + }); |
| 190 | + |
| 191 | + it("trims title and path on --document-file", () => { |
| 192 | + const result = parseCLIArgs(["sync", "--document-file", " Changelog = ./CHANGELOG.md "]); |
| 193 | + expect(result.documents).toEqual([{ title: "Changelog", source: { kind: "file", path: "./CHANGELOG.md" } }]); |
| 194 | + }); |
| 195 | + |
| 196 | + it("infers title from filename when --document-file is given a bare path", () => { |
| 197 | + const result = parseCLIArgs(["sync", "--document-file", "./CHANGELOG.md"]); |
| 198 | + expect(result.documents).toEqual([{ title: "CHANGELOG", source: { kind: "file", path: "./CHANGELOG.md" } }]); |
| 199 | + }); |
| 200 | + |
| 201 | + it("infers title from basename when --document-file path has nested directories", () => { |
| 202 | + const result = parseCLIArgs(["sync", "--document-file", "./docs/deploy-log.md"]); |
| 203 | + expect(result.documents).toEqual([ |
| 204 | + { title: "deploy-log", source: { kind: "file", path: "./docs/deploy-log.md" } }, |
| 205 | + ]); |
| 206 | + }); |
| 207 | + |
| 208 | + it("infers title from bare path with no extension", () => { |
| 209 | + const result = parseCLIArgs(["sync", "--document-file", "./NOTES"]); |
| 210 | + expect(result.documents).toEqual([{ title: "NOTES", source: { kind: "file", path: "./NOTES" } }]); |
| 211 | + }); |
| 212 | + |
| 213 | + it("strips only the final extension when inferring title", () => { |
| 214 | + const result = parseCLIArgs(["sync", "--document-file", "./release.notes.md"]); |
| 215 | + expect(result.documents).toEqual([ |
| 216 | + { title: "release.notes", source: { kind: "file", path: "./release.notes.md" } }, |
| 217 | + ]); |
| 218 | + }); |
| 219 | + |
| 220 | + it("throws when --document-file is bare '-' (stdin needs an explicit title)", () => { |
| 221 | + expect(() => parseCLIArgs(["sync", "--document-file", "-"])).toThrow("Title=-"); |
| 222 | + }); |
| 223 | + |
| 224 | + it("combines --document and --document-file", () => { |
| 225 | + const result = parseCLIArgs([ |
| 226 | + "sync", |
| 227 | + "--document", |
| 228 | + "Changelog=# v1.0.0", |
| 229 | + "--document-file", |
| 230 | + "Deploy log=./deploy.md", |
| 231 | + ]); |
| 232 | + expect(result.documents).toEqual([ |
| 233 | + { title: "Changelog", source: { kind: "inline", content: "# v1.0.0" } }, |
| 234 | + { title: "Deploy log", source: { kind: "file", path: "./deploy.md" } }, |
| 235 | + ]); |
| 236 | + }); |
| 237 | + |
| 238 | + it("throws on --document without =", () => { |
| 239 | + expect(() => parseCLIArgs(["sync", "--document", "no-separator"])).toThrow( |
| 240 | + 'Invalid --document value: "no-separator"', |
| 241 | + ); |
| 242 | + }); |
| 243 | + |
| 244 | + it("throws on --document with empty title", () => { |
| 245 | + expect(() => parseCLIArgs(["sync", "--document", "=content"])).toThrow("Document title must not be empty"); |
| 246 | + }); |
| 247 | + |
| 248 | + it("throws on --document with empty value", () => { |
| 249 | + expect(() => parseCLIArgs(["sync", "--document", "Title="])).toThrow("Document value must not be empty"); |
| 250 | + }); |
| 251 | + |
| 252 | + it("throws on --document-file with empty path", () => { |
| 253 | + expect(() => parseCLIArgs(["sync", "--document-file", "Title= "])).toThrow(); |
| 254 | + }); |
| 255 | + }); |
| 256 | + |
| 257 | + describe("--release-notes / --release-notes-file", () => { |
| 258 | + it("parses inline --release-notes", () => { |
| 259 | + const result = parseCLIArgs(["sync", "--release-notes", "## v1.0.0\n\nFirst release."]); |
| 260 | + expect(result.releaseNotes).toEqual({ |
| 261 | + source: { kind: "inline", content: "## v1.0.0\n\nFirst release." }, |
| 262 | + }); |
| 263 | + }); |
| 264 | + |
| 265 | + it("parses --release-notes-file", () => { |
| 266 | + const result = parseCLIArgs(["sync", "--release-notes-file", "./notes.md"]); |
| 267 | + expect(result.releaseNotes).toEqual({ source: { kind: "file", path: "./notes.md" } }); |
| 268 | + }); |
| 269 | + |
| 270 | + it("last-wins across multiple --release-notes occurrences", () => { |
| 271 | + const result = parseCLIArgs([ |
| 272 | + "sync", |
| 273 | + "--release-notes", |
| 274 | + "first", |
| 275 | + "--release-notes", |
| 276 | + "second", |
| 277 | + "--release-notes-file", |
| 278 | + "./notes.md", |
| 279 | + ]); |
| 280 | + expect(result.releaseNotes).toEqual({ source: { kind: "file", path: "./notes.md" } }); |
| 281 | + }); |
| 282 | + |
| 283 | + it("throws on empty --release-notes-file path", () => { |
| 284 | + expect(() => parseCLIArgs(["sync", "--release-notes-file", " "])).toThrow(); |
| 285 | + }); |
| 286 | + |
| 287 | + it("leaves releaseNotes undefined when no flag is passed", () => { |
| 288 | + const result = parseCLIArgs(["sync"]); |
| 289 | + expect(result.releaseNotes).toBeUndefined(); |
| 290 | + }); |
| 291 | + |
| 292 | + it("preserves argv order across --release-notes-file then --release-notes", () => { |
| 293 | + // Regression: previously the parser grouped values by flag name, so a later inline note |
| 294 | + // could be overridden by an earlier file note. Last on the command line should always win. |
| 295 | + const result = parseCLIArgs([ |
| 296 | + "sync", |
| 297 | + "--release-notes-file", |
| 298 | + "./generated.md", |
| 299 | + "--release-notes", |
| 300 | + "manual override", |
| 301 | + ]); |
| 302 | + expect(result.releaseNotes).toEqual({ source: { kind: "inline", content: "manual override" } }); |
| 303 | + }); |
| 304 | + |
| 305 | + it("preserves argv order across --release-notes then --release-notes-file", () => { |
| 306 | + const result = parseCLIArgs(["sync", "--release-notes", "manual", "--release-notes-file", "./final.md"]); |
| 307 | + expect(result.releaseNotes).toEqual({ source: { kind: "file", path: "./final.md" } }); |
| 308 | + }); |
| 309 | + }); |
| 310 | + |
| 311 | + describe("argv order across --document / --document-file", () => { |
| 312 | + it("preserves argv order so same-title last-wins works across flag types", () => { |
| 313 | + // The API upserts documents by title with later entries winning. The CLI must therefore send |
| 314 | + // documents in the order the user wrote them on the command line, not bucketed by flag type. |
| 315 | + const result = parseCLIArgs([ |
| 316 | + "sync", |
| 317 | + "--document-file", |
| 318 | + "Changelog=./from-file.md", |
| 319 | + "--document", |
| 320 | + "Changelog=inline override", |
| 321 | + ]); |
| 322 | + expect(result.documents).toEqual([ |
| 323 | + { title: "Changelog", source: { kind: "file", path: "./from-file.md" } }, |
| 324 | + { title: "Changelog", source: { kind: "inline", content: "inline override" } }, |
| 325 | + ]); |
| 326 | + }); |
| 327 | + |
| 328 | + it("interleaves inline and file documents in argv order", () => { |
| 329 | + const result = parseCLIArgs([ |
| 330 | + "sync", |
| 331 | + "--document", |
| 332 | + "A=inline-a", |
| 333 | + "--document-file", |
| 334 | + "B=./b.md", |
| 335 | + "--document", |
| 336 | + "C=inline-c", |
| 337 | + ]); |
| 338 | + expect(result.documents.map((d) => d.title)).toEqual(["A", "B", "C"]); |
| 339 | + }); |
165 | 340 | }); |
166 | 341 |
|
167 | | - it("returns no warning when --name is used with complete", () => { |
168 | | - const result = parseCLIArgs(["complete", "--name", "Release 1.2.0"]); |
169 | | - expect(getCLIWarnings(result)).toEqual([]); |
170 | | - }); |
171 | | - |
172 | | - it("returns no warning when --name is used with sync", () => { |
173 | | - const result = parseCLIArgs(["sync", "--name", "Release 1.2.0"]); |
174 | | - expect(getCLIWarnings(result)).toEqual([]); |
| 342 | + it("throws on unknown flags (strict mode)", () => { |
| 343 | + expect(() => parseCLIArgs(["--unknown-flag"])).toThrow(); |
175 | 344 | }); |
176 | 345 |
|
177 | 346 | it("defaults --timeout to 60 seconds", () => { |
|
0 commit comments