chore(lint): enable structured clone rules

This commit is contained in:
Peter Steinberger
2026-05-31 08:32:32 +01:00
parent 87664ed096
commit 59694e86d9
22 changed files with 83 additions and 34 deletions

View File

@@ -52,7 +52,9 @@
"eslint/unicode-bom": "error",
"eslint/yoda": "error",
"import/no-absolute-path": "error",
"import/first": "error",
"import/no-empty-named-blocks": "error",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"node/no-exports-assign": "error",
"eslint-plugin-unicorn/prefer-set-size": "error",
@@ -81,7 +83,7 @@
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unnecessary-type-conversion": "error",
"typescript/no-unnecessary-type-parameters": "error",
"typescript/no-unsafe-type-assertion": "off",
"typescript/no-unsafe-type-assertion": "error",
"typescript/no-useless-default-assignment": "error",
"typescript/no-useless-empty-export": "error",
"typescript/no-wrapper-object-types": "error",
@@ -134,6 +136,8 @@
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-size": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/require-array-join-separator": "error",

View File

@@ -16,12 +16,12 @@ function inMemoryIO(
} {
const store: CodexPluginsConfigBlock = {
enabled: options.enabled,
plugins: JSON.parse(JSON.stringify(initial)),
plugins: structuredClone(initial),
};
return {
current: () => JSON.parse(JSON.stringify(store.plugins ?? {})),
currentConfig: () => JSON.parse(JSON.stringify(store)),
readConfig: () => Promise.resolve(JSON.parse(JSON.stringify(store))),
current: () => structuredClone(store.plugins ?? {}),
currentConfig: () => structuredClone(store),
readConfig: () => Promise.resolve(structuredClone(store)),
mutate: async (update) => {
update(store);
},

View File

@@ -112,12 +112,12 @@ function inMemoryCodexPluginsIO(
} {
const store: CodexPluginsConfigBlock = {
enabled: options.enabled,
plugins: JSON.parse(JSON.stringify(initial)),
plugins: structuredClone(initial),
};
return {
current: () => JSON.parse(JSON.stringify(store.plugins ?? {})),
currentConfig: () => JSON.parse(JSON.stringify(store)),
readConfig: () => Promise.resolve(JSON.parse(JSON.stringify(store))),
current: () => structuredClone(store.plugins ?? {}),
currentConfig: () => structuredClone(store),
readConfig: () => Promise.resolve(structuredClone(store)),
mutate: async (update) => {
update(store);
},

View File

@@ -8,6 +8,11 @@ import {
} from "./inbound-job.js";
import { createBaseDiscordMessageContext } from "./message-handler.test-harness.js";
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
describe("buildDiscordInboundJob", () => {
it("prefers route session key, then base session key, then channel id for queueing", async () => {
const routed = await createBaseDiscordMessageContext({
@@ -88,7 +93,7 @@ describe("buildDiscordInboundJob", () => {
},
ownerId: "user-1",
});
const serializedPayload = JSON.parse(JSON.stringify(job.payload));
const serializedPayload = jsonRoundTrip(job.payload);
expect(serializedPayload.threadChannel).toEqual({
id: "thread-1",
name: "codex",
@@ -125,7 +130,7 @@ describe("buildDiscordInboundJob", () => {
parent: undefined,
ownerId: undefined,
});
const serializedPayload = JSON.parse(JSON.stringify(job.payload));
const serializedPayload = jsonRoundTrip(job.payload);
expect(serializedPayload.threadChannel).toEqual({
id: "thread-1",
});

View File

@@ -842,7 +842,8 @@ function resolveGoogleGemini3RetryThinkingLevel(modelId: string): GoogleThinking
function cloneGoogleGenerateContentRequest(
params: GoogleGenerateContentRequest,
): GoogleGenerateContentRequest {
return JSON.parse(JSON.stringify(params)) as GoogleGenerateContentRequest;
const serialized = JSON.stringify(params);
return JSON.parse(serialized) as GoogleGenerateContentRequest;
}
export function buildGoogleGemini3FirstResponseRetryParams(params: {

View File

@@ -192,7 +192,8 @@ async function loadBindingsFromPluginState(params: {
}
function toPluginJsonValue<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
async function persistBindingsSnapshot(params: {

View File

@@ -41,7 +41,8 @@ export function resolveMSTeamsSqliteStateEnv(
}
export function toPluginJsonValue<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
export function resolveMSTeamsSqliteStateDir(

View File

@@ -358,7 +358,7 @@ function extractLiveDockerPackageScripts(packageJson) {
}
function stripLiveDockerPackageScripts(packageJson) {
const clone = JSON.parse(JSON.stringify(packageJson));
const clone = structuredClone(packageJson);
const scripts = clone.scripts;
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
return clone;
@@ -377,7 +377,7 @@ function extractPackageScripts(packageJson) {
}
function stripPackageScripts(packageJson) {
const clone = JSON.parse(JSON.stringify(packageJson));
const clone = structuredClone(packageJson);
delete clone.scripts;
return clone;
}

View File

@@ -295,7 +295,8 @@ function toJsonSafe(value: unknown): unknown {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
const serialized = JSON.stringify(value);
return serialized === undefined ? null : (JSON.parse(serialized) as unknown);
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };

View File

@@ -271,7 +271,8 @@ function toJsonSafe(value: unknown): unknown {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
const serialized = JSON.stringify(value);
return serialized === undefined ? null : (JSON.parse(serialized) as unknown);
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };

View File

@@ -146,7 +146,8 @@ function toJsonSafe(value: unknown): unknown {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
const serialized = JSON.stringify(value);
return serialized === undefined ? null : (JSON.parse(serialized) as unknown);
} catch {
if (value instanceof Error) {
return { name: value.name, message: value.message };

View File

@@ -111,7 +111,8 @@ function cloneDiagnosticContentValue(value: unknown): unknown {
return structuredClone(value);
} catch {
try {
return JSON.parse(JSON.stringify(value)) as unknown;
const serialized = JSON.stringify(value);
return serialized === undefined ? null : (JSON.parse(serialized) as unknown);
} catch {
return String(value);
}

View File

@@ -1344,7 +1344,8 @@ function toJsonSafe(value: unknown): unknown {
return null;
}
try {
return JSON.parse(JSON.stringify(value)) as unknown;
const serialized = JSON.stringify(value);
return serialized === undefined ? null : (JSON.parse(serialized) as unknown);
} catch {
if (value instanceof Error) {
return value.message;

View File

@@ -22,6 +22,11 @@ import { makeZeroUsageSnapshot } from "../usage.js";
import { testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js";
import { resolveMediaToolInboundRoots } from "./media-tool-shared.js";
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
const publicSurfaceLoaderMocks = vi.hoisted(() => ({
loadBundledPluginPublicArtifactModuleSync: vi.fn(
({ artifactBasename, dirName }: { artifactBasename: string; dirName: string }) => {
@@ -1685,7 +1690,7 @@ describe("image tool implicit imageModel config", () => {
it("keeps an Anthropic-safe image schema snapshot", async () => {
await withMinimaxImageToolFromTempAgentDir(async (tool) => {
expect(JSON.parse(JSON.stringify(tool.parameters))).toEqual({
expect(jsonRoundTrip(tool.parameters)).toEqual({
type: "object",
properties: {
prompt: { type: "string" },

View File

@@ -7,6 +7,11 @@ import {
const DREAMING_TOKEN = "__openclaw_memory_core_short_term_promotion_dream__";
const DREAMING_TAG = "[managed-by=memory-core.short-term-promotion]";
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
function staleDreamingJob() {
return {
id: "job-1",
@@ -99,7 +104,7 @@ describe("migrateLegacyDreamingPayloadShape", () => {
wakeMode: "now",
payload: { kind: "agentTurn", message: "good morning" },
} as Record<string, unknown>;
const snapshot = JSON.parse(JSON.stringify(unrelated)) as Record<string, unknown>;
const snapshot = jsonRoundTrip(unrelated) as Record<string, unknown>;
const jobs = [unrelated];
const result = migrateLegacyDreamingPayloadShape(jobs);
expect(result).toEqual({ changed: false, rewrittenCount: 0 });

View File

@@ -19,6 +19,11 @@ vi.mock("../config/config.js", () => ({
const ORIGINAL_STATE_DIR = process.env.OPENCLAW_STATE_DIR;
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
function createRuntime(): RuntimeEnv {
return {
log: vi.fn(),
@@ -95,8 +100,8 @@ describe("flows commands", () => {
status: "blocked",
flows: [
{
...JSON.parse(JSON.stringify(flow)),
tasks: [JSON.parse(JSON.stringify(childTask))],
...jsonRoundTrip(flow),
tasks: [jsonRoundTrip(childTask)],
taskSummary: {
total: 1,
active: 1,

View File

@@ -28,6 +28,11 @@ function readJsonLog(runtime: RuntimeEnv): unknown {
return JSON.parse(String(call[0]));
}
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
async function withTaskJsonStateDir(run: () => Promise<void>): Promise<void> {
await withOpenClawTestState(
{ layout: "state-only", prefix: "openclaw-tasks-json-command-" },
@@ -84,7 +89,7 @@ describe("tasks JSON commands", () => {
count: 1,
runtime: "cli",
status: "running",
tasks: [JSON.parse(JSON.stringify(cliTask))],
tasks: [jsonRoundTrip(cliTask)],
});
});
});
@@ -170,7 +175,7 @@ describe("tasks JSON commands", () => {
ageMs: 45 * 60_000,
status: "running",
token: runningFlow.flowId,
flow: JSON.parse(JSON.stringify(runningFlow)),
flow: jsonRoundTrip(runningFlow),
},
],
});

View File

@@ -31,6 +31,11 @@ function readFirstJsonLog(runtime: RuntimeEnv): unknown {
return JSON.parse(String(message));
}
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
const zeroTaskAuditCounts = {
delivery_failed: 0,
inconsistent_timestamps: 0,
@@ -137,7 +142,7 @@ describe("tasks commands", () => {
ageMs: 45 * 60_000,
status: "running",
token: runningFlow.flowId,
flow: JSON.parse(JSON.stringify(runningFlow)),
flow: jsonRoundTrip(runningFlow),
},
]);
});

View File

@@ -38,6 +38,11 @@ const ENFORCED_MAINTENANCE_OVERRIDE = {
highWaterBytes: null,
};
function jsonRoundTrip<T>(value: T): T {
const serialized = JSON.stringify(value);
return JSON.parse(serialized) as T;
}
const archiveTimestamp = (ms: number) => new Date(ms).toISOString().replaceAll(":", "-");
const suiteRootTracker = createSuiteTempRootTracker({ prefix: "openclaw-pruning-integ-" });
@@ -1193,10 +1198,7 @@ describe("Integration: saveSessionStore with pruning", () => {
};
await fs.writeFile(storePath, JSON.stringify(store, null, 2), "utf-8");
await saveSessionStore(
storePath,
JSON.parse(JSON.stringify(store)) as Record<string, SessionEntry>,
);
await saveSessionStore(storePath, jsonRoundTrip(store) as Record<string, SessionEntry>);
const files = await fs.readdir(testDir);
const backups = files.filter((file) => file.startsWith("sessions.json.bak."));

View File

@@ -68,7 +68,7 @@ function cloneRestartSentinelPayload(
if (!payload) {
return null;
}
return JSON.parse(JSON.stringify(payload)) as RestartSentinelPayload;
return structuredClone(payload) as RestartSentinelPayload;
}
function hasRoutableDeliveryContext(context?: {

View File

@@ -91,7 +91,7 @@ export async function writeRestartSentinel(
}
function cloneRestartSentinelPayload(payload: RestartSentinelPayload): RestartSentinelPayload {
return JSON.parse(JSON.stringify(payload)) as RestartSentinelPayload;
return structuredClone(payload) as RestartSentinelPayload;
}
async function rewriteRestartSentinel(

View File

@@ -36,7 +36,9 @@ const ZERO_BASELINE_RULES = [
"eslint/unicode-bom",
"eslint/yoda",
"import/no-absolute-path",
"import/first",
"import/no-empty-named-blocks",
"import/no-duplicates",
"import/no-self-import",
"node/no-exports-assign",
"promise/no-new-statics",
@@ -45,6 +47,7 @@ const ZERO_BASELINE_RULES = [
"typescript/no-import-type-side-effects",
"typescript/no-inferrable-types",
"typescript/no-non-null-asserted-nullish-coalescing",
"typescript/no-unsafe-type-assertion",
"typescript/no-unnecessary-qualifier",
"typescript/prefer-find",
"typescript/prefer-for-of",
@@ -74,6 +77,8 @@ const ZERO_BASELINE_RULES = [
"unicorn/prefer-optional-catch-binding",
"unicorn/prefer-prototype-methods",
"unicorn/prefer-regexp-test",
"unicorn/prefer-set-has",
"unicorn/prefer-structured-clone",
"unicorn/prefer-string-slice",
"unicorn/require-array-join-separator",
"unicorn/require-number-to-fixed-digits-argument",