mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 15:31:39 +00:00
* refactor(sessions): keep helper transcripts in memory * refactor(sessions): remove file-era transcript storage * test(sessions): use SQLite identity in attempt persistence * test(codex): isolate legacy transcript fixtures * fix(sessions): preserve SQLite transcript identity * fix(sessions): harden transcript lifecycle invariants * fix(sessions): validate transcript identities * fix(sessions): close identity compatibility gaps * fix(sessions): preserve leaf and plugin identities * fix(sessions): retain dispatch transcript targets * fix(sessions): preserve active transcript context * fix(sessions): isolate artifact accounting * fix(sessions): bound SQLite usage accounting * fix(sessions): retain bounded latest usage * fix(sessions): align rebased transcript targets * test(sessions): align accessor scope fixture * fix(telegram): derive SQLite transcript identity * refactor(sessions): remove file-era compaction residue * chore(sessions): lower max-lines baseline * fix(sessions): preserve structured transcript identity * test(sessions): align doctor identity assertions * fix(sessions): isolate default SDK database * refactor(sessions): remove dead file-era exports * fix(sessions): reconcile SQLite transcript identity * fix(sessions): pass checkpoint identity explicitly * test(sessions): make entry field probe explicit * test(sessions): satisfy transcript cleanup lint * test(sessions): align diagnostics identity proof * fix(sessions): finish transcript runtime teardown * fix(sessions): preserve transcript identity invariants * fix(sessions): harden transcript compatibility edges * fix(sessions): preserve checkpoint transcript anchors * fix(sessions): preserve SQLite lifecycle invariants * fix(sessions): retarget compaction successors * test(sessions): preserve transcript fixture semantics * feat(plugin-sdk): add command transcript targets * fix(sessions): serialize transcript rewrites * fix(sessions): validate legacy successor identity * fix(sessions): normalize compaction ownership * fix(sessions): validate successor identity before adoption * fix(sessions): preserve plugin transcript ownership * fix(sessions): carry transcript identity through commands * fix(sessions): import legacy checkpoint artifacts into SQLite * fix(sessions): preserve successor transcript ownership * fix(sessions): align transcript consumers with target identity * fix(sessions): scope transcript token estimates * fix(sessions): retain agent identity across lifecycle hooks * fix(sessions): resolve scoped SQLite targets * fix(sessions): isolate lifecycle transcript targets * fix(sessions): validate compaction agent ownership * fix(sessions): preserve reset and cleanup lifecycle * fix(sessions): serialize prompt cleanup lifecycle * fix(sessions): remove stale lock import * fix(sessions): preserve reset target context * fix(sessions): fence prompt reload takeover * fix(sessions): unblock abort and default lifecycle reads * fix(sessions): validate legacy successor scope * fix(sessions): reject metadata-only runtime rows * fix(sessions): propagate custom transcript stores * fix(sessions): preserve adopted retry targets * fix(sessions): allow unkeyed usage reads * fix(sessions): harden runtime target boundaries * fix(sessions): serialize retry transcript writes * fix(sessions): bound prompt reload disposal * fix(sessions): complete retry marker identity * fix(sessions): keep legacy marker identity minimal * test(sessions): tighten teardown fixture types * fix(sessions): preserve compatibility target identity * test(sessions): persist post-checkpoint boundary turn * test(sessions): align runtime store mock contracts * style(sessions): simplify persisted identity guard * fix(sessions): prefer complete typed targets * fix(sessions): recover legacy marker targets * test(sessions): align marker lookup fixture scope * fix(sessions): validate partial transcript targets * fix(sessions): reconcile partial transcript identities * fix(sessions): canonicalize compatibility identities * test(sessions): cover compatibility aliases * fix(sessions): adopt legacy successor identity * fix(sessions): preserve usage read identity * fix(sessions): preserve partial marker compatibility * fix(sessions): validate legacy successor mappings * fix(sessions): reconcile marker store mappings * fix(sessions): preserve legacy fallback identity * fix(sessions): harden marker alias resolution * fix(sessions): prefer verified successor aliases * fix(sessions): resolve preferred marker aliases * fix(sessions): serialize cleanup admission * fix(sessions): align marker lookup scopes * fix(codex): type marker alias summaries * style(sessions): satisfy changed lint * test(sessions): align structured target assertions * fix(sessions): reconcile latest identity contracts * fix(sessions): validate transcript identity boundaries * docs(sessions): explain stable registry keys * fix(sessions): harden compatibility target round trips * fix(sessions): port usage identity to split modules * test(sessions): align subagent transcript identity * fix(sessions): finish transcript identity migration * fix(agents): route subagent completion capture through transcript targets * fix(agents): settle SQLite prompt handoff during cleanup * chore: shrink max-lines baseline after teardown * fix(sessions): port teardown across split runtime owners * fix(sessions): carry transcript targets through split owners * test(agents): use SQLite compaction target in abort coverage * chore: retain unrelated max-lines suppressions * chore: shrink max-lines baseline after main splits * style(agents): const compaction checkpoint locals * fix(sessions): harden SQLite teardown boundaries * test(sessions): use typed metadata in predicate isolation fixture * test(agents): cover malformed settlement rejections lint-safely * fix(sessions): close remaining SQLite identity races * fix(agents): fail closed on incomplete successor targets * fix(sessions): preserve transcript identity fallbacks * fix(agents): preserve session-key abort admission * fix(trajectory): validate incomplete export targets * test(sessions): drop retired pricing cache imports * fix(sessions): validate partial transcript identities * fix(sessions): close transcript identity edge cases * fix(plugins): reserve retired transcript locator slot * fix(sessions): scope transcript locks by target * style(sessions): simplify SDK initialization error * fix(sessions): preserve initialized transcript state * fix(codex): verify mirrored history session keys * fix(sessions): reject stale transcript ownership * fix(sessions): anchor asynchronous transcript ownership * fix(sessions): measure active transcript state * fix(sessions): preserve scoped transcript compaction * fix(sessions): harden transcript identity and lifecycle * fix(sessions): resolve scoped command transcript stores * fix(sessions): make transcript appends failure-atomic * fix(sessions): enforce scoped transcript ownership * fix(sessions): reject cross-owner transcript handoffs * fix(sessions): fence cleanup transcript ownership * fix(sessions): retire stale write ownership contexts * fix(sessions): preserve pending session migration state * fix(sessions): validate migrated transcript ownership * fix(sessions): validate usage transcript targets * fix(sessions): clear predecessor transcript metadata * fix(sessions): align durable session event targets * fix(sessions): fence late prompt handoffs * fix(sessions): fence lifecycle transcript fallbacks * fix(sessions): bound zero-length memory capture * fix(sessions): preserve transcript teardown ownership * fix(sessions): reject duplicate cleanup ownership * fix(sessions): serialize runtime writes with sqlite leases * fix(sessions): close sqlite teardown concurrency gaps * fix(sessions): preserve nested lifecycle failures * fix(sessions): canonicalize sqlite transcript ownership * fix(sessions): settle disposed prompt handoffs * fix(sessions): resolve canonical attempt lock targets * test(sessions): align canonical target fixtures * test(sessions): retire redundant jsonl parser coverage * refactor(sessions): split active transcript cursors * test(memory): retire legacy marker fixture * fix(sessions): preserve canonical transcript access after rebase * fix(sessions): fence prompt lease and return transcript targets * fix(sessions): colocate transcript leases with target store * fix(sessions): canonicalize transcript lease and worker targets * fix(sessions): preserve plugin and fork identity markers * fix(sessions): complete sqlite transcript target migration * fix(sessions): integrate canonical followup identity * fix(sessions): preserve bounded transcript topology * fix(sessions): validate transcript identity boundaries * fix(context): separate caller and successor targets * test(sessions): split persistence compatibility coverage * test(sessions): preserve fixture topology efficiently * chore(sdk): refresh plugin api baseline * test(agents): align compaction lock target mocks * test(sessions): seed malformed transcript fixtures directly * fix(agents): canonicalize transcript compatibility inputs * fix(agents): type optional tool result ids * test(ci): stabilize loaded process timing * test(tui): wait for collect queue admission
267 lines
12 KiB
TypeScript
267 lines
12 KiB
TypeScript
// Lint Suppressions tests cover lint suppressions script behavior.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
collectLintDisableDirectives,
|
|
isMaxLinesRule,
|
|
} from "../../scripts/check-max-lines-ratchet.mjs";
|
|
import { expectNoReaddirSyncDuring } from "../../src/test-utils/fs-scan-assertions.js";
|
|
import { listGitTrackedFiles, toRepoRelativePath } from "../../src/test-utils/repo-files.js";
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
|
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
const IGNORED_DIRS = new Set([".cache", ".git", "build", "coverage", "dist", "node_modules"]);
|
|
const ROOTS = ["src", "extensions", "scripts", "ui"] as const;
|
|
|
|
type SuppressionEntry = {
|
|
file: string;
|
|
rule: string;
|
|
};
|
|
|
|
let productionLintSuppressionsCache: SuppressionEntry[] | null = null;
|
|
let productionCodeFilesCache: string[] | null = null;
|
|
|
|
function collectFileSuppressions(file: string, source: string): SuppressionEntry[] {
|
|
return collectLintDisableDirectives(source, file).flatMap((rules) =>
|
|
rules.filter((rule) => !isMaxLinesRule(rule)).map((rule) => ({ file, rule })),
|
|
);
|
|
}
|
|
|
|
function isProductionCodeFile(relativePath: string): boolean {
|
|
const basename = path.posix.basename(relativePath);
|
|
if (!CODE_EXTENSIONS.has(path.extname(relativePath))) {
|
|
return false;
|
|
}
|
|
if (basename.startsWith("__rootdir_boundary_canary__.")) {
|
|
return false;
|
|
}
|
|
return !(
|
|
relativePath.includes("/test/") ||
|
|
relativePath.endsWith(".test.ts") ||
|
|
relativePath.endsWith(".test.tsx") ||
|
|
relativePath.endsWith(".spec.ts") ||
|
|
relativePath.endsWith(".spec.tsx")
|
|
);
|
|
}
|
|
|
|
function listGitCodeFiles(root: string): string[] | null {
|
|
return (
|
|
listGitTrackedFiles({ repoRoot, pathspecs: root })
|
|
?.filter(isProductionCodeFile)
|
|
.filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath))) ?? null
|
|
);
|
|
}
|
|
|
|
function walkCodeFiles(dir: string, files: string[] = []): string[] {
|
|
const relativeRoot = toRepoRelativePath(repoRoot, dir);
|
|
if (relativeRoot && !relativeRoot.startsWith("..") && !path.isAbsolute(relativeRoot)) {
|
|
const gitFiles = listGitCodeFiles(relativeRoot);
|
|
if (gitFiles) {
|
|
files.push(...gitFiles);
|
|
return files;
|
|
}
|
|
}
|
|
|
|
if (!fs.existsSync(dir)) {
|
|
return files;
|
|
}
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (IGNORED_DIRS.has(entry.name)) {
|
|
continue;
|
|
}
|
|
walkCodeFiles(fullPath, files);
|
|
continue;
|
|
}
|
|
const relativePath = toRepoRelativePath(repoRoot, fullPath);
|
|
if (!isProductionCodeFile(relativePath)) {
|
|
continue;
|
|
}
|
|
files.push(relativePath);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function collectProductionLintSuppressions(): SuppressionEntry[] {
|
|
if (productionLintSuppressionsCache) {
|
|
return [...productionLintSuppressionsCache];
|
|
}
|
|
const gitEntries = collectProductionLintSuppressionsFromGit();
|
|
if (gitEntries) {
|
|
productionLintSuppressionsCache = gitEntries;
|
|
return [...gitEntries];
|
|
}
|
|
const entries: SuppressionEntry[] = [];
|
|
const files = listProductionCodeFiles();
|
|
for (const relativePath of files) {
|
|
const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
|
entries.push(...collectFileSuppressions(relativePath, source));
|
|
}
|
|
productionLintSuppressionsCache = entries;
|
|
return [...entries];
|
|
}
|
|
|
|
function collectProductionLintSuppressionsFromGit(): SuppressionEntry[] | null {
|
|
const result = spawnSync(
|
|
"git",
|
|
["grep", "-z", "-l", "-e", "oxlint-disable", "-e", "eslint-disable", "--", ...ROOTS],
|
|
{
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
maxBuffer: 8 * 1024 * 1024,
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
},
|
|
);
|
|
if (result.status === 1) {
|
|
return [];
|
|
}
|
|
if (result.status !== 0) {
|
|
return null;
|
|
}
|
|
const entries: SuppressionEntry[] = [];
|
|
for (const file of result.stdout.split("\0").filter(Boolean)) {
|
|
if (!isProductionCodeFile(file) || !fs.existsSync(path.join(repoRoot, file))) {
|
|
continue;
|
|
}
|
|
entries.push(
|
|
...collectFileSuppressions(file, fs.readFileSync(path.join(repoRoot, file), "utf8")),
|
|
);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function listProductionCodeFiles(): string[] {
|
|
productionCodeFilesCache ??= ROOTS.flatMap((root) =>
|
|
walkCodeFiles(path.join(repoRoot, root)),
|
|
).toSorted();
|
|
return [...productionCodeFilesCache];
|
|
}
|
|
|
|
function summarizeSuppressions(entries: readonly SuppressionEntry[]): string[] {
|
|
const counts = new Map<string, number>();
|
|
for (const entry of entries) {
|
|
const key = `${entry.file}|${entry.rule}`;
|
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
}
|
|
return [...counts.entries()].map(([key, count]) => `${key}|${count}`).toSorted();
|
|
}
|
|
|
|
function filterExpectedSuppressionsForPresentFiles(entries: readonly string[]): string[] {
|
|
return entries.filter((entry) => {
|
|
const [file] = entry.split("|", 1);
|
|
return file !== undefined && fs.existsSync(path.join(repoRoot, file));
|
|
});
|
|
}
|
|
|
|
collectProductionLintSuppressions();
|
|
|
|
describe("production lint suppressions", () => {
|
|
it("keeps companion rules visible beside max-lines suppressions", () => {
|
|
expect(
|
|
collectFileSuppressions(
|
|
"src/example.ts",
|
|
"/* oxlint-disable\nmax-lines, no-console\n-- TODO: split this file. */",
|
|
),
|
|
).toEqual([{ file: "src/example.ts", rule: "no-console" }]);
|
|
expect(
|
|
collectFileSuppressions(
|
|
"src/example.ts",
|
|
"/* oxlint-disable eslint/max-lines, no-debugger */",
|
|
),
|
|
).toEqual([{ file: "src/example.ts", rule: "no-debugger" }]);
|
|
expect(collectFileSuppressions("src/example.ts", "/* oxlint-disable - reason */")).toEqual([]);
|
|
});
|
|
|
|
it("lists production files from git without walking source roots", () => {
|
|
expectNoReaddirSyncDuring(() => {
|
|
const files = listProductionCodeFiles();
|
|
|
|
expect(files.length).toBeGreaterThan(0);
|
|
expect(files.some((file) => file.endsWith(".test.ts"))).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("keeps the intentional production suppression tail on an explicit allowlist", () => {
|
|
expect(summarizeSuppressions(collectProductionLintSuppressions())).toEqual(
|
|
filterExpectedSuppressionsForPresentFiles([
|
|
"extensions/browser/src/browser/pw-tools-core.interactions.actions.ts|@typescript-eslint/no-implied-eval|2",
|
|
"extensions/browser/src/browser/pw-tools-core.interactions.content.ts|@typescript-eslint/no-implied-eval|1",
|
|
"extensions/browser/src/cli/browser-cli-actions-input/register.files-downloads.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"extensions/browser/src/node-host/invoke-browser.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"extensions/diffs/src/viewer-client.ts|eslint/no-underscore-dangle|1",
|
|
"extensions/discord/src/outbound-adapter.test-harness.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"extensions/discord/src/test-support/provider.test-support.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"extensions/feishu/src/bitable.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"extensions/matrix/src/onboarding.test-harness.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"extensions/slack/src/monitor/provider-support.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"scripts/changed-lanes.mjs|typescript/no-base-to-string|2",
|
|
"scripts/changed-lanes.mjs|typescript/restrict-template-expressions|2",
|
|
"src/agents/agent-bundle-mcp-runtime.ts|unicorn/prefer-add-event-listener|1",
|
|
"src/agents/agent-tools.abort.ts|typescript/prefer-promise-reject-errors|1",
|
|
"src/agents/sessions/session-manager-entries.ts|unicorn/prefer-structured-clone|1",
|
|
"src/audit/audit-event-writer.ts|unicorn/require-post-message-target-origin|2",
|
|
"src/channels/plugins/channel-runtime-surface.types.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/channels/plugins/contracts/test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/channels/plugins/types.plugin.ts|typescript/no-explicit-any|1",
|
|
"src/cli/cli-utils.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/cli/command-options.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/cli/plugins-cli-test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/cli/test-runtime-capture.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/gateway/test-helpers.server.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/hooks/module-loader.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/infra/device-pairing-store.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/infra/exec-approvals-effective.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/infra/json-file.ts|typescript-eslint/no-unnecessary-type-parameters|1",
|
|
"src/infra/outbound/send-deps.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/node-host/invoke.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/node-host/mcp.ts|unicorn/prefer-add-event-listener|1",
|
|
"src/plugin-sdk/channel-config-helpers.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugin-sdk/channel-entry-contract.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugin-sdk/facade-loader.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugin-sdk/facade-runtime.ts|typescript/no-unnecessary-type-parameters|3",
|
|
"src/plugin-sdk/json-store.ts|typescript-eslint/no-unnecessary-type-parameters|1",
|
|
"src/plugin-sdk/qa-runner-runtime.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugin-sdk/test-helpers/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugin-sdk/test-helpers/subagent-hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugins/hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugins/host-hooks.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugins/lazy-service-module.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugins/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|3",
|
|
"src/plugins/runtime/runtime-plugin-boundary.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugins/runtime/types-channel.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/plugins/trusted-tool-policy.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/state/config-machine-state.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/system-agent/setup-inference-activate.ts|no-unsafe-finally|1",
|
|
"src/system-agent/setup-inference-activate.ts|preserve-caught-error|1",
|
|
"src/tasks/task-registry.sqlite.shared.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/test-utils/vitest-mock-fn.ts|typescript/no-explicit-any|1",
|
|
"src/utils.ts|typescript/no-unnecessary-type-parameters|1",
|
|
"src/version.ts|eslint/no-underscore-dangle|1",
|
|
"ui/public/sw.js|unicorn/require-post-message-target-origin|1",
|
|
// oxlint misreads CanvasRenderingContext2D.fill(path) as Array.fill.
|
|
"ui/src/components/mascot-canvas.ts|unicorn/no-array-fill-with-reference-type|1",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("keeps production no-explicit-any suppressions on an explicit allowlist", () => {
|
|
const anySuppressions = collectProductionLintSuppressions().filter(
|
|
(entry) => entry.rule === "typescript/no-explicit-any",
|
|
);
|
|
|
|
expect(anySuppressions).toEqual([
|
|
{
|
|
file: "src/channels/plugins/types.plugin.ts",
|
|
rule: "typescript/no-explicit-any",
|
|
},
|
|
{
|
|
file: "src/test-utils/vitest-mock-fn.ts",
|
|
rule: "typescript/no-explicit-any",
|
|
},
|
|
]);
|
|
});
|
|
});
|