From 673b3f186c6b7828b5f285da095cfe6a017d4cb7 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Fri, 20 Mar 2026 00:49:16 +0300 Subject: [PATCH] CI: stabilize bundle hook and path tests --- package.json | 2 +- pnpm-lock.yaml | 6 ++--- src/hooks/workspace.ts | 24 +++++++++---------- .../channel-import-guardrails.test.ts | 5 ++++ src/plugins/bundle-mcp.test.ts | 9 +++++-- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 13f1bbbe38c..86cc04cbcbd 100644 --- a/package.json +++ b/package.json @@ -706,7 +706,6 @@ "file-type": "21.3.3", "gaxios": "7.1.4", "gigachat": "^0.0.18", - "grammy": "^1.41.1", "hono": "4.12.8", "ipaddr.js": "^2.3.0", "jiti": "^2.6.1", @@ -741,6 +740,7 @@ "@types/ws": "^8.18.1", "@typescript/native-preview": "7.0.0-dev.20260317.1", "@vitest/coverage-v8": "^4.1.0", + "grammy": "^1.41.1", "jscpd": "4.0.8", "jsdom": "^29.0.0", "lit": "^3.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f7a53f3028..a2018fafba4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,9 +101,6 @@ importers: gigachat: specifier: ^0.0.18 version: 0.0.18 - grammy: - specifier: ^1.41.1 - version: 1.41.1 hono: specifier: 4.12.8 version: 4.12.8 @@ -204,6 +201,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.1.0 version: 4.1.0(@vitest/browser@4.1.0(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.1.0))(vitest@4.1.0) + grammy: + specifier: ^1.41.1 + version: 1.41.1 jscpd: specifier: 4.0.8 version: 4.0.8 diff --git a/src/hooks/workspace.ts b/src/hooks/workspace.ts index 351690ab9d3..3cd6a3ca186 100644 --- a/src/hooks/workspace.ts +++ b/src/hooks/workspace.ts @@ -80,10 +80,17 @@ function loadHookFromDir(params: { pluginId?: string; nameHint?: string; }): Hook | null { - const hookMdPath = path.join(params.hookDir, "HOOK.md"); + let canonicalHookDir = path.resolve(params.hookDir); + try { + canonicalHookDir = fs.realpathSync.native(params.hookDir); + } catch { + // Fall back to the discovered path when realpath is unavailable. + } + + const hookMdPath = path.join(canonicalHookDir, "HOOK.md"); const content = readBoundaryFileUtf8({ absolutePath: hookMdPath, - rootPath: params.hookDir, + rootPath: canonicalHookDir, boundaryLabel: "hook directory", }); if (content === null) { @@ -98,10 +105,10 @@ function loadHookFromDir(params: { const handlerCandidates = ["handler.ts", "handler.js", "index.ts", "index.js"]; let handlerPath: string | undefined; for (const candidate of handlerCandidates) { - const candidatePath = path.join(params.hookDir, candidate); + const candidatePath = path.join(canonicalHookDir, candidate); const safeCandidatePath = resolveBoundaryFilePath({ absolutePath: candidatePath, - rootPath: params.hookDir, + rootPath: canonicalHookDir, boundaryLabel: "hook directory", }); if (safeCandidatePath) { @@ -115,20 +122,13 @@ function loadHookFromDir(params: { return null; } - let baseDir = params.hookDir; - try { - baseDir = fs.realpathSync.native(params.hookDir); - } catch { - // keep the discovered path when realpath is unavailable - } - return { name, description, source: params.source, pluginId: params.pluginId, filePath: hookMdPath, - baseDir, + baseDir: canonicalHookDir, handlerPath, }; } catch (err) { diff --git a/src/plugin-sdk/channel-import-guardrails.test.ts b/src/plugin-sdk/channel-import-guardrails.test.ts index 9b481097ed6..b138f5500bb 100644 --- a/src/plugin-sdk/channel-import-guardrails.test.ts +++ b/src/plugin-sdk/channel-import-guardrails.test.ts @@ -165,6 +165,10 @@ const LOCAL_EXTENSION_API_BARREL_EXCEPTIONS = [ // accounts.ts -> runtime-api.ts -> src/plugin-sdk/matrix -> extensions/matrix/api.ts -> accounts.ts "extensions/matrix/src/matrix/accounts.ts", ] as const; +const CORE_GUARDRAIL_EXCEPTIONS = [ + // Contract registries are test support, even though they live under src for co-located fixtures. + "src/channels/plugins/contracts/registry.ts", +] as const; function readSource(path: string): string { return readFileSync(resolve(ROOT_DIR, "..", path), "utf8"); @@ -267,6 +271,7 @@ function collectCoreSourceFiles(): string[] { normalizedFullPath.includes(".spec.") || normalizedFullPath.includes(".fixture.") || normalizedFullPath.includes(".snap") || + CORE_GUARDRAIL_EXCEPTIONS.some((suffix) => normalizedFullPath.endsWith(`/${suffix}`)) || // src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated // plugin-sdk guardrails instead of the generic "core should not touch extensions" rule. normalizedFullPath.includes(`${normalizedPluginSdkDir}/`) diff --git a/src/plugins/bundle-mcp.test.ts b/src/plugins/bundle-mcp.test.ts index b9d5ca18cf3..b9e3958c287 100644 --- a/src/plugins/bundle-mcp.test.ts +++ b/src/plugins/bundle-mcp.test.ts @@ -6,11 +6,16 @@ import { captureEnv } from "../test-utils/env.js"; import { isRecord } from "../utils.js"; import { loadEnabledBundleMcpConfig } from "./bundle-mcp.js"; import { createBundleMcpTempHarness, createBundleProbePlugin } from "./bundle-mcp.test-support.js"; +import { safeRealpathSync } from "./path-safety.js"; function getServerArgs(value: unknown): unknown[] | undefined { return isRecord(value) && Array.isArray(value.args) ? value.args : undefined; } +function canonicalizeExpectedBundlePath(value: string): string { + return safeRealpathSync(value) ?? path.resolve(value); +} + const tempHarness = createBundleMcpTempHarness(); afterEach(async () => { @@ -46,7 +51,7 @@ describe("loadEnabledBundleMcpConfig", () => { const loadedServer = loaded.config.mcpServers.bundleProbe; const loadedArgs = getServerArgs(loadedServer); const loadedServerPath = typeof loadedArgs?.[0] === "string" ? loadedArgs[0] : undefined; - const resolvedPluginRoot = await fs.realpath(pluginRoot); + const resolvedPluginRoot = canonicalizeExpectedBundlePath(pluginRoot); expect(loaded.diagnostics).toEqual([]); expect(isRecord(loadedServer) ? loadedServer.command : undefined).toBe("node"); @@ -178,7 +183,7 @@ describe("loadEnabledBundleMcpConfig", () => { }, }, }); - const resolvedPluginRoot = await fs.realpath(pluginRoot); + const resolvedPluginRoot = canonicalizeExpectedBundlePath(pluginRoot); expect(loaded.diagnostics).toEqual([]); expect(loaded.config.mcpServers.inlineProbe).toEqual({