import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; const { detectChangedScope, listChangedPaths } = (await import("../../scripts/ci-changed-scope.mjs")) as unknown as { detectChangedScope: (paths: string[]) => { runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; }; listChangedPaths: (base: string, head?: string) => string[]; }; const markerPaths: string[] = []; afterEach(() => { for (const markerPath of markerPaths) { try { fs.unlinkSync(markerPath); } catch {} } markerPaths.length = 0; }); describe("detectChangedScope", () => { it("fails safe when no paths are provided", () => { expect(detectChangedScope([])).toEqual({ runNode: true, runMacos: true, runAndroid: true, runWindows: true, }); }); it("keeps all lanes off for docs-only changes", () => { expect(detectChangedScope(["docs/ci.md", "README.md"])).toEqual({ runNode: false, runMacos: false, runAndroid: false, runWindows: false, }); }); it("enables node lane for node-relevant files", () => { expect(detectChangedScope(["src/plugins/runtime/index.ts"])).toEqual({ runNode: true, runMacos: false, runAndroid: false, runWindows: true, }); }); it("keeps node lane off for native-only changes", () => { expect(detectChangedScope(["apps/macos/Sources/Foo.swift"])).toEqual({ runNode: false, runMacos: true, runAndroid: false, runWindows: false, }); expect(detectChangedScope(["apps/shared/OpenClawKit/Sources/Foo.swift"])).toEqual({ runNode: false, runMacos: true, runAndroid: true, runWindows: false, }); }); it("does not force macOS for generated protocol model-only changes", () => { expect(detectChangedScope(["apps/macos/Sources/OpenClawProtocol/GatewayModels.swift"])).toEqual( { runNode: false, runMacos: false, runAndroid: false, runWindows: false, }, ); }); it("enables node lane for non-native non-doc files by fallback", () => { expect(detectChangedScope(["README.md"])).toEqual({ runNode: false, runMacos: false, runAndroid: false, runWindows: false, }); expect(detectChangedScope(["assets/icon.png"])).toEqual({ runNode: true, runMacos: false, runAndroid: false, runWindows: false, }); }); it("keeps windows lane off for non-runtime GitHub metadata files", () => { expect(detectChangedScope([".github/labeler.yml"])).toEqual({ runNode: true, runMacos: false, runAndroid: false, runWindows: false, }); }); it("treats base and head as literal git args", () => { const markerPath = path.join( os.tmpdir(), `openclaw-ci-changed-scope-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`, ); markerPaths.push(markerPath); const injectedBase = process.platform === "win32" ? `HEAD & echo injected > "${markerPath}" & rem` : `HEAD; touch "${markerPath}" #`; expect(() => listChangedPaths(injectedBase, "HEAD")).toThrow(); expect(fs.existsSync(markerPath)).toBe(false); }); });