mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
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.
189 lines
8.1 KiB
TypeScript
189 lines
8.1 KiB
TypeScript
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, expect, it } from "vitest";
|
|
import { resolveChangedNodeTestTargets } from "../../scripts/lib/ci-changed-node-test-plan.mts";
|
|
import { resolveCiCheckFamilyScope } from "../../scripts/lib/ci-check-family-scope.mts";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
it.each(["added", "renamed", "deleted", "import-edge"])(
|
|
"retains non-import inventories and architecture for %s source modules",
|
|
(change) => {
|
|
const cwd = tempDirs.make("node-source-inventory-");
|
|
const source = "src/infra/new-module.ts";
|
|
const guards = [
|
|
"test/scripts/pr-wrapper-source-closure.test.ts",
|
|
"test/scripts/pr-worktree-provision.test.ts",
|
|
"test/scripts/eager-import-closure.test.ts",
|
|
"test/scripts/update-restart-module-outcome.test.ts",
|
|
"test/scripts/type-suppression-inventory.test.ts",
|
|
"test/scripts/plugin-sdk-surface-report.test.ts",
|
|
];
|
|
for (const file of [...guards, ...(change === "deleted" ? [] : [source])]) {
|
|
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
|
writeFileSync(path.join(cwd, file), "export {};\n");
|
|
}
|
|
if (change === "import-edge") {
|
|
writeFileSync(path.join(cwd, source), 'import "./dependency.js";\n');
|
|
writeFileSync(path.join(cwd, "src/infra/dependency.ts"), "export {};\n");
|
|
}
|
|
const paths = change === "renamed" ? ["src/infra/old-module.ts", source] : [source];
|
|
expect(resolveChangedNodeTestTargets(paths, { cwd, selectionMode: "aggressive" })).toEqual(
|
|
guards.toSorted(),
|
|
);
|
|
const testOnly = "src/infra/own.test.ts";
|
|
mkdirSync(path.dirname(path.join(cwd, testOnly)), { recursive: true });
|
|
writeFileSync(path.join(cwd, testOnly), "export {};\n");
|
|
expect(resolveChangedNodeTestTargets([testOnly], { cwd, selectionMode: "aggressive" })).toEqual(
|
|
[testOnly],
|
|
);
|
|
expect(resolveCiCheckFamilyScope(paths).additionalGroups).toContain(
|
|
"runtime-topology-architecture",
|
|
);
|
|
},
|
|
);
|
|
|
|
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-");
|
|
const source = "src/agents/example/subject.ts";
|
|
const direct = "src/agents/example/subject.test.ts";
|
|
const nearby = "src/infra/device-pairing.test.ts";
|
|
const distant = "src/infra/state-migrations.audit-logs.test.ts";
|
|
const unrelated = "src/agents/bash-tools.exec.path.test.ts";
|
|
const files = {
|
|
[source]: "export const value = 1;",
|
|
[direct]: 'import "./subject.js";',
|
|
"src/infra/bridge.ts": 'import "../agents/example/subject.js";',
|
|
[nearby]: 'import "./bridge.js";',
|
|
"src/infra/distant.ts": 'import "./bridge.js";',
|
|
[distant]: 'import "./distant.js";',
|
|
[unrelated]: "export {};",
|
|
};
|
|
for (const [file, content] of Object.entries(files)) {
|
|
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
|
writeFileSync(path.join(cwd, file), content);
|
|
}
|
|
const reasons: { rule: string; input: string; targets: string[] }[] = [];
|
|
const selected = resolveChangedNodeTestTargets([source], {
|
|
cwd,
|
|
selectionMode: "aggressive",
|
|
onSelection: (selection) => reasons.push(selection),
|
|
});
|
|
expect(selected).toEqual([direct, nearby]);
|
|
expect(
|
|
reasons.filter(({ rule }) => rule === "import-consumer").flatMap(({ targets }) => targets),
|
|
).toContain(nearby);
|
|
expect(new Set(reasons.flatMap(({ targets }) => targets))).toEqual(new Set(selected));
|
|
const full = resolveChangedNodeTestTargets([source], { cwd, selectionMode: "full" });
|
|
expect(full).toEqual([unrelated, direct, nearby, distant].toSorted());
|
|
expect(resolveChangedNodeTestTargets([source], { cwd })).toEqual(full);
|
|
expect(
|
|
resolveChangedNodeTestTargets([source, distant], { cwd, selectionMode: "aggressive" }),
|
|
).toContain(distant);
|
|
});
|
|
|
|
it.each([
|
|
["relative", 19],
|
|
["relative", 20],
|
|
["package", 19],
|
|
["package", 20],
|
|
["mixed", 19],
|
|
["mixed", 20],
|
|
] as const)("limits %s second-hop expansion at %i direct importers", (kind, count) => {
|
|
const cwd = tempDirs.make("node-hub-selection-");
|
|
const source = kind === "relative" ? "src/core/value.ts" : "packages/subject/src/value.ts";
|
|
const specifier = kind === "relative" ? "../core/value.js" : "@fixture/subject";
|
|
const direct = Array.from(
|
|
{ length: count - 1 },
|
|
(_, index) => `src/readers/direct-${index}.test.ts`,
|
|
);
|
|
const indirect = "src/consumers/indirect.test.ts";
|
|
const files: Record<string, string> = {
|
|
"packages/subject/package.json": JSON.stringify({
|
|
name: "@fixture/subject",
|
|
exports: { ".": "./src/value.ts" },
|
|
}),
|
|
[source]: "export const value = 1;",
|
|
"src/bridge/value.ts": `import "${specifier}";`,
|
|
[indirect]: 'import "../bridge/value.js";',
|
|
...Object.fromEntries(
|
|
direct.map((file, index) => [
|
|
file,
|
|
`import "${kind === "mixed" && index % 2 ? "../../packages/subject/src/value.js" : specifier}";`,
|
|
]),
|
|
),
|
|
};
|
|
for (const [file, content] of Object.entries(files)) {
|
|
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
|
writeFileSync(path.join(cwd, file), content);
|
|
}
|
|
const selected = resolveChangedNodeTestTargets([source], { cwd, selectionMode: "aggressive" });
|
|
expect(selected).toEqual([...direct, ...(count < 20 ? [indirect] : [])].toSorted());
|
|
expect(resolveChangedNodeTestTargets([source], { cwd, selectionMode: "full" })).toEqual(
|
|
expect.arrayContaining(direct),
|
|
);
|
|
expect(
|
|
resolveChangedNodeTestTargets([source, indirect], { cwd, selectionMode: "aggressive" }),
|
|
).toContain(indirect);
|
|
if (kind === "package") {
|
|
expect(
|
|
resolveChangedNodeTestTargets(["packages/subject/package.json"], {
|
|
cwd,
|
|
selectionMode: "aggressive",
|
|
}),
|
|
).toEqual(direct.toSorted());
|
|
}
|
|
});
|
|
|
|
it.each([30, 31])(
|
|
"uses stems instead of broad directory ownership at %i adjacent tests",
|
|
(count) => {
|
|
const cwd = tempDirs.make("node-directory-selection-");
|
|
const source = "src/infra/foo.ts";
|
|
const matching = [
|
|
"src/infra/foo.test.ts",
|
|
"src/infra/foo.extra.test.ts",
|
|
"src/infra/foo-more.test.ts",
|
|
];
|
|
const unrelated = Array.from(
|
|
{ length: count - matching.length },
|
|
(_, index) => `src/infra/unrelated-${index}.test.ts`,
|
|
);
|
|
const nested = "src/infra/nested/other.test.ts";
|
|
for (const file of [source, ...matching, ...unrelated, nested]) {
|
|
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
|
writeFileSync(path.join(cwd, file), "export {};\n");
|
|
}
|
|
const selected = resolveChangedNodeTestTargets([source], { cwd, selectionMode: "aggressive" });
|
|
expect(selected).toEqual((count <= 30 ? [...matching, ...unrelated] : matching).toSorted());
|
|
expect(selected).not.toContain(nested);
|
|
expect(
|
|
resolveChangedNodeTestTargets([source, unrelated[0]!], { cwd, selectionMode: "aggressive" }),
|
|
).toContain(unrelated[0]);
|
|
},
|
|
);
|