From 4db7d6a90abc1d7f74af813da05e45c0765c3aff Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 20 Jun 2026 03:24:44 +0200 Subject: [PATCH] fix(test): guard platform version cli values --- scripts/android-version.ts | 78 ++++++++++++++++++--------- scripts/ios-version.ts | 80 ++++++++++++++++++---------- test/scripts/android-version.test.ts | 42 +++++++++++++++ test/scripts/ios-version.test.ts | 42 +++++++++++++++ 4 files changed, 189 insertions(+), 53 deletions(-) diff --git a/scripts/android-version.ts b/scripts/android-version.ts index 255a451bfead..abf3beb2a26a 100644 --- a/scripts/android-version.ts +++ b/scripts/android-version.ts @@ -5,19 +5,21 @@ import { resolveAndroidVersion } from "./lib/android-version.ts"; type CliOptions = { field: string | null; format: "json" | "shell"; + help: boolean; rootDir: string; }; function parseArgs(argv: string[]): CliOptions { let field: string | null = null; let format: "json" | "shell" = "json"; + let help = false; let rootDir = path.resolve("."); for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; switch (arg) { case "--field": { - field = argv[index + 1] ?? null; + field = readOptionValue(argv, index, "--field"); index += 1; break; } @@ -30,20 +32,15 @@ function parseArgs(argv: string[]): CliOptions { break; } case "--root": { - const value = argv[index + 1]; - if (!value) { - throw new Error("Missing value for --root."); - } + const value = readOptionValue(argv, index, "--root"); rootDir = path.resolve(value); index += 1; break; } case "-h": case "--help": { - console.log( - `Usage: node --import tsx scripts/android-version.ts [--json|--shell] [--field name] [--root dir]\n`, - ); - process.exit(0); + help = true; + break; } default: { throw new Error(`Unknown argument: ${arg}`); @@ -51,28 +48,57 @@ function parseArgs(argv: string[]): CliOptions { } } - return { field, format, rootDir }; + return { field, format, help, rootDir }; } -const options = parseArgs(process.argv.slice(2)); -const version = resolveAndroidVersion(options.rootDir); - -if (options.field) { - const value = version[options.field as keyof typeof version]; - if (value === undefined) { - throw new Error(`Unknown Android version field '${options.field}'.`); +function readOptionValue(argv: string[], index: number, flag: string): string { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing value for ${flag}.`); } - process.stdout.write(`${value}\n`); - process.exit(0); + return value; } -if (options.format === "shell") { +function printUsage(): void { process.stdout.write( - [ - `OPENCLAW_ANDROID_VERSION_NAME=${version.canonicalVersion}`, - `OPENCLAW_ANDROID_VERSION_CODE=${version.versionCode}`, - ].join("\n") + "\n", + "Usage: node --import tsx scripts/android-version.ts [--json|--shell] [--field name] [--root dir]\n\n", ); -} else { - process.stdout.write(`${JSON.stringify(version, null, 2)}\n`); +} + +function main(argv = process.argv.slice(2)): number { + const options = parseArgs(argv); + if (options.help) { + printUsage(); + return 0; + } + + const version = resolveAndroidVersion(options.rootDir); + + if (options.field) { + const value = version[options.field as keyof typeof version]; + if (value === undefined) { + throw new Error(`Unknown Android version field '${options.field}'.`); + } + process.stdout.write(`${value}\n`); + return 0; + } + + if (options.format === "shell") { + process.stdout.write( + [ + `OPENCLAW_ANDROID_VERSION_NAME=${version.canonicalVersion}`, + `OPENCLAW_ANDROID_VERSION_CODE=${version.versionCode}`, + ].join("\n") + "\n", + ); + } else { + process.stdout.write(`${JSON.stringify(version, null, 2)}\n`); + } + return 0; +} + +try { + process.exitCode = main(); +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; } diff --git a/scripts/ios-version.ts b/scripts/ios-version.ts index 5427eb7075d2..231fa57d88fb 100644 --- a/scripts/ios-version.ts +++ b/scripts/ios-version.ts @@ -5,19 +5,21 @@ import { resolveIosVersion } from "./lib/ios-version.ts"; type CliOptions = { field: string | null; format: "json" | "shell"; + help: boolean; rootDir: string; }; function parseArgs(argv: string[]): CliOptions { let field: string | null = null; let format: "json" | "shell" = "json"; + let help = false; let rootDir = path.resolve("."); for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; switch (arg) { case "--field": { - field = argv[index + 1] ?? null; + field = readOptionValue(argv, index, "--field"); index += 1; break; } @@ -30,20 +32,15 @@ function parseArgs(argv: string[]): CliOptions { break; } case "--root": { - const value = argv[index + 1]; - if (!value) { - throw new Error("Missing value for --root."); - } + const value = readOptionValue(argv, index, "--root"); rootDir = path.resolve(value); index += 1; break; } case "-h": case "--help": { - console.log( - `Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--root dir]\n`, - ); - process.exit(0); + help = true; + break; } default: { throw new Error(`Unknown argument: ${arg}`); @@ -51,29 +48,58 @@ function parseArgs(argv: string[]): CliOptions { } } - return { field, format, rootDir }; + return { field, format, help, rootDir }; } -const options = parseArgs(process.argv.slice(2)); -const version = resolveIosVersion(options.rootDir); - -if (options.field) { - const value = version[options.field as keyof typeof version]; - if (value === undefined) { - throw new Error(`Unknown iOS version field '${options.field}'.`); +function readOptionValue(argv: string[], index: number, flag: string): string { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing value for ${flag}.`); } - process.stdout.write(`${value}\n`); - process.exit(0); + return value; } -if (options.format === "shell") { +function printUsage(): void { process.stdout.write( - [ - `OPENCLAW_IOS_VERSION=${version.canonicalVersion}`, - `OPENCLAW_MARKETING_VERSION=${version.marketingVersion}`, - `OPENCLAW_BUILD_VERSION=${version.buildVersion}`, - ].join("\n") + "\n", + "Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--root dir]\n\n", ); -} else { - process.stdout.write(`${JSON.stringify(version, null, 2)}\n`); +} + +function main(argv = process.argv.slice(2)): number { + const options = parseArgs(argv); + if (options.help) { + printUsage(); + return 0; + } + + const version = resolveIosVersion(options.rootDir); + + if (options.field) { + const value = version[options.field as keyof typeof version]; + if (value === undefined) { + throw new Error(`Unknown iOS version field '${options.field}'.`); + } + process.stdout.write(`${value}\n`); + return 0; + } + + if (options.format === "shell") { + process.stdout.write( + [ + `OPENCLAW_IOS_VERSION=${version.canonicalVersion}`, + `OPENCLAW_MARKETING_VERSION=${version.marketingVersion}`, + `OPENCLAW_BUILD_VERSION=${version.buildVersion}`, + ].join("\n") + "\n", + ); + } else { + process.stdout.write(`${JSON.stringify(version, null, 2)}\n`); + } + return 0; +} + +try { + process.exitCode = main(); +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; } diff --git a/test/scripts/android-version.test.ts b/test/scripts/android-version.test.ts index 165e6bb7c098..eb454456b9de 100644 --- a/test/scripts/android-version.test.ts +++ b/test/scripts/android-version.test.ts @@ -1,4 +1,5 @@ // Android Version tests cover android version script behavior. +import { spawnSync } from "node:child_process"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { @@ -20,6 +21,47 @@ import { installAndroidFixtureCleanup(); describe("resolveAndroidVersion", () => { + it("rejects missing CLI option values before reading version files", () => { + const result = spawnSync( + process.execPath, + ["--import", "tsx", "scripts/android-version.ts", "--field"], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(1); + expect(result.stderr).toBe("Missing value for --field.\n"); + }); + + it("prints selected fields from the CLI", () => { + const rootDir = writeAndroidFixture({ + version: "2026.6.2", + versionCode: 2026060201, + }); + const result = spawnSync( + process.execPath, + [ + "--import", + "tsx", + "scripts/android-version.ts", + "--root", + rootDir, + "--field", + "canonicalVersion", + ], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("2026.6.2\n"); + expect(result.stderr).toBe(""); + }); + it("parses pinned release versions and Android version codes", () => { const rootDir = writeAndroidFixture({ version: "2026.6.2", diff --git a/test/scripts/ios-version.test.ts b/test/scripts/ios-version.test.ts index 07415f60d32b..ebb5835aeb16 100644 --- a/test/scripts/ios-version.test.ts +++ b/test/scripts/ios-version.test.ts @@ -1,4 +1,5 @@ // Ios Version tests cover ios version script behavior. +import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -16,6 +17,47 @@ import { installIosFixtureCleanup, writeIosFixture } from "./ios-version.test-su installIosFixtureCleanup(); describe("resolveIosVersion", () => { + it("rejects missing CLI option values before reading version files", () => { + const result = spawnSync( + process.execPath, + ["--import", "tsx", "scripts/ios-version.ts", "--field"], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(1); + expect(result.stderr).toBe("Missing value for --field.\n"); + }); + + it("prints selected fields from the CLI", () => { + const rootDir = writeIosFixture({ + version: "2026.4.6", + changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.6\n\nStable notes.\n", + }); + const result = spawnSync( + process.execPath, + [ + "--import", + "tsx", + "scripts/ios-version.ts", + "--root", + rootDir, + "--field", + "canonicalVersion", + ], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("2026.4.6\n"); + expect(result.stderr).toBe(""); + }); + it("parses pinned release versions and derives Apple marketing fields", () => { const rootDir = writeIosFixture({ version: "2026.4.6",