From 3bdfd60caaa70ac148863d8c6945544a25a5c2bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 16:40:55 -0700 Subject: [PATCH] fix(media): accept mixed-case local file URLs (#121226) --- extensions/msteams/src/media-helpers.test.ts | 1 + extensions/msteams/src/media-helpers.ts | 2 +- extensions/msteams/src/messenger.test.ts | 33 ++++++ package.json | 2 +- scripts/ci-changed-scope.mjs | 4 + src/channels/inbound-event/media.test.ts | 5 +- src/media/local-media-path.ts | 2 +- src/media/local-media-path.windows.test.ts | 111 +++++++++++++++++++ src/media/web-media.file-url.windows.test.ts | 48 ++++++++ src/media/web-media.ts | 2 +- src/scripts/ci-changed-scope.windows.test.ts | 23 ++++ test/package-scripts.test.ts | 14 +++ 12 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/media/local-media-path.windows.test.ts create mode 100644 src/media/web-media.file-url.windows.test.ts diff --git a/extensions/msteams/src/media-helpers.test.ts b/extensions/msteams/src/media-helpers.test.ts index bb22215e0159..258d7bc1e20c 100644 --- a/extensions/msteams/src/media-helpers.test.ts +++ b/extensions/msteams/src/media-helpers.test.ts @@ -164,6 +164,7 @@ describe("msteams media-helpers", () => { it("returns true for file:// URLs", () => { expect(isLocalPath("file:///tmp/image.png")).toBe(true); expect(isLocalPath("file://localhost/tmp/image.png")).toBe(true); + expect(isLocalPath("FILE:///C:/Users/test/image.png")).toBe(true); }); it("returns true for absolute paths", () => { diff --git a/extensions/msteams/src/media-helpers.ts b/extensions/msteams/src/media-helpers.ts index 484cb6c4e9cb..cec4a460494c 100644 --- a/extensions/msteams/src/media-helpers.ts +++ b/extensions/msteams/src/media-helpers.ts @@ -81,7 +81,7 @@ export async function extractFilename(url: string): Promise { * Check if a URL refers to a local file path. */ export function isLocalPath(url: string): boolean { - if (url.startsWith("file://") || url.startsWith("/") || url.startsWith("~")) { + if (/^file:\/\//iu.test(url) || url.startsWith("/") || url.startsWith("~")) { return true; } diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index a40ed93d3d44..80b178e924a7 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -1,6 +1,7 @@ // Msteams tests cover messenger plugin behavior. import { mkdtemp, rm, writeFile } from "node:fs/promises"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { PlatformMessageNotDispatchedError } from "openclaw/plugin-sdk/error-runtime"; import { SILENT_REPLY_TOKEN } from "openclaw/plugin-sdk/reply-chunking"; import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store"; @@ -399,6 +400,38 @@ describe("msteams messenger", () => { expect(sendActivity).not.toHaveBeenCalled(); }); + it("loads uppercase file URLs before sending personal images", async () => { + const tmpDir = await mkdtemp( + path.join(resolvePreferredOpenClawTmpDir(), "msteams-file-url-"), + ); + const localFile = path.join(tmpDir, "café image.png"); + const png = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=", + "base64", + ); + await writeFile(localFile, png); + + try { + const mediaUrl = pathToFileURL(localFile).href.replace(/^file:/u, "FILE:"); + const activity = await buildActivity( + { mediaUrl }, + { + ...baseRef, + conversation: { ...baseRef.conversation, conversationType: "personal" }, + }, + ); + const attachment = (activity.attachments as Array>)[0]; + + expect(attachment).toMatchObject({ + name: "café image.png", + contentType: "image/png", + contentUrl: `data:image/png;base64,${png.toString("base64")}`, + }); + } finally { + await rm(tmpDir, { recursive: true, force: true }); + } + }); + it("does not claim no dispatch after an earlier batch message was sent", async () => { const sendActivity = vi.fn(async () => ({ id: "sent-first" })); const missingPath = path.join(resolvePreferredOpenClawTmpDir(), "missing-second-file.txt"); diff --git a/package.json b/package.json index 788dc5a12843..84cc3af9ca4c 100644 --- a/package.json +++ b/package.json @@ -1954,7 +1954,7 @@ "test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts", "test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts", "test:watch": "node --import tsx scripts/test-projects.mts --watch", - "test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts", + "test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts", "test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts", "tool-display:check": "node --import tsx scripts/tool-display.ts --check", "tool-display:write": "node --import tsx scripts/tool-display.ts --write", diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index e4dac626384a..ffce43ee719b 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -55,6 +55,8 @@ const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/; const NODE_SCOPE_RE = /^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|\.github\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.oxlintrc\.json$|\.oxfmtrc\.jsonc$)/; const WINDOWS_SQLITE_SCOPE_RE = /^src\/(?:state\/|.*sqlite.*\.ts$)/; +const WINDOWS_FILE_URL_SCOPE_RE = + /^(?:src\/media\/(?:local-media-path(?:\.windows\.test)?|local-roots(?:\.test)?|web-media(?:\.file-url\.windows\.test)?)|src\/channels\/inbound-event\/media(?:\.test)?|src\/gateway\/managed-image-attachments(?:\.test)?|extensions\/msteams\/src\/(?:media-helpers|messenger)(?:\.test)?)\.ts$/; const WINDOWS_SCOPE_RE = /^(extensions\/mxc\/|src\/agents\/(?:bash-tools\.exec-script-(?:preflight|target)|bash-tools\.exec\.script-preflight\.test)\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive(?:\.worker(?:\.test)?)?|store\.session-lifecycle-mutation\.test)\.ts$|src\/process\/|src\/infra\/(?:(?:exec-allowlist-pattern|fs-safe-remove)(?:\.test)?|ssh-client(?:\.windows\.test)?|update-managed-service-handoff(?:-(?:command|lifecycle)\.test)?|windows-install-roots)\.ts$|src\/shared\/(?:import-specifier|runtime-import)(?:\.test)?\.ts$|src\/test-utils\/openclaw-test-state(?:\.test)?\.ts$|scripts\/(?:android-(?:app-i18n|pin-version)\.ts|ci-run-timings\.mjs|e2e\/lib\/package-compat\.mjs|generate-bundled-channel-config-metadata\.ts|install\.ps1|openclaw-cross-os-release-checks\.ts|plan-release-workflow-matrix\.mjs|run-additional-boundary-checks\.mts|verify-docker-attestations\.mjs|github\/run-openclaw-cross-os-release-checks\.sh|(?:npm-runner|pnpm-runner|ui|vitest-process-group)\.(?:mjs|mts|js)|lib\/(?:direct-run\.(?:mjs|mts)|format-generated-module\.mts|cross-os-release-checks\/[^/]+\.ts))$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|install-ps1|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github\/workflows\/(?:ci|openclaw-cross-os-release-checks-reusable)\.yml$|\.github\/actions\/setup-node-env\/action\.yml$|\.github\/actions\/setup-pnpm-store-cache\/action\.yml$)/; const WINDOWS_TEST_SCOPE_RE = @@ -170,10 +172,12 @@ export function detectChangedScope(changedPaths) { if ( (WINDOWS_SCOPE_RE.test(path) || WINDOWS_SQLITE_SCOPE_RE.test(path) || + WINDOWS_FILE_URL_SCOPE_RE.test(path) || WINDOWS_SECRETREF_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path)) && (!facts.isTestOnly || WINDOWS_TEST_SCOPE_RE.test(path) || + WINDOWS_FILE_URL_SCOPE_RE.test(path) || WINDOWS_SECRETREF_TEST_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path)) ) { diff --git a/src/channels/inbound-event/media.test.ts b/src/channels/inbound-event/media.test.ts index 224ae7d27b4b..5ca7f83714ab 100644 --- a/src/channels/inbound-event/media.test.ts +++ b/src/channels/inbound-event/media.test.ts @@ -1,4 +1,5 @@ // Inbound event media tests cover channel media attachment normalization. +import path from "node:path"; import { kindFromMime } from "@openclaw/media-core/mime"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -340,8 +341,8 @@ describe("channel inbound media facts", () => { ]); expect(probeMediaFilesWithinBudget).toHaveBeenCalledWith( [ - { filePath: "/tmp/voice.ogg", kind: "audio" }, - { filePath: "/tmp/clip.mp4", kind: "video" }, + { filePath: path.resolve("/tmp/voice.ogg"), kind: "audio" }, + { filePath: path.resolve("/tmp/clip.mp4"), kind: "video" }, ], { budgetMs: 3000, concurrency: 2, maxProbes: 8 }, ); diff --git a/src/media/local-media-path.ts b/src/media/local-media-path.ts index 32f1e58892e6..5faf75501d66 100644 --- a/src/media/local-media-path.ts +++ b/src/media/local-media-path.ts @@ -12,7 +12,7 @@ export function resolveLocalMediaPath(source: string): string | undefined { if (!trimmed || isPassThroughRemoteMediaSource(trimmed) || DATA_URL_RE.test(trimmed)) { return undefined; } - if (trimmed.startsWith("file://")) { + if (/^file:\/\//iu.test(trimmed)) { try { return safeFileURLToPath(trimmed); } catch { diff --git a/src/media/local-media-path.windows.test.ts b/src/media/local-media-path.windows.test.ts new file mode 100644 index 000000000000..0a65304f099d --- /dev/null +++ b/src/media/local-media-path.windows.test.ts @@ -0,0 +1,111 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createSolidPngBuffer } from "../../test/helpers/image-fixtures.js"; +import { toInboundMediaFactsWithMetadata } from "../channels/inbound-event/media.js"; +import { createManagedOutgoingMediaBlocks } from "../gateway/managed-image-attachments.js"; +import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; +import { withEnvAsync } from "../test-utils/env.js"; +import { resolveLocalMediaPath } from "./local-media-path.js"; +import { appendLocalMediaParentRoots } from "./local-roots.js"; + +const { probeMediaFilesWithinBudget } = vi.hoisted(() => ({ + probeMediaFilesWithinBudget: vi.fn(), +})); + +vi.mock("./media-probe.js", async (importOriginal) => ({ + ...(await importOriginal()), + probeMediaFilesWithinBudget, +})); + +function toUppercaseFileUrl(filePath: string): string { + return pathToFileURL(filePath).href.replace(/^file:/u, "FILE:"); +} + +async function withTempRoot(run: (root: string) => Promise): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-local-media-url-")); + try { + return await run(root); + } finally { + closeOpenClawStateDatabaseForTest(); + await fs.rm(root, { recursive: true, force: true }); + } +} + +describe.runIf(process.platform === "win32")("Windows local media file URLs", () => { + beforeEach(() => { + probeMediaFilesWithinBudget.mockReset(); + }); + + afterEach(() => { + closeOpenClawStateDatabaseForTest(); + }); + + it("resolves mixed-case local file schemes and rejects unsafe file URLs", async () => { + await withTempRoot(async (root) => { + const sourcePath = path.join(root, "Media Folder Ω", "photo.png"); + + expect(resolveLocalMediaPath(toUppercaseFileUrl(sourcePath))).toBe(sourcePath); + expect(resolveLocalMediaPath("FILE://server/share/photo.png")).toBeUndefined(); + expect(resolveLocalMediaPath("FILE:///C:/Media%2Fphoto.png")).toBeUndefined(); + }); + }); + + it("adds the exact parent root for an uppercase file URL", async () => { + await withTempRoot(async (root) => { + const sourcePath = path.join(root, "Media Folder Ω", "photo.png"); + + expect(appendLocalMediaParentRoots([], [toUppercaseFileUrl(sourcePath)])).toEqual([ + path.dirname(sourcePath), + ]); + }); + }); + + it("probes inbound metadata through an uppercase file URL", async () => { + await withTempRoot(async (root) => { + const sourcePath = path.join(root, "Media Folder Ω", "voice.mp3"); + await fs.mkdir(path.dirname(sourcePath), { recursive: true }); + await fs.writeFile(sourcePath, Buffer.from([0xff, 0xfb, 0x90, 0x00])); + probeMediaFilesWithinBudget.mockResolvedValueOnce([{ durationMs: 1250 }]); + + await expect( + toInboundMediaFactsWithMetadata([ + { path: toUppercaseFileUrl(sourcePath), contentType: "audio/mpeg" }, + ]), + ).resolves.toEqual([ + expect.objectContaining({ path: toUppercaseFileUrl(sourcePath), durationMs: 1250 }), + ]); + expect(probeMediaFilesWithinBudget).toHaveBeenCalledWith( + [{ filePath: sourcePath, kind: "audio" }], + { budgetMs: 3000, concurrency: 2, maxProbes: 8 }, + ); + }); + }); + + it("ingests an uppercase file URL into managed outgoing media", async () => { + await withTempRoot(async (root) => { + const sourcePath = path.join(root, "workspace", "Media Folder Ω", "photo.png"); + await fs.mkdir(path.dirname(sourcePath), { recursive: true }); + await fs.writeFile(sourcePath, createSolidPngBuffer(8, 8, { r: 24, g: 64, b: 128 })); + const sourceUrl = new URL(toUppercaseFileUrl(sourcePath)); + sourceUrl.searchParams.set("sig", "secret"); + sourceUrl.hash = "preview"; + + await withEnvAsync({ OPENCLAW_STATE_DIR: root }, async () => { + const blocks = await createManagedOutgoingMediaBlocks({ + stateDir: root, + sessionKey: "agent:main:main", + mediaUrls: [sourceUrl.href.replace(/^file:/u, "FILE:")], + localRoots: [path.join(root, "workspace")], + }); + + expect(blocks).toHaveLength(1); + expect(blocks[0]).toMatchObject({ type: "image", alt: "photo.png" }); + expect(JSON.stringify(blocks[0])).not.toContain(sourcePath); + expect(JSON.stringify(blocks[0])).not.toContain("sig=secret"); + }); + }); + }); +}); diff --git a/src/media/web-media.file-url.windows.test.ts b/src/media/web-media.file-url.windows.test.ts new file mode 100644 index 000000000000..a5c07fc42127 --- /dev/null +++ b/src/media/web-media.file-url.windows.test.ts @@ -0,0 +1,48 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createSolidPngBuffer } from "../../test/helpers/image-fixtures.js"; +import { loadWebMedia } from "./web-media.js"; + +const TINY_PNG = createSolidPngBuffer(1, 1, { r: 255, g: 255, b: 255 }); + +describe.runIf(process.platform === "win32")("Windows web media file URLs", () => { + let fixtureRoot = ""; + + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-web-media-file-url-")); + }); + + afterAll(async () => { + if (fixtureRoot) { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + } + }); + + it("loads uppercase file URLs with spaces and Unicode", async () => { + const filePath = path.join(fixtureRoot, "café image.png"); + await fs.writeFile(filePath, TINY_PNG); + const fileUrl = pathToFileURL(filePath).href.replace(/^file:/u, "FILE:"); + + const result = await loadWebMedia(fileUrl, { + maxBytes: 1024 * 1024, + localRoots: [fixtureRoot], + }); + + expect(result.buffer).toEqual(TINY_PNG); + expect(result.fileName).toBe("café image.png"); + }); + + it.each([ + "FILE://attacker/share/evil.png", + "FILE:///C:/safe/folder%2Fsecret.png", + "FILE:///C:/safe/folder%5Csecret.png", + "FILE:////attacker/share/evil.png", + ])("rejects unsafe uppercase file URL %s", async (fileUrl) => { + await expect(loadWebMedia(fileUrl, { localRoots: [fixtureRoot] })).rejects.toMatchObject({ + code: "invalid-file-url", + }); + }); +}); diff --git a/src/media/web-media.ts b/src/media/web-media.ts index 78dbeb9ddabf..b6429df00f36 100644 --- a/src/media/web-media.ts +++ b/src/media/web-media.ts @@ -1030,7 +1030,7 @@ async function loadWebMediaInternal( mediaUrl = stripLegacyMediaDirectivePrefix(mediaUrl); mediaUrl = (await resolveMediaStoreUriToPath(mediaUrl)) ?? mediaUrl; // Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.) - if (mediaUrl.startsWith("file://")) { + if (/^file:\/\//iu.test(mediaUrl)) { try { mediaUrl = safeFileURLToPath(mediaUrl); } catch (err) { diff --git a/src/scripts/ci-changed-scope.windows.test.ts b/src/scripts/ci-changed-scope.windows.test.ts index 1b2aca9a4be1..dce29144d041 100644 --- a/src/scripts/ci-changed-scope.windows.test.ts +++ b/src/scripts/ci-changed-scope.windows.test.ts @@ -115,6 +115,29 @@ describe("detectChangedScope Windows routing", () => { } }); + it("routes web and Teams file URL handling to Windows", () => { + for (const fileUrlPath of [ + "src/media/local-media-path.ts", + "src/media/local-media-path.windows.test.ts", + "src/media/local-roots.ts", + "src/media/local-roots.test.ts", + "src/media/web-media.ts", + "src/media/web-media.file-url.windows.test.ts", + "src/channels/inbound-event/media.ts", + "src/channels/inbound-event/media.test.ts", + "src/gateway/managed-image-attachments.ts", + "src/gateway/managed-image-attachments.test.ts", + "extensions/msteams/src/media-helpers.ts", + "extensions/msteams/src/media-helpers.test.ts", + "extensions/msteams/src/messenger.test.ts", + ]) { + expect(detectChangedScope([fileUrlPath]), fileUrlPath).toMatchObject({ + runNode: true, + runWindows: true, + }); + } + }); + it("routes SecretRef path-security changes and native fixtures to Windows", () => { for (const secretRefPath of [ "src/commands/doctor-gateway-auth-token.ts", diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index 23baacc33e16..476629985fe2 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -221,6 +221,12 @@ describe("package scripts", () => { ); }); + it("runs mixed-case local media file URL coverage in Windows CI", () => { + expect(readPackageJson().scripts["test:windows:ci"]).toContain( + "src/media/local-media-path.windows.test.ts", + ); + }); + it("runs the native OpenSSH resolver proof in Windows CI", () => { expect(readPackageJson().scripts["test:windows:ci"]).toContain( "src/infra/ssh-client.windows.test.ts", @@ -312,4 +318,12 @@ describe("package scripts", () => { "src/infra/fs-safe-remove.test.ts", ); }); + + it("runs web and Teams file URL coverage in Windows CI", () => { + const script = readPackageJson().scripts["test:windows:ci"]; + + expect(script).toContain("src/media/web-media.file-url.windows.test.ts"); + expect(script).toContain("extensions/msteams/src/media-helpers.test.ts"); + expect(script).toContain("extensions/msteams/src/messenger.test.ts"); + }); });