152 lines
5.5 KiB
JavaScript
152 lines
5.5 KiB
JavaScript
import { matchesGlob } from "node:path";
|
|
|
|
// Candidate ownership only: these leaf adapters and their producer tests can be
|
|
// compared with full-suite outcomes. This table never changes test execution.
|
|
// Shared application/domain code and dynamic CLI/MCP consumers are not leaf owners.
|
|
const OWNERS = {
|
|
adb: ["src/adb/**", "tests/boundary/providers/adb/**"],
|
|
apktool: ["src/apktool/**"],
|
|
dotnet: ["src/dotnet/**"],
|
|
evm: ["src/evm/**", "tests/boundary/evm/**"],
|
|
firmware: ["src/firmware/**", "tests/boundary/firmware/**"],
|
|
flutter: ["src/flutter/**"],
|
|
ghidra: ["src/ghidra/**", "tests/boundary/providers/ghidra/**"],
|
|
hopper: ["src/hopper/**", "tests/boundary/providers/hopper/**"],
|
|
ida: ["src/ida/**", "tests/boundary/providers/ida/**"],
|
|
jeb: ["src/jeb/**"],
|
|
pcap: ["src/pcap/**", "tests/boundary/pcap/**"],
|
|
sqlite: ["src/sqlite/**", "tests/boundary/sqlite/**"],
|
|
wasm: ["src/wasm/**"],
|
|
};
|
|
const ownersOf = (path) =>
|
|
Object.entries(OWNERS)
|
|
.filter(([, patterns]) => patterns.some((glob) => matchesGlob(path, glob)))
|
|
.map(([owner]) => owner);
|
|
const validPath = (path) =>
|
|
typeof path === "string" &&
|
|
/^(src|tests)\//.test(path) &&
|
|
!/[\\\0\r\n]/.test(path) &&
|
|
!path.split("/").some((part) => ["", ".", ".."].includes(part));
|
|
const full = (reason, changedPaths = []) => ({
|
|
version: 1,
|
|
mode: "full",
|
|
owners: [],
|
|
changedTests: [],
|
|
changedPaths,
|
|
reason,
|
|
});
|
|
|
|
/** Plan an observational PR subset from the classifier's existing Git diff. */
|
|
export function planTestSelection(files, event, tier) {
|
|
if (event !== "pull_request" || tier !== "fast")
|
|
return full("Only ordinary PRs are candidates", files);
|
|
if (!files.length) return full("No changed paths available");
|
|
const owners = new Set();
|
|
const changedTests = [];
|
|
for (const path of files) {
|
|
if (!validPath(path))
|
|
return full(`Shared or unknown input: ${path}`, files);
|
|
const matches = ownersOf(path);
|
|
if (!matches.length) return full(`Shared or unknown input: ${path}`, files);
|
|
for (const owner of matches) owners.add(owner);
|
|
if (path.endsWith(".test.ts")) changedTests.push(path);
|
|
}
|
|
return {
|
|
version: 1,
|
|
mode: "candidate",
|
|
owners: [...owners].sort(),
|
|
changedTests: [...new Set(changedTests)].sort(),
|
|
changedPaths: files,
|
|
reason:
|
|
"Observe affected leaf owners; retain integration and unowned tests",
|
|
};
|
|
}
|
|
|
|
/** Compare a proposal with the full run; missing observations never prove parity. */
|
|
export function compareTestSelection(plan, inventory, results) {
|
|
if (
|
|
plan?.version !== 1 ||
|
|
!["full", "candidate"].includes(plan.mode) ||
|
|
!Array.isArray(plan.owners) ||
|
|
plan.owners.some((owner) => !Object.hasOwn(OWNERS, owner)) ||
|
|
!Array.isArray(plan.changedTests) ||
|
|
!Array.isArray(plan.changedPaths) ||
|
|
plan.changedPaths.some((path) => typeof path !== "string") ||
|
|
plan.changedTests.some(
|
|
(path) => !validPath(path) || !path.endsWith(".test.ts"),
|
|
) ||
|
|
typeof plan.reason !== "string" ||
|
|
(plan.mode === "candidate" && !plan.owners.length) ||
|
|
(plan.mode === "full" && (plan.owners.length || plan.changedTests.length))
|
|
)
|
|
throw new Error("Invalid observational test selection plan");
|
|
const expected = new Set(inventory);
|
|
if (!expected.size || expected.size !== inventory.length)
|
|
throw new Error("Test selection requires a nonempty unique inventory");
|
|
const ownersByFile = new Map(
|
|
plan.mode === "candidate"
|
|
? inventory.map((path) => [path, ownersOf(path)])
|
|
: [],
|
|
);
|
|
// Removed/renamed tests have no current executable identity. Keep all tests.
|
|
const removed = plan.changedTests.filter((path) => !expected.has(path));
|
|
const missingOwners = plan.owners.filter(
|
|
(owner) =>
|
|
![...ownersByFile.values()].some((owners) => owners.includes(owner)),
|
|
);
|
|
const effective = removed.length
|
|
? full(
|
|
`Changed tests absent from current inventory: ${removed.join(", ")}`,
|
|
plan.changedPaths,
|
|
)
|
|
: missingOwners.length
|
|
? full(
|
|
`Owners without executable tests: ${missingOwners.join(", ")}`,
|
|
plan.changedPaths,
|
|
)
|
|
: plan;
|
|
const selected = [];
|
|
const omitted = [];
|
|
for (const path of inventory.toSorted()) {
|
|
const owners = ownersByFile.get(path) ?? [];
|
|
const keep =
|
|
effective.mode === "full" ||
|
|
!owners.length ||
|
|
effective.changedTests.includes(path) ||
|
|
owners.some((owner) => effective.owners.includes(owner));
|
|
(keep ? selected : omitted).push(path);
|
|
}
|
|
const actual = new Set(results.map((file) => file.name));
|
|
const missing = inventory.filter((path) => !actual.has(path));
|
|
const unexpected = [...actual].filter((path) => !expected.has(path));
|
|
const duplicateFiles = results.length - actual.size;
|
|
const unobserved = results
|
|
.filter((file) => !["passed", "failed"].includes(file.state))
|
|
.map((file) => file.name);
|
|
const omittedSet = new Set(omitted);
|
|
const missedFailures = results
|
|
.filter((file) => omittedSet.has(file.name) && file.state === "failed")
|
|
.map((file) => file.name);
|
|
return {
|
|
version: 1,
|
|
observational: true,
|
|
plan: effective,
|
|
selected,
|
|
omitted,
|
|
missing,
|
|
unexpected,
|
|
duplicateFiles,
|
|
unobserved,
|
|
missedFailures,
|
|
complete:
|
|
!missing.length &&
|
|
!unexpected.length &&
|
|
!duplicateFiles &&
|
|
!unobserved.length,
|
|
// Phase totals are scheduling weights, not an estimate of wall-time savings.
|
|
omittedDurationMs: results
|
|
.filter((file) => omittedSet.has(file.name))
|
|
.reduce((sum, file) => sum + file.durationMs, 0),
|
|
};
|
|
}
|