From 5ab66a5fe469b81007256f81c14812a77dc0b580 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Oct 2026 09:55:09 -0700 Subject: [PATCH] fix(ci): run Telegram skill tests when a PR changes only skill test files (#163328) The Telegram userbot skill's node:test (*.test.mjs) and Python (*.test.py) files are not Vitest-routable, so aggressive PR selection drops them as direct targets. Their only CI owner is the Vitest wrapper test/scripts/telegram-e2e-userbot-skill.test.ts, which the policy watch selected for 20 named non-test scripts only. Test-only skill PRs (e.g. #162968) and later scripts (reply-policy-*, telegram-runtime) planned nothing in either selection mode. Watch the skill's whole scripts/ directory, the agents/openai.yaml the wrapper parses, and the fixture it loads by URL. The wrapper stays in the release-only core-tooling family; it now also runs on PRs that touch the skill, the only PRs that can break these self-contained scripts. --- docs/ci/scope-and-routing/node-test-lanes.md | 5 ++- scripts/lib/ci-policy-test-watch.mts | 25 +++---------- ...i-changed-node-test-plan.selection.test.ts | 36 ++++++++++++------- test/scripts/ci-node-test-plan.test.ts | 28 ++++++++++++++- 4 files changed, 59 insertions(+), 35 deletions(-) diff --git a/docs/ci/scope-and-routing/node-test-lanes.md b/docs/ci/scope-and-routing/node-test-lanes.md index 3c4281083b60..6f21af0ae6d3 100644 --- a/docs/ci/scope-and-routing/node-test-lanes.md +++ b/docs/ci/scope-and-routing/node-test-lanes.md @@ -125,7 +125,10 @@ and scripts that the import graph cannot discover. Explicit policy watches retain their matching tests in PR CI even when the broader tooling or runtime suite is deferred. This includes wrapper dependency checks, Gateway client callsite scans, and upgrade-survivor package checks. Unrelated -deferred tests stay excluded. +deferred tests stay excluded. Changed test files that no Vitest config routes, +such as skill `node:test` and Python suites, are never direct targets; the watch +for their Vitest wrapper must cover those test files too, or test-only edits run +in no PR job. Aggressive PR selection keeps changed tests and direct runtime import consumers, including package and SDK aliases. A changed module with fewer than 20 direct diff --git a/scripts/lib/ci-policy-test-watch.mts b/scripts/lib/ci-policy-test-watch.mts index 31125b7f3da1..99e5d75cc124 100644 --- a/scripts/lib/ci-policy-test-watch.mts +++ b/scripts/lib/ci-policy-test-watch.mts @@ -1966,28 +1966,13 @@ const policyTestWatches: readonly PolicyTestWatch[] = [ ], }, { + // Sole CI owner of the skill's node:test and Python suites: no Vitest config + // routes those files, so test-only skill edits must select this wrapper. testFile: "test/scripts/telegram-e2e-userbot-skill.test.ts", watchGlobs: [ - ".agents/skills/telegram-e2e-userbot/scripts/followup-drain-control-preload.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/published-upgrade-artifact.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/published-upgrade-scenario.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/qa-credential-lease.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/run-mock-sut-user-e2e.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/run-published-upgrade-user-e2e.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/scenario.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-api-ignore-abort-preload.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-binding-checkpoint.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-binding-forum.py", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-binding-upgrade-verdict.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-run-scope.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-test-api-proxy.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-test-credential.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-test-doctor.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-test-group.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/telegram-test-recover.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/triage-mock-openai.mjs", - ".agents/skills/telegram-e2e-userbot/scripts/user-driver.py", - ".agents/skills/telegram-e2e-userbot/scripts/user-record.py", + ".agents/skills/telegram-e2e-userbot/agents/openai.yaml", + ".agents/skills/telegram-e2e-userbot/scripts/**", + "test/scripts/fixtures/triage-fixture-startup.mjs", ], }, { diff --git a/test/scripts/ci-changed-node-test-plan.selection.test.ts b/test/scripts/ci-changed-node-test-plan.selection.test.ts index 948f387ac566..ecf069d0da83 100644 --- a/test/scripts/ci-changed-node-test-plan.selection.test.ts +++ b/test/scripts/ci-changed-node-test-plan.selection.test.ts @@ -44,19 +44,29 @@ it.each(["added", "renamed", "deleted", "import-edge"])( }, ); -it("selects changed test files directly only on routable Vitest surfaces", () => { - const cwd = tempDirs.make("node-selection-routable-"); - // Skill scripts carry node:test files that no Vitest config owns. - const skillTest = ".agents/skills/example/scripts/driver.test.mjs"; - const routable = "src/infra/own.test.ts"; - for (const file of [skillTest, routable]) { - mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true }); - writeFileSync(path.join(cwd, file), "export {};\n"); - } - expect( - resolveChangedNodeTestTargets([skillTest, routable], { cwd, selectionMode: "aggressive" }), - ).toEqual([routable]); -}); +it.each(["aggressive", "full"] as const)( + "routes skill node:test and Python files through their Vitest owner in %s mode", + (selectionMode) => { + const cwd = tempDirs.make("node-selection-routable-"); + // Skill scripts carry node:test and Python files that no Vitest config owns. + const unownedSkillTest = ".agents/skills/example/scripts/driver.test.mjs"; + const skillTest = ".agents/skills/telegram-e2e-userbot/scripts/driver.test.mjs"; + const skillPythonTest = ".agents/skills/telegram-e2e-userbot/scripts/driver.test.py"; + const wrapper = "test/scripts/telegram-e2e-userbot-skill.test.ts"; + const routable = "src/infra/own.test.ts"; + for (const file of [unownedSkillTest, skillTest, skillPythonTest, wrapper, routable]) { + mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true }); + writeFileSync(path.join(cwd, file), "export {};\n"); + } + expect(resolveChangedNodeTestTargets([skillTest], { cwd, selectionMode })).toEqual([wrapper]); + expect( + resolveChangedNodeTestTargets([unownedSkillTest, skillPythonTest, routable], { + cwd, + selectionMode, + }), + ).toEqual([routable, wrapper]); + }, +); it("bounds protected regressions to nearby consumers and restores area coverage in full mode", () => { const cwd = tempDirs.make("node-selection-"); diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 67ee0a290b4a..bb2b120ba1cb 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join, matchesGlob } from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; @@ -2055,6 +2055,32 @@ describe("scripts/lib/ci-node-test-plan.mts", () => { } }); + it("runs Telegram skill script changes, including test-only edits, through the skill wrapper", () => { + const wrapper = "test/scripts/telegram-e2e-userbot-skill.test.ts"; + const scriptsDir = ".agents/skills/telegram-e2e-userbot/scripts"; + const scripts = readdirSync(scriptsDir, { withFileTypes: true }) + .filter((entry) => entry.isFile() && !entry.name.startsWith(".")) + .map((entry) => `${scriptsDir}/${entry.name}`); + expect(scripts.filter((file) => /\.test\.(?:mjs|py)$/u.test(file)).length).toBeGreaterThan(0); + for (const changedPath of scripts) { + expect(resolvePolicyTestTargets([changedPath]), changedPath).toContain(wrapper); + } + const changedTest = `${scriptsDir}/telegram-run-composition.test.mjs`; + const shards = expectDefined( + createChangedNodeTestShards([changedTest], { selectionMode: "aggressive" }), + "skill test plan", + ); + const groups = shards.flatMap((shard) => shard.groups ?? []); + const selected = [ + ...shards.flatMap((shard) => shard.targets ?? []), + ...groups.flatMap((group) => group.includePatterns ?? []), + ]; + expect(selected).not.toContain(changedTest); + const owners = groups.filter((group) => group.includePatterns?.includes(wrapper)); + expect(owners).toHaveLength(1); + expect(owners[0]?.configs).toEqual(["test/vitest/vitest.tooling.config.ts"]); + }); + it("matches policy owners with literal and native glob semantics", () => { const changedPath = "ui/src/styles/base.css"; expect(isPolicyTestOwnedPath(changedPath)).toBe(true);