mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
ci: add session accessor boundary ratchet
This commit is contained in:
@@ -1583,6 +1583,7 @@
|
||||
"lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs",
|
||||
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
|
||||
"lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs",
|
||||
"lint:tmp:session-accessor-boundary": "node scripts/check-session-accessor-boundary.mjs",
|
||||
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
|
||||
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",
|
||||
"lint:web-fetch-provider-boundaries": "node scripts/check-web-fetch-provider-boundaries.mjs",
|
||||
|
||||
146
scripts/check-session-accessor-boundary.mjs
Normal file
146
scripts/check-session-accessor-boundary.mjs
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from "node:path";
|
||||
import ts from "typescript";
|
||||
import {
|
||||
collectFileViolations,
|
||||
resolveRepoRoot,
|
||||
resolveSourceRoots,
|
||||
runAsScript,
|
||||
toLine,
|
||||
unwrapExpression,
|
||||
} from "./lib/ts-guard-utils.mjs";
|
||||
|
||||
const legacyReaderNames = new Set(["loadSessionStore", "readSessionEntries"]);
|
||||
|
||||
export const migratedSessionAccessorFiles = new Set([
|
||||
"src/config/sessions/combined-store-gateway.ts",
|
||||
"src/gateway/session-utils.ts",
|
||||
"src/gateway/sessions-resolve.ts",
|
||||
"src/gateway/server-methods/sessions.ts",
|
||||
]);
|
||||
|
||||
function normalizeRelativePath(filePath) {
|
||||
return filePath.replaceAll(path.sep, "/");
|
||||
}
|
||||
|
||||
function propertyAccessName(expression) {
|
||||
const unwrapped = unwrapExpression(expression);
|
||||
if (ts.isIdentifier(unwrapped)) {
|
||||
return unwrapped.text;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(unwrapped)) {
|
||||
return unwrapped.name.text;
|
||||
}
|
||||
if (ts.isElementAccessExpression(unwrapped) && ts.isStringLiteral(unwrapped.argumentExpression)) {
|
||||
return unwrapped.argumentExpression.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bindingName(node) {
|
||||
if (node.propertyName && ts.isIdentifier(node.propertyName)) {
|
||||
return node.propertyName.text;
|
||||
}
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
return node.name.text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findSessionAccessorBoundaryViolations(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const violations = [];
|
||||
|
||||
const visit = (node) => {
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
const namedBindings = node.importClause?.namedBindings;
|
||||
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
||||
for (const specifier of namedBindings.elements) {
|
||||
const importedName = specifier.propertyName?.text ?? specifier.name.text;
|
||||
if (legacyReaderNames.has(importedName)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, specifier),
|
||||
reason: `imports legacy session store reader "${importedName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isBindingElement(node)) {
|
||||
const name = bindingName(node);
|
||||
if (name && legacyReaderNames.has(name)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node),
|
||||
reason: `aliases legacy session store reader "${name}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isPropertyAccessExpression(node) && legacyReaderNames.has(node.name.text)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.name),
|
||||
reason: `references legacy session store reader "${node.name.text}"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isElementAccessExpression(node) &&
|
||||
ts.isStringLiteral(node.argumentExpression) &&
|
||||
legacyReaderNames.has(node.argumentExpression.text)
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.argumentExpression),
|
||||
reason: `references legacy session store reader "${node.argumentExpression.text}"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (ts.isCallExpression(node)) {
|
||||
const calleeName = propertyAccessName(node.expression);
|
||||
if (
|
||||
calleeName &&
|
||||
legacyReaderNames.has(calleeName) &&
|
||||
ts.isIdentifier(unwrapExpression(node.expression))
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.expression),
|
||||
reason: `calls legacy session store reader "${calleeName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
return violations;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
const sourceRoots = resolveSourceRoots(repoRoot, ["src/config/sessions", "src/gateway"]);
|
||||
const violations = await collectFileViolations({
|
||||
repoRoot,
|
||||
sourceRoots,
|
||||
skipFile: (filePath) =>
|
||||
!migratedSessionAccessorFiles.has(normalizeRelativePath(path.relative(repoRoot, filePath))),
|
||||
findViolations: findSessionAccessorBoundaryViolations,
|
||||
});
|
||||
|
||||
if (violations.length === 0) {
|
||||
console.log("session accessor boundary guard passed.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Found legacy session store reader usage in session-accessor migrated files:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
|
||||
}
|
||||
console.error(
|
||||
"Use src/config/sessions/session-accessor.ts helpers for migrated read/projection paths. Expand this ratchet only after a slice migrates more files.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
@@ -14,6 +14,7 @@ export const BOUNDARY_CHECKS = [
|
||||
["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]],
|
||||
["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]],
|
||||
["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]],
|
||||
["lint:tmp:session-accessor-boundary", "pnpm", ["run", "lint:tmp:session-accessor-boundary"]],
|
||||
["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]],
|
||||
[
|
||||
"lint:plugins:no-register-http-handler",
|
||||
@@ -442,11 +443,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY === undefined
|
||||
? "OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY"
|
||||
: "OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY";
|
||||
const concurrency = resolveConcurrency(
|
||||
concurrencyRaw,
|
||||
4,
|
||||
concurrencyLabel,
|
||||
);
|
||||
const concurrency = resolveConcurrency(concurrencyRaw, 4, concurrencyLabel);
|
||||
const checkTimeoutMs = resolvePositiveInteger(
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS,
|
||||
DEFAULT_CHECK_TIMEOUT_MS,
|
||||
|
||||
75
test/scripts/check-session-accessor-boundary.test.ts
Normal file
75
test/scripts/check-session-accessor-boundary.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findSessionAccessorBoundaryViolations,
|
||||
migratedSessionAccessorFiles,
|
||||
} from "../../scripts/check-session-accessor-boundary.mjs";
|
||||
|
||||
describe("session accessor boundary guard", () => {
|
||||
it("ratchets only the files migrated by the session accessor gateway slice", () => {
|
||||
expect(migratedSessionAccessorFiles).toEqual(
|
||||
new Set([
|
||||
"src/config/sessions/combined-store-gateway.ts",
|
||||
"src/gateway/session-utils.ts",
|
||||
"src/gateway/sessions-resolve.ts",
|
||||
"src/gateway/server-methods/sessions.ts",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags legacy reader imports", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
import { loadSessionStore, readSessionEntries as readEntries } from "../config/sessions.js";
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'imports legacy session store reader "loadSessionStore"' },
|
||||
{ line: 2, reason: 'imports legacy session store reader "readSessionEntries"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags direct and namespace legacy reader calls", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
loadSessionStore(storePath);
|
||||
sessions.readSessionEntries(storePath);
|
||||
sessions["loadSessionStore"](storePath);
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'calls legacy session store reader "loadSessionStore"' },
|
||||
{ line: 3, reason: 'references legacy session store reader "readSessionEntries"' },
|
||||
{ line: 4, reason: 'references legacy session store reader "loadSessionStore"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags aliased namespace reader references", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
const load = sessions.loadSessionStore;
|
||||
const { readSessionEntries: readEntries } = sessions;
|
||||
const { loadSessionStore } = sessions;
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 2, reason: 'references legacy session store reader "loadSessionStore"' },
|
||||
{ line: 3, reason: 'aliases legacy session store reader "readSessionEntries"' },
|
||||
{ line: 4, reason: 'aliases legacy session store reader "loadSessionStore"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows migrated accessor reads", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
import { listSessionEntries } from "../config/sessions/session-accessor.js";
|
||||
listSessionEntries({ storePath });
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores comments and strings that describe legacy readers", () => {
|
||||
expect(
|
||||
findSessionAccessorBoundaryViolations(`
|
||||
// loadSessionStore and readSessionEntries used to be called here.
|
||||
const description = "loadSessionStore";
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -92,12 +92,17 @@ describe("run-additional-boundary-checks", () => {
|
||||
expect(() => parseShardSpec("5/4")).toThrow("Invalid shard spec");
|
||||
});
|
||||
|
||||
it("keeps the raw HTTP/2 import guard in source boundary checks", () => {
|
||||
expect(BOUNDARY_CHECKS[6]).toEqual({
|
||||
it("keeps the temporary ratchet guards in source boundary checks", () => {
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "lint:tmp:no-raw-http2-imports",
|
||||
command: "pnpm",
|
||||
args: ["run", "lint:tmp:no-raw-http2-imports"],
|
||||
});
|
||||
expect(BOUNDARY_CHECKS).toContainEqual({
|
||||
label: "lint:tmp:session-accessor-boundary",
|
||||
command: "pnpm",
|
||||
args: ["run", "lint:tmp:session-accessor-boundary"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the Telegram grammY type import guard in source boundary checks", () => {
|
||||
|
||||
Reference in New Issue
Block a user