| 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; |
| runSkillsPython: 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, |
| runSkillsPython: 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, |
| runSkillsPython: 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, |
| runSkillsPython: false, |
| }); |
| }); |
|
|
| 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, |
| runSkillsPython: false, |
| }); |
| expect(detectChangedScope(["apps/shared/OpenClawKit/Sources/Foo.swift"])).toEqual({ |
| runNode: false, |
| runMacos: true, |
| runAndroid: true, |
| runWindows: false, |
| runSkillsPython: 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, |
| runSkillsPython: 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, |
| runSkillsPython: false, |
| }); |
|
|
| expect(detectChangedScope(["assets/icon.png"])).toEqual({ |
| runNode: true, |
| runMacos: false, |
| runAndroid: false, |
| runWindows: false, |
| runSkillsPython: 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, |
| runSkillsPython: false, |
| }); |
| }); |
|
|
| it("runs Python skill tests when skills change", () => { |
| expect(detectChangedScope(["skills/openai-image-gen/scripts/test_gen.py"])).toEqual({ |
| runNode: true, |
| runMacos: false, |
| runAndroid: false, |
| runWindows: false, |
| runSkillsPython: true, |
| }); |
| }); |
|
|
| it("runs platform lanes when the CI workflow changes", () => { |
| expect(detectChangedScope([".github/workflows/ci.yml"])).toEqual({ |
| runNode: true, |
| runMacos: true, |
| runAndroid: true, |
| runWindows: true, |
| runSkillsPython: true, |
| }); |
| }); |
|
|
| 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); |
| }); |
| }); |
|
|