mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(tests): harden native macos plugin proof
This commit is contained in:
@@ -12,6 +12,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/perf: tighten restart and startup benchmark failure handling so long profiling runs, failed probes, and fresh Linux runners no longer produce false passing or `n/a` results.
|
||||
- Checks: keep intentional Knip unused-file findings optional so full CI and sparse proof workspaces stay aligned.
|
||||
- Docker: restore writable `~/.config` in runtime images. Fixes #85968. Thanks @hkoessler and @Bartok9.
|
||||
- Plugin SDK: keep legacy root diagnostic subscriptions connected when built plugin SDK aliases resolve diagnostic helpers through a separate module graph.
|
||||
- Tests: normalize macOS canonical temp paths in exec allowlists, fs-safe trash assertions, installed plugin matching, Telegram topic-name stores, and built ACPX MCP server expectations so native macOS proof runners cover the intended behavior.
|
||||
- Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.
|
||||
- Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.
|
||||
- Codex: log when implicit app-server `never` approvals are promoted for OpenClaw tool policy, including whether the trigger was a `before_tool_call` hook or trusted tool policy.
|
||||
|
||||
@@ -7,8 +7,12 @@ import { resolveAcpxPluginConfig, resolveAcpxPluginRoot } from "./config.js";
|
||||
const requireFromTest = createRequire(import.meta.url);
|
||||
const TSX_IMPORT = requireFromTest.resolve("tsx");
|
||||
|
||||
function expectedSourceMcpServerArgs(entrypoint: string): string[] {
|
||||
return ["--import", TSX_IMPORT, path.resolve(entrypoint)];
|
||||
function expectedMcpServerArgs(params: { sourceEntry: string; distEntry: string }): string[] {
|
||||
const distEntry = path.resolve(params.distEntry);
|
||||
if (fs.existsSync(distEntry)) {
|
||||
return [distEntry];
|
||||
}
|
||||
return ["--import", TSX_IMPORT, path.resolve(params.sourceEntry)];
|
||||
}
|
||||
|
||||
describe("embedded acpx plugin config", () => {
|
||||
@@ -164,7 +168,10 @@ describe("embedded acpx plugin config", () => {
|
||||
const server = resolved.mcpServers["openclaw-plugin-tools"];
|
||||
expect(server).toEqual({
|
||||
command: process.execPath,
|
||||
args: expectedSourceMcpServerArgs("src/mcp/plugin-tools-serve.ts"),
|
||||
args: expectedMcpServerArgs({
|
||||
sourceEntry: "src/mcp/plugin-tools-serve.ts",
|
||||
distEntry: "dist/mcp/plugin-tools-serve.js",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,7 +186,10 @@ describe("embedded acpx plugin config", () => {
|
||||
const server = resolved.mcpServers["openclaw-tools"];
|
||||
expect(server).toEqual({
|
||||
command: process.execPath,
|
||||
args: expectedSourceMcpServerArgs("src/mcp/openclaw-tools-serve.ts"),
|
||||
args: expectedMcpServerArgs({
|
||||
sourceEntry: "src/mcp/openclaw-tools-serve.ts",
|
||||
distEntry: "dist/mcp/openclaw-tools-serve.js",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { BuildTelegramMessageContextParams, TelegramMediaRef } from "./bot-message-context.js";
|
||||
import { setTelegramTopicNameStoreFactoryForTest } from "./topic-name-cache.js";
|
||||
|
||||
export const baseTelegramMessageContextConfig = {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
|
||||
@@ -8,6 +10,13 @@ export const baseTelegramMessageContextConfig = {
|
||||
} as never;
|
||||
|
||||
type TelegramTestSessionRuntime = NonNullable<BuildTelegramMessageContextParams["sessionRuntime"]>;
|
||||
type TopicNameEntryForTest = {
|
||||
name: string;
|
||||
iconColor?: number;
|
||||
iconCustomEmojiId?: string;
|
||||
closed?: boolean;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
type BuildTelegramMessageContextForTestParams = {
|
||||
message: Record<string, unknown>;
|
||||
@@ -26,28 +35,65 @@ type BuildTelegramMessageContextForTestParams = {
|
||||
resolveTelegramGroupConfig?: BuildTelegramMessageContextParams["resolveTelegramGroupConfig"];
|
||||
};
|
||||
|
||||
const telegramMessageContextSessionRuntimeForTest = {
|
||||
buildChannelInboundEventContext,
|
||||
readSessionUpdatedAt: () => undefined,
|
||||
recordInboundSession: async () => undefined,
|
||||
resolveInboundLastRouteSessionKey: ({ route, sessionKey }) =>
|
||||
route.lastRoutePolicy === "main" ? route.mainSessionKey : sessionKey,
|
||||
resolvePinnedMainDmOwnerFromAllowlist: () => null,
|
||||
resolveStorePath: () => "/tmp/openclaw/session-store.json",
|
||||
} satisfies NonNullable<BuildTelegramMessageContextParams["sessionRuntime"]>;
|
||||
const telegramTopicNameStoresForTest = new Map<string, Map<string, TopicNameEntryForTest>>();
|
||||
|
||||
function resolveSessionStorePathForTest(testName: string | undefined): string {
|
||||
const hash = createHash("sha256")
|
||||
.update(`${process.pid}:${testName ?? "unknown"}`)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
return `/tmp/openclaw/session-store-${hash}.json`;
|
||||
}
|
||||
|
||||
function createTelegramMessageContextSessionRuntimeForTest(
|
||||
storePath: string,
|
||||
): TelegramTestSessionRuntime {
|
||||
return {
|
||||
buildChannelInboundEventContext,
|
||||
readSessionUpdatedAt: () => undefined,
|
||||
recordInboundSession: async () => undefined,
|
||||
resolveInboundLastRouteSessionKey: ({ route, sessionKey }) =>
|
||||
route.lastRoutePolicy === "main" ? route.mainSessionKey : sessionKey,
|
||||
resolvePinnedMainDmOwnerFromAllowlist: () => null,
|
||||
resolveStorePath: () => storePath,
|
||||
};
|
||||
}
|
||||
|
||||
function installTelegramTopicNameStoreForTest() {
|
||||
setTelegramTopicNameStoreFactoryForTest((namespace) => {
|
||||
const entries = telegramTopicNameStoresForTest.get(namespace) ?? new Map();
|
||||
telegramTopicNameStoresForTest.set(namespace, entries);
|
||||
return {
|
||||
async register(key, value) {
|
||||
entries.set(key, value);
|
||||
},
|
||||
async entries() {
|
||||
return Array.from(entries, ([key, value]) => ({ key, value }));
|
||||
},
|
||||
async delete(key) {
|
||||
return entries.delete(key);
|
||||
},
|
||||
async clear() {
|
||||
entries.clear();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildTelegramMessageContextForTest(
|
||||
params: BuildTelegramMessageContextForTestParams,
|
||||
): Promise<
|
||||
Awaited<ReturnType<typeof import("./bot-message-context.js").buildTelegramMessageContext>>
|
||||
> {
|
||||
const { vi } = await loadVitestModule();
|
||||
const { expect, vi } = await loadVitestModule();
|
||||
const buildTelegramMessageContext = await loadBuildTelegramMessageContext();
|
||||
const sessionRuntime =
|
||||
params.sessionRuntime === null
|
||||
? undefined
|
||||
: {
|
||||
...telegramMessageContextSessionRuntimeForTest,
|
||||
...createTelegramMessageContextSessionRuntimeForTest(
|
||||
resolveSessionStorePathForTest(expect.getState().currentTestName),
|
||||
),
|
||||
...params.sessionRuntime,
|
||||
};
|
||||
return await buildTelegramMessageContext({
|
||||
@@ -119,6 +165,7 @@ async function loadVitestModule() {
|
||||
}
|
||||
|
||||
async function installMessageContextTestMocks() {
|
||||
installTelegramTopicNameStoreForTest();
|
||||
if (messageContextMocksInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -433,10 +433,15 @@ describe("agents delete command", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const expectedOpsWorkspace = path.join(
|
||||
await fs.realpath(path.dirname(opsWorkspace)),
|
||||
path.basename(opsWorkspace),
|
||||
);
|
||||
|
||||
await agentsDeleteCommand({ id: "ops", force: true, json: true }, runtime);
|
||||
|
||||
expect(fsSafeMocks.movePathToTrash).toHaveBeenCalledWith(opsWorkspace, {
|
||||
allowedRoots: [path.dirname(opsWorkspace)],
|
||||
expect(fsSafeMocks.movePathToTrash).toHaveBeenCalledWith(expectedOpsWorkspace, {
|
||||
allowedRoots: [path.dirname(expectedOpsWorkspace)],
|
||||
});
|
||||
expect(processMocks.runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -64,16 +64,15 @@ describe("formatOpenAIOAuthTlsPreflightFix", () => {
|
||||
code: "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
||||
message: "unable to get local issuer certificate",
|
||||
});
|
||||
expect(text).toBe(
|
||||
[
|
||||
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
||||
"Cause: UNABLE_TO_GET_ISSUER_CERT_LOCALLY (unable to get local issuer certificate)",
|
||||
"",
|
||||
"Fix (Homebrew Node/OpenSSL):",
|
||||
"- brew postinstall ca-certificates",
|
||||
"- brew postinstall openssl@3",
|
||||
"- Retry the OAuth login flow.",
|
||||
].join("\n"),
|
||||
expect(text).toContain(
|
||||
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
||||
);
|
||||
expect(text).toContain(
|
||||
"Cause: UNABLE_TO_GET_ISSUER_CERT_LOCALLY (unable to get local issuer certificate)",
|
||||
);
|
||||
expect(text).toContain("Fix (Homebrew Node/OpenSSL):");
|
||||
expect(text).toContain("- brew postinstall ca-certificates");
|
||||
expect(text).toContain("- brew postinstall openssl@3");
|
||||
expect(text).toContain("- Retry the OAuth login flow.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,6 +71,10 @@ function requireFirstRunCommandCall(): RunCommandCall {
|
||||
return call as RunCommandCall;
|
||||
}
|
||||
|
||||
function expectedTrashSourcePath(targetPath: string): string {
|
||||
return path.join(fs.realpathSync(path.dirname(targetPath)), path.basename(targetPath));
|
||||
}
|
||||
|
||||
describe("handleReset", () => {
|
||||
it("uses active profile paths for destructive reset targets", async () => {
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-reset-profile-"));
|
||||
@@ -95,6 +99,13 @@ describe("handleReset", () => {
|
||||
vi.stubEnv("OPENCLAW_CONFIG_PATH", profileConfigPath);
|
||||
|
||||
const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
|
||||
const expectedTrashedPaths = [
|
||||
profileConfigPath,
|
||||
profileCredentialsDir,
|
||||
profileSessionsDir,
|
||||
workspaceDir,
|
||||
].map(expectedTrashSourcePath);
|
||||
const expectedDefaultCredentialsDir = expectedTrashSourcePath(defaultCredentialsDir);
|
||||
|
||||
try {
|
||||
await handleReset("full", workspaceDir, runtime);
|
||||
@@ -103,13 +114,8 @@ describe("handleReset", () => {
|
||||
}
|
||||
|
||||
const trashedPaths = mocks.movePathToTrash.mock.calls.map(([targetPath]) => targetPath);
|
||||
expect(trashedPaths).toEqual([
|
||||
profileConfigPath,
|
||||
profileCredentialsDir,
|
||||
profileSessionsDir,
|
||||
workspaceDir,
|
||||
]);
|
||||
expect(trashedPaths).not.toContain(defaultCredentialsDir);
|
||||
expect(trashedPaths).toEqual(expectedTrashedPaths);
|
||||
expect(trashedPaths).not.toContain(expectedDefaultCredentialsDir);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,6 +125,7 @@ describe("moveToTrash", () => {
|
||||
const targetPath = path.join(testRoot, "target");
|
||||
fs.mkdirSync(targetPath, { recursive: true });
|
||||
const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
|
||||
const sourcePath = expectedTrashSourcePath(targetPath);
|
||||
|
||||
try {
|
||||
await moveToTrash(targetPath, runtime);
|
||||
@@ -126,8 +133,8 @@ describe("moveToTrash", () => {
|
||||
fs.rmSync(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
expect(mocks.movePathToTrash).toHaveBeenCalledWith(targetPath, {
|
||||
allowedRoots: [path.dirname(targetPath)],
|
||||
expect(mocks.movePathToTrash).toHaveBeenCalledWith(sourcePath, {
|
||||
allowedRoots: [path.dirname(sourcePath)],
|
||||
});
|
||||
expect(mocks.runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
expect(runtime.log).toHaveBeenCalledWith(`Moved to Trash: ${targetPath}`);
|
||||
|
||||
@@ -83,6 +83,21 @@ describe("matchesExecAllowlistPattern", () => {
|
||||
expect(matchesExecAllowlistPattern("/tmp/Allowed-Tool", "/tmp/Allowed-Tool")).toBe(true);
|
||||
});
|
||||
|
||||
it.runIf(process.platform === "darwin")("matches macOS /private/var temp aliases", () => {
|
||||
expect(
|
||||
matchesExecAllowlistPattern(
|
||||
"/var/folders/example/bin/tool",
|
||||
"/private/var/folders/example/bin/tool",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
matchesExecAllowlistPattern(
|
||||
"/private/var/folders/example/bin/tool",
|
||||
"/var/folders/example/bin/tool",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.runIf(process.platform === "win32")("preserves case-insensitive matching on Windows", () => {
|
||||
expect(matchesExecAllowlistPattern("C:/Tools/Allowed-Tool", "c:/tools/allowed-tool")).toBe(
|
||||
true,
|
||||
|
||||
@@ -11,7 +11,16 @@ function normalizeMatchTarget(value: string): string {
|
||||
const stripped = value.replace(/^\\\\[?.]\\/, "");
|
||||
return normalizeLowercaseStringOrEmpty(stripped.replace(/\\/g, "/"));
|
||||
}
|
||||
return value.replace(/\\\\/g, "/");
|
||||
const normalized = value.replace(/\\\\/g, "/");
|
||||
if (process.platform === "darwin") {
|
||||
if (normalized === "/private/var") {
|
||||
return "/var";
|
||||
}
|
||||
if (normalized.startsWith("/private/var/")) {
|
||||
return normalized.slice("/private".length);
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function tryRealpath(value: string): string | null {
|
||||
|
||||
@@ -10,6 +10,7 @@ const pluginSdkSubpathsCache = new Map();
|
||||
const pluginSdkPackageNames = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"];
|
||||
const pluginSdkSourceExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
|
||||
const privateQaExcludedPluginSdkSubpaths = new Set(["ssrf-runtime-internal"]);
|
||||
const DIAGNOSTIC_EVENTS_STATE_KEY = Symbol.for("openclaw.diagnosticEvents.state.v1");
|
||||
const isDistRootAlias = __filename.includes(
|
||||
`${path.sep}dist${path.sep}plugin-sdk${path.sep}root-alias.cjs`,
|
||||
);
|
||||
@@ -77,12 +78,94 @@ function resolveControlCommandGate(params) {
|
||||
return { commandAuthorized, shouldBlock };
|
||||
}
|
||||
|
||||
function createDiagnosticEventsState() {
|
||||
return {
|
||||
marker: DIAGNOSTIC_EVENTS_STATE_KEY,
|
||||
enabled: true,
|
||||
seq: 0,
|
||||
listeners: new Set(),
|
||||
dispatchDepth: 0,
|
||||
asyncQueue: [],
|
||||
asyncDrainScheduled: false,
|
||||
asyncDroppedEvents: 0,
|
||||
asyncDroppedTrustedEvents: 0,
|
||||
asyncDroppedUntrustedEvents: 0,
|
||||
asyncDroppedPriorityEvents: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function isDiagnosticEventsState(value) {
|
||||
return (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
value.marker === DIAGNOSTIC_EVENTS_STATE_KEY &&
|
||||
typeof value.enabled === "boolean" &&
|
||||
typeof value.seq === "number" &&
|
||||
value.listeners instanceof Set &&
|
||||
typeof value.dispatchDepth === "number" &&
|
||||
Array.isArray(value.asyncQueue) &&
|
||||
typeof value.asyncDrainScheduled === "boolean"
|
||||
);
|
||||
}
|
||||
|
||||
function getDiagnosticEventsState(create) {
|
||||
const existing = globalThis[DIAGNOSTIC_EVENTS_STATE_KEY];
|
||||
if (isDiagnosticEventsState(existing)) {
|
||||
existing.asyncDroppedEvents ??= 0;
|
||||
existing.asyncDroppedTrustedEvents ??= 0;
|
||||
existing.asyncDroppedUntrustedEvents ??= 0;
|
||||
existing.asyncDroppedPriorityEvents ??= 0;
|
||||
return existing;
|
||||
}
|
||||
if (!create) {
|
||||
return null;
|
||||
}
|
||||
const state = createDiagnosticEventsState();
|
||||
Object.defineProperty(globalThis, DIAGNOSTIC_EVENTS_STATE_KEY, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: state,
|
||||
writable: false,
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
function onDiagnosticEventFromSharedState(listener) {
|
||||
const state = getDiagnosticEventsState(true);
|
||||
const internalListener = (event, metadata) => {
|
||||
if (metadata && metadata.trusted) {
|
||||
return;
|
||||
}
|
||||
if (event && event.type === "log.record") {
|
||||
return;
|
||||
}
|
||||
listener(event);
|
||||
};
|
||||
state.listeners.add(internalListener);
|
||||
return () => {
|
||||
state.listeners.delete(internalListener);
|
||||
};
|
||||
}
|
||||
|
||||
function onDiagnosticEvent(listener) {
|
||||
const beforeState = getDiagnosticEventsState(false);
|
||||
const beforeSize = beforeState?.listeners?.size;
|
||||
const diagnosticEvents = loadDiagnosticEventsModule();
|
||||
if (!diagnosticEvents || typeof diagnosticEvents.onDiagnosticEvent !== "function") {
|
||||
throw new Error("openclaw/plugin-sdk root alias could not resolve onDiagnosticEvent");
|
||||
return onDiagnosticEventFromSharedState(listener);
|
||||
}
|
||||
return diagnosticEvents.onDiagnosticEvent(listener);
|
||||
const unsubscribeDiagnosticEvents = diagnosticEvents.onDiagnosticEvent(listener);
|
||||
const afterState = getDiagnosticEventsState(false);
|
||||
if (afterState && afterState.listeners.size > (beforeSize ?? 0)) {
|
||||
return unsubscribeDiagnosticEvents;
|
||||
}
|
||||
// Keep legacy root listeners connected when a built alias resolves the lazy
|
||||
// diagnostic module in a separate graph from the active core emitter.
|
||||
const unsubscribeSharedState = onDiagnosticEventFromSharedState(listener);
|
||||
return () => {
|
||||
unsubscribeDiagnosticEvents();
|
||||
unsubscribeSharedState();
|
||||
};
|
||||
}
|
||||
|
||||
function getPackageRoot() {
|
||||
|
||||
@@ -74,14 +74,15 @@ function loadRootAliasWithStubs(options?: {
|
||||
const monolithicExports = options?.monolithicExports ?? {
|
||||
slowHelper: () => "loaded",
|
||||
};
|
||||
const context = {
|
||||
process: {
|
||||
env: options?.env ?? {},
|
||||
platform: options?.platform ?? "darwin",
|
||||
},
|
||||
};
|
||||
const wrapper = vm.runInNewContext(
|
||||
`(function (exports, require, module, __filename, __dirname) {${rootAliasSource}\n})`,
|
||||
{
|
||||
process: {
|
||||
env: options?.env ?? {},
|
||||
platform: options?.platform ?? "darwin",
|
||||
},
|
||||
},
|
||||
context,
|
||||
{ filename: rootAliasPath },
|
||||
) as (
|
||||
exports: Record<string, unknown>,
|
||||
@@ -158,6 +159,7 @@ function loadRootAliasWithStubs(options?: {
|
||||
return createJitiOptions;
|
||||
},
|
||||
loadedSpecifiers,
|
||||
globalContext: context as Record<PropertyKey, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -523,6 +525,31 @@ describe("plugin-sdk root alias", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("bridges diagnostic listeners through shared process state when the lazy module is isolated", () => {
|
||||
const seen: string[] = [];
|
||||
const lazyModule = loadDiagnosticEventsAlias(["diagnostic-events-W3Hz61fI.js"]);
|
||||
const unsubscribe = (
|
||||
lazyModule.moduleExports.onDiagnosticEvent as (
|
||||
listener: (event: { type: string }) => void,
|
||||
) => () => void
|
||||
)((event) => {
|
||||
seen.push(event.type);
|
||||
});
|
||||
const state = lazyModule.globalContext[Symbol.for("openclaw.diagnosticEvents.state.v1")] as {
|
||||
listeners: Set<(event: { type: string }, metadata: { trusted: boolean }) => void>;
|
||||
};
|
||||
|
||||
for (const listener of state.listeners) {
|
||||
listener({ type: "model.usage" }, { trusted: false });
|
||||
listener({ type: "log.record" }, { trusted: false });
|
||||
listener({ type: "model.usage" }, { trusted: true });
|
||||
}
|
||||
unsubscribe();
|
||||
|
||||
expect(seen).toEqual(["model.usage"]);
|
||||
expect(state.listeners.size).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "forwards delegateCompactionToRuntime through the compat-backed root alias",
|
||||
|
||||
@@ -757,19 +757,30 @@ function matchesInstalledPluginRecord(params: {
|
||||
if (!record) {
|
||||
return false;
|
||||
}
|
||||
const resolvedCandidateSource = resolveUserPath(params.candidate.source, params.env);
|
||||
const candidateSource = safeRealpathSync(resolvedCandidateSource) ?? resolvedCandidateSource;
|
||||
const candidatePaths = [
|
||||
params.candidate.rootDir,
|
||||
params.candidate.packageDir,
|
||||
params.candidate.source,
|
||||
params.candidate.setupSource,
|
||||
]
|
||||
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
||||
.map((entry) => {
|
||||
const resolved = resolveUserPath(entry, params.env);
|
||||
return safeRealpathSync(resolved) ?? resolved;
|
||||
});
|
||||
const trackedPaths = [record.installPath, record.sourcePath]
|
||||
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
||||
.map((entry) => {
|
||||
const resolved = resolveUserPath(entry, params.env);
|
||||
return safeRealpathSync(resolved) ?? resolved;
|
||||
});
|
||||
if (trackedPaths.length === 0) {
|
||||
if (trackedPaths.length === 0 || candidatePaths.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return trackedPaths.some((trackedPath) => {
|
||||
return candidateSource === trackedPath || isPathInside(trackedPath, candidateSource);
|
||||
return candidatePaths.some((candidatePath) => {
|
||||
return trackedPaths.some((trackedPath) => {
|
||||
return candidatePath === trackedPath || isPathInside(trackedPath, candidatePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user