fix(ui): keep Control UI code off the state database schema loader (#152130)

Import the existing record and fresh-module helpers directly in Control UI
unit tests. Add a transitive static import guard to the boundary lane and
keep the unchanged Gateway config round-trip proof in the Node test lane.

The reported suites pass before and after #152053 (0485ca29d6), then fail
at d69a4fa9ca (#151652): the plugin SDK fixture barrel starts re-exporting
runtime system-event helpers and reaches the state database through config.
Preserve all assertions and the server schema loader's file-asset contract.

Main failure: https://github.com/openclaw/openclaw/actions/runs/35382740723
Related: #152053, #151652, #152099
This commit is contained in:
Peter Steinberger
2026-09-18 14:48:40 -07:00
committed by GitHub
parent feb2c818d2
commit 778f2c4a39
5 changed files with 58 additions and 5 deletions
+31
View File
@@ -0,0 +1,31 @@
import path from "node:path";
import { expect, it } from "vitest";
import { listGitTrackedFiles } from "../src/test-utils/repo-files.js";
import { findSourceImportBackedges } from "./helpers/source-import-closure.js";
import { uiNodeDrivenBrowserTestFiles } from "./vitest/vitest.ui-paths.mjs";
const repoRoot = path.resolve(import.meta.dirname, "..");
it("keeps Control UI runtime imports off the state database", () => {
const files = listGitTrackedFiles({ repoRoot, pathspecs: ["ui/src", "src/state"] });
if (!files) {
throw new Error("Cannot list Control UI import boundary sources");
}
// Node/E2E harnesses may exercise real Gateway owners outside the browser.
const entries = files.filter(
(file) =>
file.startsWith("ui/src/") &&
file.endsWith(".ts") &&
!file.startsWith("ui/src/e2e/") &&
!file.endsWith(".e2e.test.ts") &&
!file.endsWith(".node.test.ts") &&
!uiNodeDrivenBrowserTestFiles.includes(file),
);
const forbidden = files.filter((file) =>
/^src\/state\/openclaw-state-(?:db.*|schema)\.ts$/u.test(file),
);
expect(entries.length).toBeGreaterThan(0);
expect(forbidden.length).toBeGreaterThan(0);
expect(findSourceImportBackedges(entries, forbidden)).toEqual([]);
});
+24 -4
View File
@@ -20,7 +20,19 @@ if (errors.length) {
);
}
function resolveSourceModule(importer: string, specifier: string): string | undefined {
function resolveSourceModule(importer: string, rawSpecifier: string): string | undefined {
// Vite query suffixes select asset handling without changing the source file.
const specifier = rawSpecifier.split("?", 1)[0]!;
// Browser stylesheets are assets, but misspelled paths still fail this guard.
if (
specifier.startsWith(".") &&
specifier.endsWith(".css") &&
fs
.statSync(path.resolve(path.dirname(importer), specifier), { throwIfNoEntry: false })
?.isFile()
) {
return undefined;
}
// This guard owns repository source, including workspace package and SDK aliases.
// Node builtins and installed external packages are terminal dependencies.
const mapped = Object.keys(options.paths ?? {}).some((pattern) => {
@@ -104,9 +116,17 @@ function staticDependencies(file: string): string[] {
return dependencies;
}
export function findSourceImportBackedges(entry: string, forbidden: readonly string[]): string[] {
const pending = [{ file: path.join(repoRoot, entry), parents: [] as string[] }];
export function findSourceImportBackedges(
entry: string | readonly string[],
forbidden: readonly string[],
): string[] {
const entries = typeof entry === "string" ? [entry] : entry;
const pending = entries.map((file) => ({
file: path.join(repoRoot, file),
parents: [] as string[],
}));
const visited = new Set<string>();
const forbiddenFiles = new Set(forbidden);
const violations: string[] = [];
for (const { file, parents } of pending) {
if (visited.has(file)) {
@@ -114,7 +134,7 @@ export function findSourceImportBackedges(entry: string, forbidden: readonly str
}
visited.add(file);
const chain = [...parents, file];
if (forbidden.includes(path.relative(repoRoot, file).split(path.sep).join("/"))) {
if (forbiddenFiles.has(path.relative(repoRoot, file).split(path.sep).join("/"))) {
violations.push(chain.map((part) => path.relative(repoRoot, part)).join(" -> "));
continue;
}
+1
View File
@@ -19,6 +19,7 @@ export const boundaryTestFiles = [
"src/infra/package-json.test.ts",
"src/infra/path-env.test.ts",
"src/infra/stable-node-path.test.ts",
"test/control-ui-import-boundary.test.ts",
"test/extension-import-boundaries.test.ts",
"test/extension-test-boundary.test.ts",
"test/plugin-extension-import-boundary.test.ts",
+2 -1
View File
@@ -1,7 +1,8 @@
// @vitest-environment node
// Control UI tests cover translate behavior.
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../../../src/plugin-sdk/test-helpers/import-fresh.js";
import { createStorageMock } from "../../test-helpers/storage.ts";
import * as translate from "../lib/translate.ts";
import { ar } from "../locales/ar.ts";