mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-31 20:47:00 +00:00
* chore: update dependencies with a seven-day cooldown Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 85123e9c-a543-48e3-a29c-9b01ea5ef83f * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 74b57fb5-59b0-4d2f-8884-b509c249838e * fix: repair dependency qualification gates Join pending plugin-state writes during native guard preparation. Update ingress and cancellation fixtures at their actual owning boundaries and provision the Watch runtime before screenshots. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix: qualify dependency update runtime and tooling paths Join database-wide pending publication before preparing native authority; retain execution settlement independently of terminal presentation. Defer interaction-only chat dialogs, exercise the actual formatter wrapper, and provision usable Watch runtimes without weakening gates. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 8642734d-2859-4f3c-a70e-7a4d2f5a1ea1 * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 6147753b-a480-4d04-8882-6d9b51006f47 * fix: prepare monitor authority and verify Watch runtime readiness Rejoin plugin-state publication after awaited Codex monitor preparation while preserving the original synchronous authority guard. Require Watch runtime bootability before accepting cached simulator metadata. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 02d702e3-4030-496e-abc5-c818d4aec7a0 * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 3e069942-c5c9-4059-9fe0-bae78e34b6e6 * test: await lazy attachment previews in consumer fixtures Preserve attachment behavior assertions while waiting for their async preview boundary. Control the cold read until its pending state is visible. Extract preview cases from the over-cap chat-view test without removing coverage. Include the canonical root-help mock-export repair frome63269c28a. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: d8af6943-60b3-445b-8fa5-536dfd300cc8 * test(codex): isolate deferred hooks and handshake clocks Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * Publish openclaw/update-all-dependencies-with-a-seven-day-cool-off Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 18e051ba-09cc-4fa2-87b7-49eda74ac61b * test(ci): keep the capacity-plan timings mock export-preserving (cherry picked from commitb791dd0664) * test(ui): align model defaults help expectations --------- Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
358 lines
10 KiB
JavaScript
358 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import * as ts from "typescript/ast";
|
|
import { isCodeFile, isTestRelatedFile, listRepoFilesSync } from "./check-file-utils.js";
|
|
import { renderFindingGroups } from "./lib/grouped-findings.js";
|
|
import { createNativeTypeScriptParser } from "./lib/native-typescript.mts";
|
|
import { parseInventoryReportCliArgs } from "./lib/report-cli-helpers.mts";
|
|
|
|
type SkipInventoryKind = "alias" | "call";
|
|
type SkipInventoryReason =
|
|
| "conditional-skip"
|
|
| "explicit-skip"
|
|
| "focused-only"
|
|
| "live-gate"
|
|
| "optional-dependency"
|
|
| "platform-gate"
|
|
| "todo";
|
|
|
|
type SkipInventoryTarget = "describe" | "it" | "test" | "unknown";
|
|
|
|
type TestSkipInventoryFinding = {
|
|
excerpt: string;
|
|
file: string;
|
|
kind: SkipInventoryKind;
|
|
line: number;
|
|
method: "only" | "runIf" | "skip" | "skipIf" | "todo";
|
|
reason: SkipInventoryReason;
|
|
target: SkipInventoryTarget;
|
|
};
|
|
|
|
export type TestSkipInventoryReport = {
|
|
findings: TestSkipInventoryFinding[];
|
|
scannedFileCount: number;
|
|
schemaVersion: 1;
|
|
summary: {
|
|
findingCount: number;
|
|
reasonCounts: Record<SkipInventoryReason, number>;
|
|
scannedFileCount: number;
|
|
touchedFileCount: number;
|
|
};
|
|
};
|
|
|
|
const EMPTY_REASON_COUNTS: Record<SkipInventoryReason, number> = {
|
|
"conditional-skip": 0,
|
|
"explicit-skip": 0,
|
|
"focused-only": 0,
|
|
"live-gate": 0,
|
|
"optional-dependency": 0,
|
|
"platform-gate": 0,
|
|
todo: 0,
|
|
};
|
|
const SKIP_METHODS = new Set(["only", "runIf", "skip", "skipIf", "todo"]);
|
|
const TEST_TARGETS = new Set(["describe", "it", "test"]);
|
|
const TRANSPARENT_CHAIN_METHODS = new Set(["concurrent", "each", "sequential"]);
|
|
|
|
function targetFromExpression(expression: ts.Expression): SkipInventoryTarget {
|
|
if (ts.isIdentifier(expression) && TEST_TARGETS.has(expression.text)) {
|
|
return expression.text as SkipInventoryTarget;
|
|
}
|
|
if (
|
|
ts.isPropertyAccessExpression(expression) &&
|
|
TRANSPARENT_CHAIN_METHODS.has(expression.name.text)
|
|
) {
|
|
return targetFromExpression(expression.expression);
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
function propertyAccessSkipMethod(
|
|
expression: ts.Expression,
|
|
): { method: TestSkipInventoryFinding["method"]; target: SkipInventoryTarget } | null {
|
|
if (!ts.isPropertyAccessExpression(expression) || !SKIP_METHODS.has(expression.name.text)) {
|
|
return null;
|
|
}
|
|
const target = targetFromExpression(expression.expression);
|
|
if (target === "unknown") {
|
|
return null;
|
|
}
|
|
return {
|
|
method: expression.name.text as TestSkipInventoryFinding["method"],
|
|
target,
|
|
};
|
|
}
|
|
|
|
function skipMethodFromExpression(
|
|
expression: ts.Expression,
|
|
): { method: TestSkipInventoryFinding["method"]; target: SkipInventoryTarget } | null {
|
|
if (ts.isParenthesizedExpression(expression)) {
|
|
return skipMethodFromExpression(expression.expression);
|
|
}
|
|
|
|
const directMethod = propertyAccessSkipMethod(expression);
|
|
if (directMethod) {
|
|
return directMethod;
|
|
}
|
|
|
|
if (ts.isConditionalExpression(expression)) {
|
|
return (
|
|
skipMethodFromExpression(expression.whenTrue) ??
|
|
skipMethodFromExpression(expression.whenFalse)
|
|
);
|
|
}
|
|
if (ts.isCallExpression(expression)) {
|
|
return skipMethodFromExpression(expression.expression);
|
|
}
|
|
if (
|
|
ts.isPropertyAccessExpression(expression) &&
|
|
TRANSPARENT_CHAIN_METHODS.has(expression.name.text)
|
|
) {
|
|
return skipMethodFromExpression(expression.expression);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function methodReason(params: {
|
|
file: string;
|
|
method: TestSkipInventoryFinding["method"];
|
|
sourceFile: ts.SourceFile;
|
|
textNode: ts.Node;
|
|
}): SkipInventoryReason {
|
|
if (params.method === "todo") {
|
|
return "todo";
|
|
}
|
|
if (params.method === "only") {
|
|
return "focused-only";
|
|
}
|
|
|
|
const sourceText = params.textNode.getText(params.sourceFile).toLowerCase();
|
|
const text = `${params.file.toLowerCase()}\n${sourceText}`;
|
|
if (
|
|
sourceText.includes("process.platform") ||
|
|
sourceText.includes("win32") ||
|
|
sourceText.includes("darwin") ||
|
|
sourceText.includes("linux")
|
|
) {
|
|
return "platform-gate";
|
|
}
|
|
if (
|
|
text.includes(".live.test.") ||
|
|
sourceText.includes("openclaw_live") ||
|
|
sourceText.includes("live_test") ||
|
|
sourceText.includes("api_key") ||
|
|
sourceText.includes("token") ||
|
|
/\blive\b/u.test(sourceText)
|
|
) {
|
|
return "live-gate";
|
|
}
|
|
if (
|
|
text.includes("chromium") ||
|
|
text.includes("browser") ||
|
|
text.includes("docker") ||
|
|
text.includes("ffmpeg") ||
|
|
text.includes("gotoolchain") ||
|
|
text.includes("go toolchain")
|
|
) {
|
|
return "optional-dependency";
|
|
}
|
|
if (params.method === "skipIf" || params.method === "runIf") {
|
|
return "conditional-skip";
|
|
}
|
|
if (params.method === "skip" && containsConditionalExpression(params.textNode)) {
|
|
return "conditional-skip";
|
|
}
|
|
return "explicit-skip";
|
|
}
|
|
|
|
function containsConditionalExpression(node: ts.Node): boolean {
|
|
if (ts.isConditionalExpression(node)) {
|
|
return true;
|
|
}
|
|
return node.forEachChild((child) => containsConditionalExpression(child) || undefined) ?? false;
|
|
}
|
|
|
|
function scanFile(params: { file: string; sourceFile: ts.SourceFile }): TestSkipInventoryFinding[] {
|
|
const { sourceFile } = params;
|
|
const lines = sourceFile.text.split(/\r?\n/u);
|
|
const findings: TestSkipInventoryFinding[] = [];
|
|
|
|
function addFinding(details: {
|
|
kind: SkipInventoryKind;
|
|
method: TestSkipInventoryFinding["method"];
|
|
node: ts.Node;
|
|
reasonNode: ts.Node;
|
|
target: SkipInventoryTarget;
|
|
}): void {
|
|
const { line } = sourceFile.getLineAndCharacterOfPosition(details.node.getStart());
|
|
findings.push({
|
|
excerpt: lines[line]?.trim() ?? "",
|
|
file: params.file,
|
|
kind: details.kind,
|
|
line: line + 1,
|
|
method: details.method,
|
|
reason: methodReason({
|
|
file: params.file,
|
|
method: details.method,
|
|
sourceFile,
|
|
textNode: details.reasonNode,
|
|
}),
|
|
target: details.target,
|
|
});
|
|
}
|
|
|
|
function visit(node: ts.Node): void {
|
|
if (ts.isVariableDeclaration(node)) {
|
|
const alias = node.initializer && skipMethodFromExpression(node.initializer);
|
|
if (alias) {
|
|
addFinding({
|
|
kind: "alias",
|
|
method: alias.method,
|
|
node,
|
|
reasonNode: node.initializer ?? node,
|
|
target: alias.target,
|
|
});
|
|
}
|
|
} else if (ts.isCallExpression(node)) {
|
|
const method = isNestedTestModifierCallee(node)
|
|
? null
|
|
: skipMethodFromExpression(node.expression);
|
|
if (method) {
|
|
addFinding({
|
|
kind: "call",
|
|
method: method.method,
|
|
node,
|
|
reasonNode: node,
|
|
target: method.target,
|
|
});
|
|
}
|
|
}
|
|
node.forEachChild(visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return findings;
|
|
}
|
|
|
|
function isNestedTestModifierCallee(node: ts.CallExpression): boolean {
|
|
const parent = node.parent;
|
|
return (
|
|
(ts.isCallExpression(parent) && parent.expression === node) ||
|
|
(ts.isPropertyAccessExpression(parent) && parent.expression === node)
|
|
);
|
|
}
|
|
|
|
export function collectTestSkipInventoryReport(
|
|
params: { repoRoot?: string } = {},
|
|
): TestSkipInventoryReport {
|
|
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
|
const files = listRepoFilesSync(repoRoot, {
|
|
includeFile: (file) => isCodeFile(file) && isTestRelatedFile(file),
|
|
});
|
|
const findings: TestSkipInventoryFinding[] = [];
|
|
const parser = createNativeTypeScriptParser({ cwd: repoRoot });
|
|
try {
|
|
const sources = files.map((fileName) => ({
|
|
fileName,
|
|
text: fs.readFileSync(path.join(repoRoot, fileName), "utf8"),
|
|
}));
|
|
for (const sourceFile of parser.parseSourceFiles(sources)) {
|
|
const file = path.relative(repoRoot, sourceFile.fileName).split(path.sep).join("/");
|
|
findings.push(...scanFile({ file, sourceFile }));
|
|
}
|
|
} finally {
|
|
parser.close();
|
|
}
|
|
const reasonCounts = { ...EMPTY_REASON_COUNTS };
|
|
for (const finding of findings) {
|
|
reasonCounts[finding.reason] += 1;
|
|
}
|
|
return {
|
|
findings,
|
|
scannedFileCount: files.length,
|
|
schemaVersion: 1,
|
|
summary: {
|
|
findingCount: findings.length,
|
|
reasonCounts,
|
|
scannedFileCount: files.length,
|
|
touchedFileCount: new Set(findings.map((finding) => finding.file)).size,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function renderTestSkipInventoryReport(
|
|
report: TestSkipInventoryReport,
|
|
options: { limit?: number } = {},
|
|
): string {
|
|
const limit = options.limit === 0 ? Number.POSITIVE_INFINITY : (options.limit ?? 120);
|
|
const reasonCounts =
|
|
Object.entries(report.summary.reasonCounts)
|
|
.filter(([, count]) => count > 0)
|
|
.map(([reason, count]) => `${reason}: ${count}`)
|
|
.join(", ") || "none";
|
|
const lines = [
|
|
"OpenClaw test skip inventory",
|
|
`Scanned files: ${report.summary.scannedFileCount}`,
|
|
`Findings: ${report.summary.findingCount} in ${report.summary.touchedFileCount} file(s)`,
|
|
`Reasons: ${reasonCounts}`,
|
|
"",
|
|
];
|
|
|
|
if (report.findings.length === 0) {
|
|
lines.push("Findings: none");
|
|
} else {
|
|
lines.push("Findings:");
|
|
lines.push(
|
|
...renderFindingGroups(
|
|
report.findings,
|
|
limit,
|
|
(finding) =>
|
|
` L${finding.line} ${finding.target}.${finding.method} ${finding.reason}: ${finding.excerpt}`,
|
|
),
|
|
);
|
|
}
|
|
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
function printHelp(): void {
|
|
process.stdout.write(`OpenClaw test skip inventory
|
|
|
|
Usage:
|
|
pnpm test:skip-inventory:report [options]
|
|
|
|
Options:
|
|
--json Print the full JSON report
|
|
--limit <n> Maximum text findings to print; use 0 for all (default: 120)
|
|
--repo-root <path> Repository root to scan (default: current working directory)
|
|
--help, -h Show this help message
|
|
`);
|
|
}
|
|
|
|
export function main(argv = process.argv.slice(2)): number {
|
|
const args = parseInventoryReportCliArgs(argv);
|
|
if (args.help) {
|
|
printHelp();
|
|
return 0;
|
|
}
|
|
|
|
const report = collectTestSkipInventoryReport({ repoRoot: args.repoRoot });
|
|
if (args.json) {
|
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
} else {
|
|
process.stdout.write(renderTestSkipInventoryReport(report, { limit: args.limit }));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
process.exitCode = main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|