From 5616639f2e16e6b8c687ee1fffdead1ce215141a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 06:15:39 +0900 Subject: [PATCH 1/8] =?UTF-8?q?Revert=20"=E2=9C=85=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E9=AB=98=E7=BD=AE=E4=BF=A1=E5=86=97=E4=BD=99=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=20(#1653)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit eeb8cb05c5a969c59a4276a7964933cffcfc7e5f. --- e2e/keep-alive.spec.ts | 40 +++++------ e2e/popup.spec.ts | 9 ++- e2e/script-editor.spec.ts | 18 +++++ e2e/script-management.spec.ts | 7 ++ packages/filesystem/s3/client.test.ts | 12 ++++ packages/filesystem/s3/s3.test.ts | 22 ++++++ packages/message/message_queue.test.ts | 39 ++++++++++- packages/message/server.test.ts | 58 ++++++++++++++++ packages/message/window_message.test.ts | 40 +++++++++++ .../service/agent/core/compact_prompt.test.ts | 9 +++ .../service/agent/core/content_utils.test.ts | 13 ++++ .../agent/core/mcp_tool_executor.test.ts | 19 +++++ .../agent/core/session_tool_registry.test.ts | 22 ++++++ .../agent/core/skill_script_executor.test.ts | 17 +++-- src/app/service/service_worker/script.ts | 2 +- src/locales/locales.test.ts | 13 ++++ src/pages/components/use-is-mobile.test.ts | 12 ++++ src/pages/confirm/confirm-options.test.ts | 17 ++++- .../install/components/InstallStates.test.tsx | 35 ++++++---- .../components/WatchingBanner.test.tsx | 5 ++ src/pages/install/useInstallData.test.ts | 16 ++--- src/pages/install/useInstallData.ts | 21 +++--- src/pages/options/layout/Sidebar.test.tsx | 5 ++ src/pages/options/onboarding/steps.test.ts | 13 ++-- .../routes/Agent/Chat/AskUserBlock.test.tsx | 5 ++ .../routes/Agent/Chat/MessageItem.test.tsx | 5 ++ .../routes/Agent/Chat/SubAgentBlock.test.tsx | 5 ++ .../routes/Agent/Chat/TaskListBlock.test.tsx | 5 ++ .../routes/Agent/Chat/ToolCallBlock.test.tsx | 5 ++ .../options/routes/Agent/Mcp/McpCard.test.tsx | 6 ++ .../Agent/components/AgentCardMenu.test.tsx | 17 +++++ .../routes/Agent/components/agentDocs.test.ts | 7 ++ .../Setting/sections/RuntimeSection.tsx | 4 +- src/pkg/backup/backup.test.ts | 69 +++++++++++++++++++ src/pkg/utils/async_queue.test.ts | 20 ++++++ src/pkg/utils/match.test.ts | 3 + src/pkg/utils/message_value.test.ts | 16 +++++ src/pkg/utils/regex_to_glob.test.ts | 14 ++++ src/pkg/utils/script.test.ts | 35 ++++++++++ src/pkg/utils/skill-md.test.ts | 55 +++++++++++++++ src/pkg/utils/skill-zip.test.ts | 55 +++++++++++++++ src/pkg/utils/skill_script.test.ts | 44 ++++++++++++ src/pkg/utils/url-utils.test.ts | 4 ++ src/pkg/utils/url_matcher.test.ts | 9 +++ src/pkg/utils/utils.test.ts | 9 +++ vitest.config.ts | 2 +- 46 files changed, 784 insertions(+), 74 deletions(-) create mode 100644 e2e/script-editor.spec.ts create mode 100644 src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx diff --git a/e2e/keep-alive.spec.ts b/e2e/keep-alive.spec.ts index 079d49e12..d2cc17f9c 100644 --- a/e2e/keep-alive.spec.ts +++ b/e2e/keep-alive.spec.ts @@ -2,21 +2,10 @@ import { test, expect } from "./fixtures"; import { openOptionsPage } from "./utils"; import type { CDPSession } from "@playwright/test"; +const KEEP_ALIVE_LABEL = "Keep Background and Scheduled Scripts Alive"; const SERVICE_WORKER_URL = "/service_worker.js"; const HEARTBEAT_VALIDATION_WINDOW_MS = 31_000; -const openRuntimeSettings = async (context: Parameters[0], extensionId: string) => { - const page = await openOptionsPage(context, extensionId); - await page - .getByTestId("view-toggle") - .or(page.getByTestId("mobile-search")) - .first() - .waitFor({ state: "visible", timeout: 30_000 }); - await page.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); - await expect(page.getByTestId("setting-page")).toBeVisible({ timeout: 20_000 }); - return page; -}; - type CdpTargetMessage = { sessionId: string; message: string; @@ -64,14 +53,17 @@ const sendTargetCommand = async ( test.describe("Chrome MV3 service worker keep-alive", () => { test("offscreen runtime heartbeat keeps the service worker active", async ({ context, extensionId }) => { - const optionsPage = await openRuntimeSettings(context, extensionId); + const optionsPage = await openOptionsPage(context, extensionId); + const cdp = await context.newCDPSession(optionsPage); try { - const keepAliveSwitch = optionsPage.getByTestId("keep-alive-switch"); - await keepAliveSwitch.scrollIntoViewIfNeeded(); + await optionsPage.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); + const label = optionsPage.getByText(KEEP_ALIVE_LABEL, { exact: true }); + await label.scrollIntoViewIfNeeded(); + + const keepAliveSwitch = label.locator("xpath=../..").getByRole("switch"); await expect(keepAliveSwitch).toBeVisible(); await expect(keepAliveSwitch).toHaveAttribute("aria-checked", "false"); - const cdp = await context.newCDPSession(optionsPage); await expect .poll( @@ -101,22 +93,24 @@ test.describe("Chrome MV3 service worker keep-alive", () => { }); test("disabling the setting allows the service worker to become idle", async ({ context, extensionId }) => { - const optionsPage = await openRuntimeSettings(context, extensionId); - let cdp: CDPSession | undefined; + const optionsPage = await openOptionsPage(context, extensionId); + const cdp = await context.newCDPSession(optionsPage); let offscreenSessionId: string | undefined; let nextCommandId = 1; try { - const keepAliveSwitch = optionsPage.getByTestId("keep-alive-switch"); - await keepAliveSwitch.scrollIntoViewIfNeeded(); + await optionsPage.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); + const label = optionsPage.getByText(KEEP_ALIVE_LABEL, { exact: true }); + await label.scrollIntoViewIfNeeded(); + + const keepAliveSwitch = label.locator("xpath=../..").getByRole("switch"); await expect(keepAliveSwitch).toBeVisible(); await expect(keepAliveSwitch).toHaveAttribute("aria-checked", "false"); - cdp = await context.newCDPSession(optionsPage); await expect .poll( async () => { - const { targetInfos } = await cdp!.send("Target.getTargets"); + const { targetInfos } = await cdp.send("Target.getTargets"); return targetInfos.some((target) => target.url.endsWith("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/src/offscreen.html")); }, { timeout: 15_000 } @@ -174,7 +168,7 @@ test.describe("Chrome MV3 service worker keep-alive", () => { .toBe(true); } finally { if (offscreenSessionId) { - await cdp!.send("Target.detachFromTarget", { sessionId: offscreenSessionId }); + await cdp.send("Target.detachFromTarget", { sessionId: offscreenSessionId }); } if (!optionsPage.isClosed()) await optionsPage.close(); } diff --git a/e2e/popup.spec.ts b/e2e/popup.spec.ts index 4ba45cf4e..4e8a55d13 100644 --- a/e2e/popup.spec.ts +++ b/e2e/popup.spec.ts @@ -1,9 +1,14 @@ import { test, expect } from "./fixtures"; import { openPopupPage } from "./utils"; -// new-ui popup(shadcn):全局 Radix Switch、Radix Accordion 分组、图标按钮(aria-label 设置/更多菜单)、 -// Radix DropdownMenu(role=menuitem)。 +// new-ui popup(shadcn):标题 h1、全局 Radix Switch、Radix Accordion 分组、 +// 图标按钮(aria-label 设置/更多菜单)、Radix DropdownMenu(role=menuitem)。 test.describe("Popup 页面", () => { + test("应加载并显示 ScriptCat 标题", async ({ context, extensionId }) => { + const page = await openPopupPage(context, extensionId); + await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 }); + }); + test("应显示全局脚本启用/禁用开关", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); // 顶部全局开关为 Radix Switch(role=switch) diff --git a/e2e/script-editor.spec.ts b/e2e/script-editor.spec.ts new file mode 100644 index 000000000..143e5f95f --- /dev/null +++ b/e2e/script-editor.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "./fixtures"; +import { openEditorPage, openOptionsPage, saveCurrentEditor } from "./utils"; + +// new-ui 脚本编辑器:路由 #/script/editor 加载空白模板(normal.tpl,含 ==UserScript==); +// Monaco 选择器(.monaco-editor/.view-lines) 为框架级不变;保存成功为 sonner toast。 +test.describe("Script 编辑器", () => { + test("保存后脚本应出现在列表中", async ({ context, extensionId }) => { + const editorPage = await openEditorPage(context, extensionId); + await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); + await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 }); + + await saveCurrentEditor(context, extensionId, editorPage); + + const listPage = await openOptionsPage(context, extensionId); + // 保存后列表非空(无空状态) + await expect(listPage.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); + }); +}); diff --git a/e2e/script-management.spec.ts b/e2e/script-management.spec.ts index f702b8eb0..5988fbbe8 100644 --- a/e2e/script-management.spec.ts +++ b/e2e/script-management.spec.ts @@ -18,6 +18,13 @@ async function createScriptAndGoToList(context: BrowserContext, extensionId: str } test.describe("脚本管理", () => { + test("创建脚本后应出现在列表中", async ({ context, extensionId }) => { + const page = await createScriptAndGoToList(context, extensionId); + // 列表非空(无空状态) + await expect(page.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); + await expect(page.getByRole("switch").first()).toBeVisible({ timeout: 10_000 }); + }); + test("应能切换脚本的启用/禁用", async ({ context, extensionId }) => { const page = await createScriptAndGoToList(context, extensionId); diff --git a/packages/filesystem/s3/client.test.ts b/packages/filesystem/s3/client.test.ts index bd9504278..94bdf9ca3 100644 --- a/packages/filesystem/s3/client.test.ts +++ b/packages/filesystem/s3/client.test.ts @@ -14,6 +14,18 @@ describe("S3Error", () => { expect(err.message).toBe("The specified key does not exist"); expect(err.statusCode).toBe(404); }); + + it("应当可被 try/catch 捕获并通过 instanceof 判断", () => { + try { + throw new S3Error("AccessDenied", "Access Denied", 403); + } catch (e) { + expect(e).toBeInstanceOf(S3Error); + if (e instanceof S3Error) { + expect(e.code).toBe("AccessDenied"); + expect(e.statusCode).toBe(403); + } + } + }); }); // ---- S3Client 构造函数与 getter 方法 ---- diff --git a/packages/filesystem/s3/s3.test.ts b/packages/filesystem/s3/s3.test.ts index e9bbb39d1..99484cf8f 100644 --- a/packages/filesystem/s3/s3.test.ts +++ b/packages/filesystem/s3/s3.test.ts @@ -123,6 +123,21 @@ describe("S3FileSystem", () => { // ---- open ---- describe("open", () => { + it("应当返回 S3FileReader", async () => { + const fileInfo: FileInfo = { + name: "test.txt", + path: "/docs", + size: 100, + digest: "abc", + createtime: 1000, + updatetime: 2000, + }; + const reader = await fs.open(fileInfo); + + expect(reader).toBeDefined(); + expect(reader.read).toBeTypeOf("function"); + }); + it("S3FileReader.read 应调用 client.request GET", async () => { const fileInfo: FileInfo = { name: "hello.txt", @@ -190,6 +205,13 @@ describe("S3FileSystem", () => { // ---- create ---- describe("create", () => { + it("应当返回 S3FileWriter", async () => { + const writer = await fs.create("test.txt"); + + expect(writer).toBeDefined(); + expect(writer.write).toBeTypeOf("function"); + }); + it("S3FileWriter.write 应调用 client.request PUT", async () => { (mockClient.request as ReturnType).mockResolvedValue(createMockResponse({ ok: true })); diff --git a/packages/message/message_queue.test.ts b/packages/message/message_queue.test.ts index 5d652c71a..9db04729f 100644 --- a/packages/message/message_queue.test.ts +++ b/packages/message/message_queue.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { MessageQueue, type IMessageQueue } from "./message_queue"; +import { MessageQueue, MessageQueueGroup, type IMessageQueue } from "./message_queue"; const nextTick = () => Promise.resolve().then(() => {}); @@ -16,6 +16,11 @@ describe("MessageQueueGroup", () => { }); describe("基本功能测试", () => { + it.concurrent("应该能够创建分组", () => { + const group = messageQueue.group("api-group"); + expect(group).toBeInstanceOf(MessageQueueGroup); + }); + it.concurrent("应该能够在分组中订阅和发布消息", () => { const group = messageQueue.group("api-sendBasic"); const handler = vi.fn(); @@ -264,4 +269,36 @@ describe("MessageQueueGroup", () => { expect(handler).toHaveBeenCalledTimes(1); }); }); + + describe("边界情况测试", () => { + it.concurrent("没有中间件的分组应该正常工作", () => { + const group = messageQueue.group("api-groupNoMiddleware"); + const handler = vi.fn(); + + group.subscribe("test-groupNoMiddleware", handler); + group.emit("test-groupNoMiddleware", { data: "test-groupNoMiddleware" }); + + expect(handler).toHaveBeenCalledWith({ data: "test-groupNoMiddleware" }); + }); + + it.concurrent("应该能够处理复杂的数据类型", () => { + const group = messageQueue.group("api-complexPayload"); + const handler = vi.fn(); + + const complexData = { + array: [1, 2, 3], + object: { nested: true }, + number: 42, + string: "test-complexPayload", + boolean: true, + null: null, + undefined: undefined, + }; + + group.subscribe("test-complexPayload", handler); + group.emit("test-complexPayload", complexData); + + expect(handler).toHaveBeenCalledWith(complexData); + }); + }); }); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index b7b1ff007..b58037491 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -66,6 +66,21 @@ describe("Server", () => { expect(response.data).toBe("sync response"); }); + it.concurrent("应该能够处理异步函数", async () => { + const mockHandler = vi.fn().mockResolvedValue("async response"); + + server.on("on-async", mockHandler); + + const response = await client.sendMessage({ + action: "api/on-async", + data: { param: "value-async" }, + }); + + expect(mockHandler).toHaveBeenCalledWith({ param: "value-async" }, expect.any(SenderRuntime)); + expect(response.code).toBe(0); + expect(response.data).toBe("async response"); + }); + it.concurrent("应该能够处理函数抛出的错误", async () => { const error = new Error("test error"); const mockHandler = vi.fn().mockImplementation(() => { @@ -382,6 +397,25 @@ describe("Server", () => { expect(handler).toHaveBeenCalledTimes(1); }); + it("没有中间件的 Group 应该正常工作", async () => { + const group = server.group("api"); + + const handler = vi.fn(async (params: any) => { + return { data: params }; + }); + + group.on("nomiddle", handler); + + const response = await client.sendMessage({ + action: "api/api/nomiddle", + data: { message: "hello" }, + }); + + expect(response.code).toBe(0); + expect(response.data).toEqual({ data: { message: "hello" } }); + expect(handler).toHaveBeenCalledTimes(1); + }); + it("中间件应该能够处理异步错误", async () => { const errorMiddleware = vi.fn(async (params: any, con: any, next: any) => { if (params.throwError) { @@ -621,6 +655,30 @@ describe("Server", () => { expect(response.data).toBe("empty response"); }); + it.concurrent("应该能够处理复杂的数据类型", async () => { + const complexData = { + array: [1, 2, 3], + object: { nested: true }, + number: 42, + string: "test", + boolean: true, + null: null, + undefined: undefined, + }; + + const mockHandler = vi.fn().mockImplementation((params) => params); + + server.on("on-complex", mockHandler); + + const response = await client.sendMessage({ + action: "api/on-complex", + data: complexData, + }); + + expect(response.code).toBe(0); + expect(response.data).toEqual(complexData); + }); + it.concurrent("应该能够处理返回 undefined 的函数", async () => { const mockHandler = vi.fn().mockReturnValue(undefined); diff --git a/packages/message/window_message.test.ts b/packages/message/window_message.test.ts index 00be4f8d7..6f3d636b4 100644 --- a/packages/message/window_message.test.ts +++ b/packages/message/window_message.test.ts @@ -133,6 +133,13 @@ describe("ServiceWorkerMessageSend", () => { }); describe("ServiceWorkerClientMessage", () => { + it("controller 可用时直接使用", () => { + const clientMsg = new ServiceWorkerClientMessage(); + + expect((clientMsg as any).sw).not.toBeNull(); + expect((clientMsg as any).sw.postMessage).toBe(swPostMessageMock); + }); + it("controller 为 null 时通过 ready 获取 active SW", async () => { const readyPostMessage = vi.fn(); Object.defineProperty(navigator, "serviceWorker", { @@ -327,6 +334,19 @@ describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", return { swSend, clientMsg }; } + it("sendMessage: client→SW 请求并收到响应", async () => { + const { swSend, clientMsg } = createWiredPair(); + + // SW 端注册处理器 + swSend.onMessage((msg: any, sendResponse: any) => { + sendResponse({ code: 0, data: (msg.data as string) + " world" }); + return true; + }); + + const result = await clientMsg.sendMessage({ action: "test/echo", data: "hello" }); + expect(result).toEqual({ code: 0, data: "hello world" }); + }); + it("connect: 建立连接后双向通信", async () => { const { swSend, clientMsg } = createWiredPair(); @@ -382,6 +402,26 @@ describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", expect(serverDisconnected).toBe(true); }); + it("sendMessage: 支持传输复杂对象(模拟结构化克隆场景)", async () => { + const { swSend, clientMsg } = createWiredPair(); + + swSend.onMessage((msg: any, sendResponse: any) => { + // 原样返回,验证数据完整性 + sendResponse({ code: 0, data: msg.data }); + return true; + }); + + const complexData = { + array: [1, 2, 3], + nested: { a: { b: "deep" } }, + nullVal: null, + boolVal: true, + }; + + const result = await clientMsg.sendMessage({ action: "test/complex", data: complexData }); + expect((result as any).data).toEqual(complexData); + }); + it("与 Server 集成: forwardMessage 路径", async () => { const swSend = new ServiceWorkerMessageSend(); const clientMsg = new ServiceWorkerClientMessage(); diff --git a/src/app/service/agent/core/compact_prompt.test.ts b/src/app/service/agent/core/compact_prompt.test.ts index 572e6f9a9..31bf49415 100644 --- a/src/app/service/agent/core/compact_prompt.test.ts +++ b/src/app/service/agent/core/compact_prompt.test.ts @@ -19,6 +19,15 @@ describe("extractSummary", () => { it("handles empty tags", () => { expect(extractSummary("")).toBe(""); }); + + it("handles multiline content inside ", () => { + const response = ` +Line 1 +Line 2 +Line 3 +`; + expect(extractSummary(response)).toBe("Line 1\nLine 2\nLine 3"); + }); }); describe("buildCompactUserPrompt", () => { diff --git a/src/app/service/agent/core/content_utils.test.ts b/src/app/service/agent/core/content_utils.test.ts index eb66fe331..2dd6cce39 100644 --- a/src/app/service/agent/core/content_utils.test.ts +++ b/src/app/service/agent/core/content_utils.test.ts @@ -8,6 +8,10 @@ describe("content_utils", () => { expect(getTextContent("hello world")).toBe("hello world"); }); + it("returns empty string for empty string", () => { + expect(getTextContent("")).toBe(""); + }); + it("extracts text from ContentBlock[]", () => { const blocks: ContentBlock[] = [ { type: "text", text: "Hello " }, @@ -25,6 +29,10 @@ describe("content_utils", () => { expect(getTextContent(blocks)).toBe(""); }); + it("returns empty string for empty ContentBlock[]", () => { + expect(getTextContent([])).toBe(""); + }); + it("handles audio blocks (skipped in text extraction)", () => { const blocks: ContentBlock[] = [ { type: "text", text: "Listen: " }, @@ -50,6 +58,11 @@ describe("content_utils", () => { ]; expect(normalizeContent(blocks)).toBe(blocks); }); + + it("returns empty array as-is", () => { + const blocks: ContentBlock[] = []; + expect(normalizeContent(blocks)).toBe(blocks); + }); }); describe("isContentBlocks", () => { diff --git a/src/app/service/agent/core/mcp_tool_executor.test.ts b/src/app/service/agent/core/mcp_tool_executor.test.ts index e8e5c1e74..d01415e63 100644 --- a/src/app/service/agent/core/mcp_tool_executor.test.ts +++ b/src/app/service/agent/core/mcp_tool_executor.test.ts @@ -19,6 +19,16 @@ describe("MCPToolExecutor", () => { expect(client.callTool).toHaveBeenCalledWith("search", { query: "hello" }); }); + it("应正确传递工具名", async () => { + const client = createMockClient({ data: [1, 2, 3] }); + const executor = new MCPToolExecutor(client, "fetch_data"); + + const result = await executor.execute({ limit: 10 }); + + expect(result).toEqual({ data: [1, 2, 3] }); + expect(client.callTool).toHaveBeenCalledWith("fetch_data", { limit: 10 }); + }); + it("callTool 抛出异常时应向上传播", async () => { const client = { callTool: vi.fn().mockRejectedValue(new Error("MCP error")), @@ -82,6 +92,15 @@ describe("MCPToolExecutor", () => { expect(result).toEqual(mcpContent); }); + it("非数组结果应原样返回", async () => { + const client = createMockClient("plain string result"); + const executor = new MCPToolExecutor(client, "simple_tool"); + + const result = await executor.execute({}); + + expect(result).toBe("plain string result"); + }); + it("image 缺少 mimeType 时应默认为 image/png", async () => { const mcpContent = [{ type: "image", data: "abc123" }]; const client = createMockClient(mcpContent); diff --git a/src/app/service/agent/core/session_tool_registry.test.ts b/src/app/service/agent/core/session_tool_registry.test.ts index 97a5704d0..f821086e0 100644 --- a/src/app/service/agent/core/session_tool_registry.test.ts +++ b/src/app/service/agent/core/session_tool_registry.test.ts @@ -259,6 +259,28 @@ describe("SessionToolRegistry", () => { expect(resA[0].result).toBe("fetched"); expect(resB[0].result).toBe("fetched"); }); + + it("session 释放(GC)后 parent 不受影响", () => { + const parent = new ToolRegistry(); + parent.registerBuiltin( + builtinDef, + createExecutor(async () => "") + ); + + // 创建临时 session 并让其超出作用域 + { + const session = new SessionToolRegistry(parent); + session.register( + "session", + taskDef, + createExecutor(async () => "") + ); + expect(session.listSessionTools()).toHaveLength(1); + } + + // parent 无任何 session 工具痕迹 + expect(parent.getDefinitions().map((d) => d.name)).toEqual(["web_fetch"]); + }); }); describe("脚本工具 miss-then-callback", () => { diff --git a/src/app/service/agent/core/skill_script_executor.test.ts b/src/app/service/agent/core/skill_script_executor.test.ts index 033bae2f1..a6d90d710 100644 --- a/src/app/service/agent/core/skill_script_executor.test.ts +++ b/src/app/service/agent/core/skill_script_executor.test.ts @@ -233,12 +233,19 @@ return result;`, }); }); +describe("getSkillScriptNameByUuid", () => { + it("未注册的 UUID 应返回空字符串", () => { + expect(getSkillScriptNameByUuid("skillscript-unknown-uuid")).toBe(""); + }); + + it("空字符串应返回空字符串", () => { + expect(getSkillScriptNameByUuid("")).toBe(""); + }); +}); + describe("getSkillScriptGrantsByUuid", () => { - it("未注册的 UUID 应返回空工具名和权限列表", () => { - for (const uuid of ["unregistered", ""]) { - expect(getSkillScriptNameByUuid(uuid)).toBe(""); - expect(getSkillScriptGrantsByUuid(uuid)).toEqual([]); - } + it("未注册的 UUID 应返回空数组", () => { + expect(getSkillScriptGrantsByUuid("skillscript-unknown-uuid")).toEqual([]); }); it("执行期间应能通过 UUID 获取 grants", async () => { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 985067df2..e75e5469e 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -323,7 +323,7 @@ export class ScriptService { action: { type: "redirect" as chrome.declarativeNetRequest.RuleActionType, redirect: { - regexSubstitution: `${installPageURL}?byWebRequest=1&url=\\1`, + regexSubstitution: `${installPageURL}?url=\\1`, }, }, condition: condition, diff --git a/src/locales/locales.test.ts b/src/locales/locales.test.ts index 4dbba146f..1ac16cf16 100644 --- a/src/locales/locales.test.ts +++ b/src/locales/locales.test.ts @@ -120,4 +120,17 @@ describe.concurrent("i18nDescription", () => { const result = i18nDescription(script); expect(result).toBe(""); }); + + it("description 字段为空数组时返回 空字串", () => { + i18n.language = "en-US"; + + const script = { + metadata: { + description: [], + } as SCMetadata, + }; + + const result = i18nDescription(script); + expect(result).toBe(""); + }); }); diff --git a/src/pages/components/use-is-mobile.test.ts b/src/pages/components/use-is-mobile.test.ts index 4a8d8f69f..07e3e369f 100644 --- a/src/pages/components/use-is-mobile.test.ts +++ b/src/pages/components/use-is-mobile.test.ts @@ -32,6 +32,18 @@ function stubMatchMedia(initialMatches: boolean, matchesOnSubscribe?: boolean) { } describe("useIsMobile 视口断点", () => { + it("视口 < 768px 时返回 true", () => { + stubMatchMedia(true); + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(true); + }); + + it("视口 ≥ 768px 时返回 false", () => { + stubMatchMedia(false); + const { result } = renderHook(() => useIsMobile()); + expect(result.current).toBe(false); + }); + it("监听 change 事件,视口变化时更新返回值", () => { const mql = stubMatchMedia(false); const { result } = renderHook(() => useIsMobile()); diff --git a/src/pages/confirm/confirm-options.test.ts b/src/pages/confirm/confirm-options.test.ts index f717938a4..22739c1a3 100644 --- a/src/pages/confirm/confirm-options.test.ts +++ b/src/pages/confirm/confirm-options.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vitest"; -import { resolveConfirmType, availableDurations, canApplyToAll, isHighSensitive } from "./confirm-options"; +import { + resolveConfirmType, + availableDurations, + canApplyToAll, + isSiteAccess, + isHighSensitive, +} from "./confirm-options"; import type { ConfirmParam } from "@App/app/service/service_worker/permission_verify"; const cp = (over: Partial = {}): ConfirmParam => ({ permission: "cors", ...over }); @@ -49,3 +55,12 @@ describe("授权选项 · 高敏感权限警示", () => { expect(isHighSensitive(cp({ permission: "file_storage" }))).toBe(false); }); }); + +describe("授权选项 · 站点访问识别", () => { + it("extension-site-access 应识别为站点访问(单按钮变体)", () => { + expect(isSiteAccess(cp({ permission: "extension-site-access" }))).toBe(true); + }); + it("其它权限不是站点访问", () => { + expect(isSiteAccess(cp({ permission: "cors" }))).toBe(false); + }); +}); diff --git a/src/pages/install/components/InstallStates.test.tsx b/src/pages/install/components/InstallStates.test.tsx index ede3749f8..c8f561c00 100644 --- a/src/pages/install/components/InstallStates.test.tsx +++ b/src/pages/install/components/InstallStates.test.tsx @@ -39,23 +39,32 @@ describe("InstallError 加载失败状态屏", () => { expect(screen.getByText("Error: Fetch failed with status 404")).toBeInTheDocument(); }); - it("提供重试和自定义标题时可分别触发重试与关闭", () => { + it("保留顶部品牌栏(对照设计稿,失败态不丢失外壳)", () => { + render( {}} />); + expect(screen.getByTestId("install-top-bar")).toBeInTheDocument(); + }); + + it("提供 onRetry 时渲染重试按钮并可点击", () => { const onRetry = vi.fn(); - const onClose = vi.fn(); - const { rerender } = render(); + render( {}} />); + fireEvent.click(screen.getByText("重试").closest("button")!); + expect(onRetry).toHaveBeenCalledTimes(1); + }); - expect(screen.getByText("无效安装地址")).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "重试" })); - fireEvent.click(screen.getByRole("button", { name: "关闭" })); - expect(onRetry).toHaveBeenCalledOnce(); - expect(onClose).toHaveBeenCalledOnce(); + it("未提供 onRetry 时不渲染重试按钮", () => { + render( {}} />); + expect(screen.queryByText("重试")).not.toBeInTheDocument(); + }); - rerender(); - expect(screen.queryByRole("button", { name: "重试" })).not.toBeInTheDocument(); + it("点击关闭触发 onClose", () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByText("关闭").closest("button")!); + expect(onClose).toHaveBeenCalledTimes(1); }); - it("保留顶部品牌栏(对照设计稿,失败态不丢失外壳)", () => { - render( {}} />); - expect(screen.getByTestId("install-top-bar")).toBeInTheDocument(); + it("可自定义标题(用于无效页面)", () => { + render( {}} />); + expect(screen.getByText("无效页面")).toBeInTheDocument(); }); }); diff --git a/src/pages/install/components/WatchingBanner.test.tsx b/src/pages/install/components/WatchingBanner.test.tsx index 837746737..ce1cb4085 100644 --- a/src/pages/install/components/WatchingBanner.test.tsx +++ b/src/pages/install/components/WatchingBanner.test.tsx @@ -8,6 +8,11 @@ beforeAll(() => initTestLanguage("zh-CN")); afterEach(cleanup); describe("WatchingBanner 文件监听横幅", () => { + it("渲染监听横幅容器", () => { + render(); + expect(screen.getByTestId("watching-banner")).toBeInTheDocument(); + }); + it("提供最后同步时间时渲染时间戳区", () => { render(); expect(screen.getByTestId("watching-last-sync")).toBeInTheDocument(); diff --git a/src/pages/install/useInstallData.test.ts b/src/pages/install/useInstallData.test.ts index 502aac12d..162c126a2 100644 --- a/src/pages/install/useInstallData.test.ts +++ b/src/pages/install/useInstallData.test.ts @@ -249,8 +249,8 @@ describe("useInstallData 数据流编排", () => { expect(state.view.oldCode).toBe("// old code"); }); - describe("安装成功后离开安装页:独立新标签应关闭,网页链接接管的原标签应返回上一页", () => { - const setupReady = async (paramOptions: Record = {}) => { + describe("安装成功后离开安装页:独立新标签应关闭,同标签内被重定向而来应返回上一页", () => { + const setupReady = async () => { window.history.replaceState({}, "", "/install.html?uuid=u1"); const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; const info: ScriptInfo = { @@ -261,7 +261,7 @@ describe("useInstallData 数据流编排", () => { metadata, source: "user", }; - (scriptClient.getInstallInfo as Mock).mockResolvedValue([false, info, paramOptions]); + (scriptClient.getInstallInfo as Mock).mockResolvedValue([false, info, {}]); (getTempCode as Mock).mockResolvedValue("// code"); (prepareScriptByCode as Mock).mockResolvedValue({ script: makeAction(metadata) }); (scriptClient.install as Mock).mockResolvedValue(undefined); @@ -270,11 +270,11 @@ describe("useInstallData 数据流编排", () => { return result; }; - it("独立新标签即使 history.length > 1 也应 window.close()", async () => { + it("history.length 为 1(以新标签打开)时应 window.close()", async () => { const result = await setupReady(); const closeSpy = vi.spyOn(window, "close").mockImplementation(() => {}); const backSpy = vi.spyOn(window.history, "back").mockImplementation(() => {}); - vi.spyOn(window.history, "length", "get").mockReturnValue(2); + vi.spyOn(window.history, "length", "get").mockReturnValue(1); await act(async () => { await result.current.install(); @@ -286,11 +286,11 @@ describe("useInstallData 数据流编排", () => { expect(backSpy).not.toHaveBeenCalled(); }); - it("byWebRequest 入口即使 history.length 为 1 也应 history.back() 而非关闭标签", async () => { - const result = await setupReady({ byWebRequest: true }); + it("history.length > 1(同一标签被就地重定向而来)时应 history.back() 而非关闭标签", async () => { + const result = await setupReady(); const closeSpy = vi.spyOn(window, "close").mockImplementation(() => {}); const backSpy = vi.spyOn(window.history, "back").mockImplementation(() => {}); - vi.spyOn(window.history, "length", "get").mockReturnValue(1); + vi.spyOn(window.history, "length", "get").mockReturnValue(2); await act(async () => { await result.current.install(); diff --git a/src/pages/install/useInstallData.ts b/src/pages/install/useInstallData.ts index f578a48dc..81ba5daa2 100644 --- a/src/pages/install/useInstallData.ts +++ b/src/pages/install/useInstallData.ts @@ -124,19 +124,19 @@ const buildScriptInfo = (uuid: string, code: string, url: string, metadata: SCMe source: "user", }); -// 安装页可能是专为安装打开的新标签,也可能由网页脚本链接接管用户原标签。 -// history.length 无法区分两者:扩展新标签也可能继承多条历史,因此必须使用入口携带的 -// byWebRequest 信号;后者若直接 window.close() 会连带关掉用户本来在看的页面。 +// 安装页可能是专为安装打开的新标签(history.length === 1,关闭无损), +// 也可能是由 declarativeNetRequest 就地重定向而来的用户原浏览标签(history.length > 1), +// 后者若直接 window.close() 会连带关掉用户本来在看的页面,应改为返回上一页。 // install()/close() 等可能在短时间内被重复触发(如用户连续点击、close 与 install 的 // setTimeout 前后脚打到),leaveInstallPageRunning 防止 back()/close() 被并发调用多次; // 推到 requestAnimationFrame 里执行,让触发它的那次交互(如按钮点击态)先完成一帧渲染。 let leaveInstallPageRunning = false; -const leaveInstallPage = (byWebRequest: boolean) => { +const leaveInstallPage = () => { if (leaveInstallPageRunning) return; leaveInstallPageRunning = true; requestAnimationFrame(() => { leaveInstallPageRunning = false; - if (byWebRequest) { + if (window.history.length > 1) { window.history.back(); } else { window.close(); @@ -190,7 +190,6 @@ export function useInstallData(): UseInstallData { const infoRef = useRef(null); const handleRef = useRef(null); const skillUuidRef = useRef(null); - const byWebRequestRef = useRef(false); useEffect(() => { const params = new URLSearchParams(location.search); @@ -200,7 +199,6 @@ export function useInstallData(): UseInstallData { const fid = params.get("file"); const urlIdx = location.search.indexOf("url="); const rawUrl = !uuid && urlIdx !== -1 ? location.search.slice(urlIdx + 4) : null; - byWebRequestRef.current = params.get("byWebRequest") === "1"; let cancelled = false; const failed = (e: unknown) => { @@ -266,7 +264,6 @@ export function useInstallData(): UseInstallData { const code = await getTempCode(uuid); if (code === undefined) throw new Error(t("install:script_info_load_failed")); info.code = code; - byWebRequestRef.current = cached?.[2]?.byWebRequest === true; await loadFromInfo(info, !!cached?.[0], cached?.[2] || {}); } else if (rawUrl) { // .cat.md URL → Skill 安装流程(DNR 把 *.cat.md 重定向到安装页),不走脚本解析;仅 agent 启用时 @@ -369,7 +366,7 @@ export function useInstallData(): UseInstallData { await scriptClient.install({ script, code: info.code }); notify.success(t("install:success")); } - if (closeAfterInstall) setTimeout(() => leaveInstallPage(byWebRequestRef.current), 300); + if (closeAfterInstall) setTimeout(() => leaveInstallPage(), 300); } catch (e) { notify.error(`${t("install:failed")}: ${(e as Error)?.message || String(e)}`); } @@ -393,7 +390,7 @@ export function useInstallData(): UseInstallData { if (opts?.noMoreUpdates && info && !info.userSubscribe) { void scriptClient.setCheckUpdateUrl(info.uuid, false); } - leaveInstallPage(byWebRequestRef.current); + leaveInstallPage(); }, []); // 监听文件变更后自动重装,并刷新视图代码 @@ -450,7 +447,7 @@ export function useInstallData(): UseInstallData { try { await agentClient.completeSkillInstall(uuid); notify.success(t("install:success")); - setTimeout(() => leaveInstallPage(byWebRequestRef.current), 300); + setTimeout(() => leaveInstallPage(), 300); } catch (e) { notify.error(`${t("install:failed")}: ${(e as Error)?.message || String(e)}`); } @@ -459,7 +456,7 @@ export function useInstallData(): UseInstallData { const cancelSkill = useCallback(() => { const uuid = skillUuidRef.current; if (uuid) void agentClient.cancelSkillInstall(uuid); - leaveInstallPage(byWebRequestRef.current); + leaveInstallPage(); }, []); // 重新触发加载(供加载失败后的重试按钮) diff --git a/src/pages/options/layout/Sidebar.test.tsx b/src/pages/options/layout/Sidebar.test.tsx index 4292fcf2e..f89c21d09 100644 --- a/src/pages/options/layout/Sidebar.test.tsx +++ b/src/pages/options/layout/Sidebar.test.tsx @@ -36,6 +36,11 @@ const subLabels = () => [ ]; describe("Sidebar 侧边栏 AI Agent 菜单", () => { + it("渲染 AI Agent 子菜单入口", () => { + const { getByText } = renderSidebar(); + expect(getByText(t("agent:title"))).toBeInTheDocument(); + }); + it("默认折叠,点击 AI Agent 后展开显示 7 个子项", () => { const { getByText, queryByTestId, getByTestId } = renderSidebar(); expect(queryByTestId("sidebar-agent-submenu")).toBeNull(); diff --git a/src/pages/options/onboarding/steps.test.ts b/src/pages/options/onboarding/steps.test.ts index 9d8946b21..53ebc4468 100644 --- a/src/pages/options/onboarding/steps.test.ts +++ b/src/pages/options/onboarding/steps.test.ts @@ -2,6 +2,14 @@ import { describe, it, expect } from "vitest"; import { DESKTOP_STEPS, MOBILE_STEPS } from "./steps"; describe("巡览步骤配置", () => { + it("桌面应有 6 个步骤", () => { + expect(DESKTOP_STEPS).toHaveLength(6); + }); + + it("移动端应为更精简的 3 个步骤", () => { + expect(MOBILE_STEPS).toHaveLength(3); + }); + it("每个步骤都应带 guide 命名空间的标题与正文 key", () => { for (const s of [...DESKTOP_STEPS, ...MOBILE_STEPS]) { expect(s.titleKey.startsWith("guide:")).toBe(true); @@ -14,9 +22,4 @@ describe("巡览步骤配置", () => { const allowed = new Set([...DESKTOP_STEPS.map((s) => s.id), "subscribe"]); for (const s of MOBILE_STEPS) expect(allowed.has(s.id)).toBe(true); }); - - it("桌面与移动端分别保留 6 步和 3 步巡览", () => { - expect(DESKTOP_STEPS).toHaveLength(6); - expect(MOBILE_STEPS).toHaveLength(3); - }); }); diff --git a/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx b/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx index 9af62320e..17fd3e9d8 100644 --- a/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx @@ -7,6 +7,11 @@ beforeAll(() => initTestLanguage("zh-CN")); afterEach(() => cleanup()); describe("用户提问块 AskUserBlock", () => { + it("展示问题文本", () => { + render(); + expect(screen.getByText("选择一个颜色")).toBeInTheDocument(); + }); + it("单选点击选项后立即提交该选项", () => { const onRespond = vi.fn(); render(); diff --git a/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx b/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx index 9b8a131e6..7e26f4c3f 100644 --- a/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx +++ b/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx @@ -18,6 +18,11 @@ const msg = (over: Partial & Pick) }); describe("用户消息 UserMessageItem", () => { + it("展示用户文本气泡", () => { + render(); + expect(screen.getByText("你好世界")).toBeInTheDocument(); + }); + it("编辑后保存触发 onEdit 携带新文本", () => { const onEdit = vi.fn(); render(); diff --git a/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx b/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx index d7b222841..b037f8bf8 100644 --- a/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx @@ -19,6 +19,11 @@ const state = (over?: Partial): SubAgentState => ({ }); describe("子代理块 SubAgentBlock", () => { + it("展示子代理描述", () => { + render(); + expect(screen.getByText("搜索资料")).toBeInTheDocument(); + }); + it("依据 isRunning 标注运行/完成状态", () => { const { rerender } = render(); expect(screen.getByTestId("subagent-status").dataset.running).toBe("true"); diff --git a/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx b/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx index ea18481b2..38e464b3d 100644 --- a/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx @@ -30,4 +30,9 @@ describe("任务清单块 TaskListBlock", () => { expect(screen.getByTestId("task-a").dataset.status).toBe("completed"); expect(screen.getByTestId("task-b").dataset.status).toBe("pending"); }); + + it("展示每个任务的标题", () => { + render(); + expect(screen.getByText("抓取首页")).toBeInTheDocument(); + }); }); diff --git a/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx b/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx index 1a86f570d..8bb90b4f1 100644 --- a/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx @@ -16,6 +16,11 @@ const tc = (overrides?: Partial): ToolCall => ({ }); describe("工具调用块 ToolCallBlock", () => { + it("始终展示工具名称", () => { + render(); + expect(screen.getByText("web_search")).toBeInTheDocument(); + }); + it("默认折叠,不展示参数", () => { render(); expect(screen.queryByText(/"query":"天气"/)).toBeNull(); diff --git a/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx b/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx index e54de40b3..05f71cbe3 100644 --- a/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx +++ b/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx @@ -18,6 +18,12 @@ const server = { function noop() {} describe("McpCard MCP 服务器卡片", () => { + it("展示名称与 URL", () => { + render(); + expect(screen.getByText("本地工具")).toBeInTheDocument(); + expect(screen.getByText("http://localhost:8080/mcp")).toBeInTheDocument(); + }); + it("点击开关触发 onToggle", () => { const onToggle = vi.fn(); render(); diff --git a/src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx b/src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx new file mode 100644 index 000000000..315cf4c07 --- /dev/null +++ b/src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx @@ -0,0 +1,17 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, cleanup, screen, fireEvent } from "@testing-library/react"; +import { Pencil } from "lucide-react"; +import { AgentCardMenu } from "./AgentCardMenu"; + +afterEach(() => cleanup()); + +describe("AgentCardMenu 卡片菜单", () => { + it("点击菜单项触发 onSelect", () => { + const onSelect = vi.fn(); + render(); + // Radix 触发器在 pointerdown(左键) 时展开菜单——真实点击即包含此事件 + fireEvent.pointerDown(screen.getByTestId("card-menu"), { button: 0 }); + fireEvent.click(screen.getByTestId("card-menu-edit")); + expect(onSelect).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/pages/options/routes/Agent/components/agentDocs.test.ts b/src/pages/options/routes/Agent/components/agentDocs.test.ts index 1e4f57257..025e906e6 100644 --- a/src/pages/options/routes/Agent/components/agentDocs.test.ts +++ b/src/pages/options/routes/Agent/components/agentDocs.test.ts @@ -10,4 +10,11 @@ describe("agentDocUrl 文档深链", () => { expect(agentDocUrl("opfs")).toBe("https://docs.scriptcat.org/docs/dev/agent/agent-opfs"); expect(agentDocUrl("settings")).toBe("https://docs.scriptcat.org/docs/dev/agent/agent"); }); + + it("文档链接均不是站点根(确保深链)", () => { + for (const page of ["provider", "skills", "mcp", "tasks", "opfs", "settings"] as const) { + expect(agentDocUrl(page)).not.toBe("https://docs.scriptcat.org"); + expect(agentDocUrl(page)).toContain("/docs/dev/agent/"); + } + }); }); diff --git a/src/pages/options/routes/Setting/sections/RuntimeSection.tsx b/src/pages/options/routes/Setting/sections/RuntimeSection.tsx index 4a1d9ec3e..5fb6bb420 100644 --- a/src/pages/options/routes/Setting/sections/RuntimeSection.tsx +++ b/src/pages/options/routes/Setting/sections/RuntimeSection.tsx @@ -155,7 +155,7 @@ export function RuntimeSection({ register }: { register: (id: string) => (el: HT label={t("settings:keep_scripts_alive.title")} description={t("settings:keep_scripts_alive.description")} > - + )} @@ -165,7 +165,7 @@ export function RuntimeSection({ register }: { register: (id: string) => (el: HT label={t("settings:keep_scripts_alive.title")} description={t("settings:keep_scripts_alive.description")} > - + )} diff --git a/src/pkg/backup/backup.test.ts b/src/pkg/backup/backup.test.ts index a58eb5b87..fec61d9c9 100644 --- a/src/pkg/backup/backup.test.ts +++ b/src/pkg/backup/backup.test.ts @@ -114,6 +114,75 @@ describe.concurrent("backup", () => { expect(resp).toEqual(data); }); + it.concurrent("export and import script - name and version only", async () => { + const zipFile = createJSZip(); + const fs = new ZipFileSystem(zipFile); + const data: BackupData = { + script: [ + { + code: `// ==UserScript== + // @name New Userscript + // @version 1 + // ==/UserScript== + + console.log('hello world')`, + options: { + options: {}, + meta: { + name: "test", + modified: 1, + file_url: "", + }, + settings: { + enabled: true, + position: 1, + }, + }, + resources: [ + { + meta: { name: "test1", mimetype: "text/plain" }, + base64: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + source: "hello world", + }, + ], + requires: [ + { + meta: { name: "test2", mimetype: "text/plain" }, + base64: "data:text/plain;base64,aGVsbG8gd29ybGQ=", + source: "hello world", + }, + ], + requiresCss: [ + { + meta: { name: "test3", mimetype: "application/javascript" }, + base64: "data:application/javascript;base64,aGVsbG8gd29ybGQ=", + source: "hello world", + }, + ], + storage: { + ts: ts0 + 2, + data: { + num: 1, + str: "data", + bool: false, + }, + }, + lastModificationDate: expect.any(Number), + }, + ], + subscribe: [], + } as unknown as BackupData; + await new BackupExport(fs).export(data); + expect(data.script[0].storage.data.num).toEqual("n1"); + expect(data.script[0].storage.data.str).toEqual("sdata"); + expect(data.script[0].storage.data.bool).toEqual("bfalse"); + const resp = await parseBackupZipFile(zipFile); + data.script[0].storage.data.num = 1; + data.script[0].storage.data.str = "data"; + data.script[0].storage.data.bool = false; + expect(resp).toEqual(data); + }); + it.concurrent("export and import script - 2 scripts", async () => { const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); diff --git a/src/pkg/utils/async_queue.test.ts b/src/pkg/utils/async_queue.test.ts index 42623db50..22a70ce60 100644 --- a/src/pkg/utils/async_queue.test.ts +++ b/src/pkg/utils/async_queue.test.ts @@ -347,4 +347,24 @@ describe.concurrent("stackAsyncTask 测试", () => { expect(order).toEqual([0, 1, 2, 3, 4]); expect(results).toEqual([0, 1, 2, 3, 4]); }); + + /* ------------------- 7. 跨 key 链接(正确 await 返回值) ------------------- */ + it.concurrent("【7】跨 key 链接:内部任务返回值可被外层 await(不 await stackAsyncTask)", async () => { + const kOuter = generateKey("outer"); + const kInner = generateKey("inner"); + + const pOuter = stackAsyncTask(kOuter, async () => { + const pInner = stackAsyncTask(kInner, async () => { + return "inner-data"; + }); + const data = await pInner; // 正确:await 返回值 + return `outer(${data})`; + }); + + setupBlockingTask(kOuter).resolve(); + setupBlockingTask(kInner).resolve(); + await flush(); + + await expect(pOuter).resolves.toBe("outer(inner-data)"); + }); }); diff --git a/src/pkg/utils/match.test.ts b/src/pkg/utils/match.test.ts index a7f7ccbfd..6a13ceea4 100644 --- a/src/pkg/utils/match.test.ts +++ b/src/pkg/utils/match.test.ts @@ -205,6 +205,9 @@ describe.concurrent("UrlMatch-google", () => { expect(url.urlMatch("https://www.google.com/foo/baz/bar")).toEqual(["ok1", "ok2", "ok3"]); expect(url.urlMatch("https://docs.google.com/foobar")).toEqual(["ok1", "ok2", "ok3"]); }); + it.concurrent("match4", () => { + expect(url.urlMatch("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]); + }); it.concurrent("match5", () => { expect(url.urlMatch("http://127.0.0.1/")).toEqual(["ok5"]); expect(url.urlMatch("http://127.0.0.1/foo/bar.html")).toEqual(["ok5"]); diff --git a/src/pkg/utils/message_value.test.ts b/src/pkg/utils/message_value.test.ts index 77f66d4da..f0c10e01a 100644 --- a/src/pkg/utils/message_value.test.ts +++ b/src/pkg/utils/message_value.test.ts @@ -55,6 +55,13 @@ describe.concurrent("encodeRValue 编码函数", () => { expect(encoded[0]).toBe(RType.STANDARD); expect(encoded[1]).toBe(big); }); + + it.concurrent("应正确处理联合类型的编码", () => { + const value: string | null = "联合类型测试"; + const encoded = encodeRValue(value); + expect(encoded[0]).toBe(RType.STANDARD); + expect(encoded[1]).toBe(value); + }); }); describe.concurrent("decodeRValue 解码函数", () => { @@ -138,4 +145,13 @@ describe.concurrent("encodeRValue 与 decodeRValue 组合行为", () => { } }); }); + + it.concurrent("应对联合类型值进行正确的往返编码解码", () => { + type Union = string | number | null | undefined; + const values: Union[] = [undefined, null, 1, 0, 123, "abc", ""]; + + const roundTrip = values.map((v) => decodeRValue(encodeRValue(v))); + + expect(roundTrip).toEqual(values); + }); }); diff --git a/src/pkg/utils/regex_to_glob.test.ts b/src/pkg/utils/regex_to_glob.test.ts index 81bd2cc7d..db7232715 100644 --- a/src/pkg/utils/regex_to_glob.test.ts +++ b/src/pkg/utils/regex_to_glob.test.ts @@ -404,5 +404,19 @@ describe.concurrent("regexToGlob - comprehensive test suite (regrouped & comment ok("(cat|car|cap)\\.txt", "ca?.txt"); // prior pattern with fixed suffix ok("v(?:\\d{2}|latest)", "v??*"); // min 2 digits, or 'latest' -> '??*' }); + + it.concurrent("8.8 invalid regex from additional remain null", () => { + // 继续确保无效正则 → null + // Still invalid → null + // 注:呼叫regexToGlob前,已经用 new RegExp 生成,所以不会出现非法RegEx字串 + bad("(ab"); + bad("test\\"); + bad("([a-z]"); // unbalanced () and [] + // bad("(?P\\w+)"); // unsupported named group (PCRE-style) + // bad("(?'name'\\w+)"); // unsupported named group (alternate syntax) + // bad("(?|a|b)"); // branch reset group (PCRE), unsupported + // bad("a**"); // consecutive quantifiers invalid + // bad("*"); // bare quantifier invalid + }); }); }); diff --git a/src/pkg/utils/script.test.ts b/src/pkg/utils/script.test.ts index 2c1cf044b..d21e95365 100644 --- a/src/pkg/utils/script.test.ts +++ b/src/pkg/utils/script.test.ts @@ -386,6 +386,41 @@ console.log('Hello World'); expect(result?.author).toEqual([""]); }); + it.concurrent("正確解析元数据(空version)", () => { + const code = ` +// ==UserScript== +// @name 测试脚本 +// @namespace http://tampermonkey.net/ +// @match https://example.org/* +// @match https://test.com/* +// @match https://demo.com/* +// @description +// @early-start +// @author +// @match https://example.com/* +// @grant + GM_setValue +// @grant GM_getValue +// ==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual([ + "https://example.org/*", + "https://test.com/*", + "https://demo.com/*", + "https://example.com/*", + ]); + expect(result?.["early-start"]).toEqual([""]); + expect(result?.grant).toEqual(["", "GM_getValue"]); + expect(result?.description).toEqual([""]); + expect(result?.author).toEqual([""]); + }); + it.concurrent("正確解析元数据(換行空白1)", () => { const code = ` // ==UserScript== diff --git a/src/pkg/utils/skill-md.test.ts b/src/pkg/utils/skill-md.test.ts index 1b93194d4..82a7685a0 100644 --- a/src/pkg/utils/skill-md.test.ts +++ b/src/pkg/utils/skill-md.test.ts @@ -158,6 +158,36 @@ Prompt.`; expect(result.metadata.references).toBeUndefined(); }); + it("应正确解析完整的 SKILL.cat.md(含 version + scripts + references + config)", () => { + const content = `--- +name: price-compare +description: 多平台比价 +version: 2.0.0 +scripts: + - compare.js +references: + - api_docs.md +config: + api_key: + title: API Key + type: text + secret: true +--- + +# Price Compare + +比价工具使用说明。`; + + const result = parseSkillMd(content)!; + expect(result.metadata.name).toBe("price-compare"); + expect(result.metadata.version).toBe("2.0.0"); + expect(result.metadata.scripts).toEqual(["compare.js"]); + expect(result.metadata.references).toEqual(["api_docs.md"]); + expect(result.metadata.config).toBeDefined(); + expect(result.metadata.config!.api_key.secret).toBe(true); + expect(result.prompt).toContain("# Price Compare"); + }); + it("scripts 中过滤非字符串值", () => { const content = `--- name: filter-test @@ -294,4 +324,29 @@ Prompt.`; const result = parseSkillMd(content)!; expect(result.metadata.config).toBeUndefined(); }); + + it("应正确处理多行 prompt 内容", () => { + const content = `--- +name: multi-line +description: test +--- + +# Title + +Paragraph 1. + +## Subtitle + +- item 1 +- item 2 + +\`\`\`js +console.log("hello"); +\`\`\``; + + const result = parseSkillMd(content)!; + expect(result.prompt).toContain("# Title"); + expect(result.prompt).toContain("- item 1"); + expect(result.prompt).toContain('console.log("hello");'); + }); }); diff --git a/src/pkg/utils/skill-zip.test.ts b/src/pkg/utils/skill-zip.test.ts index 6273397be..cf1dd2bc0 100644 --- a/src/pkg/utils/skill-zip.test.ts +++ b/src/pkg/utils/skill-zip.test.ts @@ -190,4 +190,59 @@ description: 淘宝购物助手 expect(toolMeta!.params[0].name).toBe("pageType"); expect(toolMeta!.params[1].name).toBe("tabId"); }); + + it("ZIP 解析输出结构与 installSkill 参数签名一致", async () => { + const zipData = await createTestZip({ + "SKILL.md": `---\nname: sig-test\ndescription: Signature test\n---\nPrompt.`, + "scripts/helper.js": VALID_SKILLSCRIPT_CODE, + "references/doc.md": "Doc content", + }); + + const result = await parseSkillZip(zipData); + + // 验证结构:skillMd 是 string,scripts 是 {name, code}[],references 是 {name, content}[] + expect(typeof result.skillMd).toBe("string"); + expect(Array.isArray(result.scripts)).toBe(true); + expect(Array.isArray(result.references)).toBe(true); + + for (const s of result.scripts) { + expect(typeof s.name).toBe("string"); + expect(typeof s.code).toBe("string"); + expect(s.name).toBeTruthy(); + expect(s.code).toBeTruthy(); + } + + for (const r of result.references) { + expect(typeof r.name).toBe("string"); + expect(typeof r.content).toBe("string"); + expect(r.name).toBeTruthy(); + expect(r.content).toBeTruthy(); + } + }); + + it("嵌套目录 ZIP 的完整流程:解析 → 验证 SKILL.md → 验证 SkillScript", async () => { + const zipData = await createTestZip({ + "taobao-skill/SKILL.md": `---\nname: nested-skill\ndescription: 嵌套目录测试\n---\n嵌套 Skill 提示词。`, + "taobao-skill/scripts/extract.js": VALID_SKILLSCRIPT_CODE, + "taobao-skill/references/guide.txt": "使用指南内容", + }); + + const zipResult = await parseSkillZip(zipData); + + // Step 1: SKILL.md 正确 + const parsed = parseSkillMd(zipResult.skillMd); + expect(parsed).not.toBeNull(); + expect(parsed!.metadata.name).toBe("nested-skill"); + + // Step 2: SkillScript 正确 + expect(zipResult.scripts).toHaveLength(1); + const toolMeta = parseSkillScriptMetadata(zipResult.scripts[0].code); + expect(toolMeta).not.toBeNull(); + expect(toolMeta!.name).toBe("taobao_extract"); + + // Step 3: references 正确 + expect(zipResult.references).toHaveLength(1); + expect(zipResult.references[0].name).toBe("guide.txt"); + expect(zipResult.references[0].content).toBe("使用指南内容"); + }); }); diff --git a/src/pkg/utils/skill_script.test.ts b/src/pkg/utils/skill_script.test.ts index ad4d36754..0faddd502 100644 --- a/src/pkg/utils/skill_script.test.ts +++ b/src/pkg/utils/skill_script.test.ts @@ -71,6 +71,20 @@ return args.value * 2; expect(meta.params[1].required).toBe(false); }); + it("应正确解析无参数的工具", () => { + const code = ` +// ==SkillScript== +// @name ping +// @description 测试连通性 +// ==/SkillScript== +return "pong"; +`; + const meta = parseSkillScriptMetadata(code)!; + expect(meta.name).toBe("ping"); + expect(meta.params).toHaveLength(0); + expect(meta.grants).toHaveLength(0); + }); + it("应正确解析多个 @grant", () => { const code = ` // ==SkillScript== @@ -86,6 +100,19 @@ return "ok"; expect(meta.grants).toEqual(["GM.xmlHttpRequest", "GM.getValue", "GM.setValue"]); }); + it("应正确解析单个 @require URL", () => { + const code = ` +// ==SkillScript== +// @name xlsx_tool +// @description 生成 Excel +// @require https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js +// ==/SkillScript== +return XLSX.utils.book_new(); +`; + const meta = parseSkillScriptMetadata(code)!; + expect(meta.requires).toEqual(["https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"]); + }); + it("应正确解析多个 @require URL", () => { const code = ` // ==SkillScript== @@ -302,4 +329,21 @@ return x;`; const body = getSkillScriptBody(code); expect(body).toBe("const x = 1;\nreturn x;"); }); + + it("应保留元数据头后面的所有代码", () => { + const code = `// ==SkillScript== +// @name test +// @description 测试 +// @param city string [required] 城市 +// @grant GM.xmlHttpRequest +// ==/SkillScript== + +const result = await GM.xmlHttpRequest({url: "http://example.com/" + args.city}); +const data = JSON.parse(result.responseText); +return data;`; + const body = getSkillScriptBody(code); + expect(body).toContain("const result = await GM.xmlHttpRequest"); + expect(body).toContain("return data;"); + expect(body).not.toContain("==SkillScript=="); + }); }); diff --git a/src/pkg/utils/url-utils.test.ts b/src/pkg/utils/url-utils.test.ts index 263bea744..13e29f457 100644 --- a/src/pkg/utils/url-utils.test.ts +++ b/src/pkg/utils/url-utils.test.ts @@ -21,6 +21,10 @@ describe.concurrent("prettyUrl", () => { it.concurrent("should decode Emoji domains", () => { expect(prettyUrl("https://xn--vi8h.la/path")).toBe("https://🍕.la/path"); }); + + it.concurrent("should handle mixed Latin and Foreign scripts", () => { + expect(prettyUrl("http://xn--maana-pta.com")).toBe("http://mañana.com/"); + }); }); describe.concurrent("Path and Percent Encoding", () => { diff --git a/src/pkg/utils/url_matcher.test.ts b/src/pkg/utils/url_matcher.test.ts index 62e7781fc..bc3f36c81 100644 --- a/src/pkg/utils/url_matcher.test.ts +++ b/src/pkg/utils/url_matcher.test.ts @@ -999,4 +999,13 @@ describe.concurrent("embeddedPatternChecker", () => { const code3 = embeddedPatternCheckerString('"https://example.com/secret/data"', JSON.stringify(reduced)); expect(eval(code3)).toBe(false); }); + + it.concurrent("embeddedPatternCheckerString 生成可执行代码", () => { + const patterns = extractUrlPatterns(["@match *://example.com/*"]); + const reduced = patterns.map(({ ruleType, ruleContent }) => ({ ruleType, ruleContent })); + const codeStr = embeddedPatternCheckerString("location.href", JSON.stringify(reduced)); + // 验证生成的是一个函数调用表达式字符串(IIFE 形式) + expect(typeof codeStr).toBe("string"); + expect(codeStr).toContain("location.href"); + }); }); diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index 2a3762870..3add3ea08 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -572,6 +572,10 @@ describe.concurrent("normalizeResponseHeaders", () => { expect(normalizeResponseHeaders("")).toBe(""); }); + it.concurrent("returns empty string for falsy-like empty string (only case possible with string type)", () => { + expect(normalizeResponseHeaders(String(""))).toBe(""); + }); + it.concurrent("keeps valid header lines and outputs name:value joined with CRLF", () => { const input = "Content-Type: text/plain\nX-Test: abc\n"; expect(normalizeResponseHeaders(input)).toBe("Content-Type:text/plain\r\nX-Test:abc"); @@ -597,6 +601,11 @@ describe.concurrent("normalizeResponseHeaders", () => { expect(normalizeResponseHeaders(input)).toBe("X-名前:値"); }); + it.concurrent("does not include a trailing CRLF at the end of output", () => { + const input = "A: 1\nB: 2\n"; + expect(normalizeResponseHeaders(input).endsWith("\r\n")).toBe(false); + }); + it.concurrent("standard test", () => { const input = `content-type: text/html; charset=utf-8\r\n server: Apache/2.4.41 (Ubuntu)\r\n diff --git a/vitest.config.ts b/vitest.config.ts index 01f9c8ef7..f21eb862f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -31,7 +31,7 @@ const ISOLATED = [ "src/app/service/content/exec_script.test.ts", ]; -const BASE_EXCLUDE = ["**/node_modules/**", "**/.claude/**", "**/.dev-kit/**", "e2e/**"]; +const BASE_EXCLUDE = ["**/node_modules/**", "**/.claude/**", "e2e/**"]; // 页面层(React 渲染,含 .ts 的 renderHook 测试)用例的真实 solo 成本在覆盖率下可达 100–200ms, // 乘上 worker 并行负载后 340ms 预算必然偶发超时(本地满载观测峰值 ~630ms); From bbdd6800b9e24851495ca4e08407ff1df01320da Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:18:26 +0900 Subject: [PATCH 2/8] fix: preserve web-request install provenance --- src/app/service/service_worker/script.ts | 2 +- src/pages/install/useInstallData.test.ts | 36 ++++++++++++++++++------ src/pages/install/useInstallData.ts | 25 +++++++++------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index e75e5469e..985067df2 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -323,7 +323,7 @@ export class ScriptService { action: { type: "redirect" as chrome.declarativeNetRequest.RuleActionType, redirect: { - regexSubstitution: `${installPageURL}?url=\\1`, + regexSubstitution: `${installPageURL}?byWebRequest=1&url=\\1`, }, }, condition: condition, diff --git a/src/pages/install/useInstallData.test.ts b/src/pages/install/useInstallData.test.ts index 162c126a2..aae5da100 100644 --- a/src/pages/install/useInstallData.test.ts +++ b/src/pages/install/useInstallData.test.ts @@ -185,6 +185,7 @@ describe("assembleInstallView 组装安装视图", () => { describe("useInstallData 数据流编排", () => { afterEach(() => { + vi.restoreAllMocks(); vi.clearAllMocks(); window.history.replaceState({}, "", "/install.html"); }); @@ -250,7 +251,7 @@ describe("useInstallData 数据流编排", () => { }); describe("安装成功后离开安装页:独立新标签应关闭,同标签内被重定向而来应返回上一页", () => { - const setupReady = async () => { + const setupReady = async (paramOptions: Record = {}) => { window.history.replaceState({}, "", "/install.html?uuid=u1"); const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; const info: ScriptInfo = { @@ -261,7 +262,7 @@ describe("useInstallData 数据流编排", () => { metadata, source: "user", }; - (scriptClient.getInstallInfo as Mock).mockResolvedValue([false, info, {}]); + (scriptClient.getInstallInfo as Mock).mockResolvedValue([false, info, paramOptions]); (getTempCode as Mock).mockResolvedValue("// code"); (prepareScriptByCode as Mock).mockResolvedValue({ script: makeAction(metadata) }); (scriptClient.install as Mock).mockResolvedValue(undefined); @@ -270,11 +271,11 @@ describe("useInstallData 数据流编排", () => { return result; }; - it("history.length 为 1(以新标签打开)时应 window.close()", async () => { + it("独立新标签即使 history.length > 1 也应 window.close()", async () => { const result = await setupReady(); const closeSpy = vi.spyOn(window, "close").mockImplementation(() => {}); const backSpy = vi.spyOn(window.history, "back").mockImplementation(() => {}); - vi.spyOn(window.history, "length", "get").mockReturnValue(1); + vi.spyOn(window.history, "length", "get").mockReturnValue(2); await act(async () => { await result.current.install(); @@ -286,21 +287,40 @@ describe("useInstallData 数据流编排", () => { expect(backSpy).not.toHaveBeenCalled(); }); - it("history.length > 1(同一标签被就地重定向而来)时应 history.back() 而非关闭标签", async () => { - const result = await setupReady(); + it("byWebRequest 入口即使 history.length 为 1 也应 history.back() 而非关闭标签", async () => { + const result = await setupReady({ byWebRequest: true }); const closeSpy = vi.spyOn(window, "close").mockImplementation(() => {}); const backSpy = vi.spyOn(window.history, "back").mockImplementation(() => {}); - vi.spyOn(window.history, "length", "get").mockReturnValue(2); + vi.spyOn(window.history, "length", "get").mockReturnValue(1); await act(async () => { await result.current.install(); - // leaveInstallPage 延后到 install() 里 300ms 的 setTimeout 再叠一帧 rAF 才真正执行,多等一点确保已触发 await new Promise((r) => setTimeout(r, 320)); }); expect(backSpy).toHaveBeenCalledOnce(); expect(closeSpy).not.toHaveBeenCalled(); }); + + it("byWebRequest 的直接 URL 入口应把来源标记传给脚本匹配", async () => { + window.history.replaceState({}, "", "/install.html?byWebRequest=1&url=https://e.com/x.user.js"); + const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; + (fetchScriptBody as Mock).mockResolvedValue("// code"); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockResolvedValue({ script: makeAction(metadata) }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + + expect(prepareScriptByCode).toHaveBeenCalledWith( + "// code", + "https://e.com/x.user.js", + undefined, + false, + undefined, + { byWebRequest: true } + ); + }); }); it("?skill= 时读取技能数据进入 skill 状态", async () => { diff --git a/src/pages/install/useInstallData.ts b/src/pages/install/useInstallData.ts index 81ba5daa2..44f1894c1 100644 --- a/src/pages/install/useInstallData.ts +++ b/src/pages/install/useInstallData.ts @@ -124,19 +124,19 @@ const buildScriptInfo = (uuid: string, code: string, url: string, metadata: SCMe source: "user", }); -// 安装页可能是专为安装打开的新标签(history.length === 1,关闭无损), -// 也可能是由 declarativeNetRequest 就地重定向而来的用户原浏览标签(history.length > 1), -// 后者若直接 window.close() 会连带关掉用户本来在看的页面,应改为返回上一页。 +// 安装页可能是专为安装打开的新标签,也可能由网页脚本链接接管用户原标签。 +// history.length 无法区分两者:扩展新标签也可能继承多条历史,因此必须使用入口携带的 +// byWebRequest 信号;后者若直接 window.close() 会连带关掉用户本来在看的页面。 // install()/close() 等可能在短时间内被重复触发(如用户连续点击、close 与 install 的 // setTimeout 前后脚打到),leaveInstallPageRunning 防止 back()/close() 被并发调用多次; // 推到 requestAnimationFrame 里执行,让触发它的那次交互(如按钮点击态)先完成一帧渲染。 let leaveInstallPageRunning = false; -const leaveInstallPage = () => { +const leaveInstallPage = (byWebRequest: boolean) => { if (leaveInstallPageRunning) return; leaveInstallPageRunning = true; requestAnimationFrame(() => { leaveInstallPageRunning = false; - if (window.history.length > 1) { + if (byWebRequest) { window.history.back(); } else { window.close(); @@ -190,6 +190,7 @@ export function useInstallData(): UseInstallData { const infoRef = useRef(null); const handleRef = useRef(null); const skillUuidRef = useRef(null); + const byWebRequestRef = useRef(false); useEffect(() => { const params = new URLSearchParams(location.search); @@ -199,6 +200,7 @@ export function useInstallData(): UseInstallData { const fid = params.get("file"); const urlIdx = location.search.indexOf("url="); const rawUrl = !uuid && urlIdx !== -1 ? location.search.slice(urlIdx + 4) : null; + byWebRequestRef.current = params.get("byWebRequest") === "1"; let cancelled = false; const failed = (e: unknown) => { @@ -264,6 +266,7 @@ export function useInstallData(): UseInstallData { const code = await getTempCode(uuid); if (code === undefined) throw new Error(t("install:script_info_load_failed")); info.code = code; + byWebRequestRef.current = cached?.[2]?.byWebRequest === true; await loadFromInfo(info, !!cached?.[0], cached?.[2] || {}); } else if (rawUrl) { // .cat.md URL → Skill 安装流程(DNR 把 *.cat.md 重定向到安装页),不走脚本解析;仅 agent 启用时 @@ -301,7 +304,9 @@ export function useInstallData(): UseInstallData { }); const metadata = parseMetadata(code); if (!metadata) throw new Error(t("install:script_info_load_failed")); - await loadFromInfo(buildScriptInfo(uuidv4(), code, parsed.href, metadata), false, {}); + await loadFromInfo(buildScriptInfo(uuidv4(), code, parsed.href, metadata), false, { + byWebRequest: byWebRequestRef.current, + }); } else if (fid) { const handle = await loadHandle(fid); if (!handle) throw new Error(t("install:script_info_load_failed")); @@ -366,7 +371,7 @@ export function useInstallData(): UseInstallData { await scriptClient.install({ script, code: info.code }); notify.success(t("install:success")); } - if (closeAfterInstall) setTimeout(() => leaveInstallPage(), 300); + if (closeAfterInstall) setTimeout(() => leaveInstallPage(byWebRequestRef.current), 300); } catch (e) { notify.error(`${t("install:failed")}: ${(e as Error)?.message || String(e)}`); } @@ -390,7 +395,7 @@ export function useInstallData(): UseInstallData { if (opts?.noMoreUpdates && info && !info.userSubscribe) { void scriptClient.setCheckUpdateUrl(info.uuid, false); } - leaveInstallPage(); + leaveInstallPage(byWebRequestRef.current); }, []); // 监听文件变更后自动重装,并刷新视图代码 @@ -447,7 +452,7 @@ export function useInstallData(): UseInstallData { try { await agentClient.completeSkillInstall(uuid); notify.success(t("install:success")); - setTimeout(() => leaveInstallPage(), 300); + setTimeout(() => leaveInstallPage(byWebRequestRef.current), 300); } catch (e) { notify.error(`${t("install:failed")}: ${(e as Error)?.message || String(e)}`); } @@ -456,7 +461,7 @@ export function useInstallData(): UseInstallData { const cancelSkill = useCallback(() => { const uuid = skillUuidRef.current; if (uuid) void agentClient.cancelSkillInstall(uuid); - leaveInstallPage(); + leaveInstallPage(byWebRequestRef.current); }, []); // 重新触发加载(供加载失败后的重试按钮) From 2638187f44987c5c85216e48f3c496472287029c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:24:56 +0900 Subject: [PATCH 3/8] test: stabilize keep-alive E2E selectors --- e2e/keep-alive.spec.ts | 45 ++++++++++++------- .../Setting/sections/RuntimeSection.tsx | 4 +- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/e2e/keep-alive.spec.ts b/e2e/keep-alive.spec.ts index d2cc17f9c..3e20cb040 100644 --- a/e2e/keep-alive.spec.ts +++ b/e2e/keep-alive.spec.ts @@ -2,10 +2,26 @@ import { test, expect } from "./fixtures"; import { openOptionsPage } from "./utils"; import type { CDPSession } from "@playwright/test"; -const KEEP_ALIVE_LABEL = "Keep Background and Scheduled Scripts Alive"; const SERVICE_WORKER_URL = "/service_worker.js"; const HEARTBEAT_VALIDATION_WINDOW_MS = 31_000; +const openRuntimeSettings = async (context: Parameters[0], extensionId: string) => { + const page = await openOptionsPage(context, extensionId); + try { + await page + .getByTestId("view-toggle") + .or(page.getByTestId("mobile-search")) + .first() + .waitFor({ state: "visible", timeout: 30_000 }); + await page.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); + await expect(page.getByTestId("setting-page")).toBeVisible({ timeout: 20_000 }); + return page; + } catch (error) { + if (!page.isClosed()) await page.close(); + throw error; + } +}; + type CdpTargetMessage = { sessionId: string; message: string; @@ -53,17 +69,14 @@ const sendTargetCommand = async ( test.describe("Chrome MV3 service worker keep-alive", () => { test("offscreen runtime heartbeat keeps the service worker active", async ({ context, extensionId }) => { - const optionsPage = await openOptionsPage(context, extensionId); - const cdp = await context.newCDPSession(optionsPage); + const optionsPage = await openRuntimeSettings(context, extensionId); try { - await optionsPage.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); - const label = optionsPage.getByText(KEEP_ALIVE_LABEL, { exact: true }); - await label.scrollIntoViewIfNeeded(); - - const keepAliveSwitch = label.locator("xpath=../..").getByRole("switch"); + const keepAliveSwitch = optionsPage.getByTestId("keep-alive-switch"); + await keepAliveSwitch.scrollIntoViewIfNeeded(); await expect(keepAliveSwitch).toBeVisible(); await expect(keepAliveSwitch).toHaveAttribute("aria-checked", "false"); + const cdp = await context.newCDPSession(optionsPage); await expect .poll( @@ -93,24 +106,22 @@ test.describe("Chrome MV3 service worker keep-alive", () => { }); test("disabling the setting allows the service worker to become idle", async ({ context, extensionId }) => { - const optionsPage = await openOptionsPage(context, extensionId); - const cdp = await context.newCDPSession(optionsPage); + const optionsPage = await openRuntimeSettings(context, extensionId); + let cdp: CDPSession | undefined; let offscreenSessionId: string | undefined; let nextCommandId = 1; try { - await optionsPage.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); - const label = optionsPage.getByText(KEEP_ALIVE_LABEL, { exact: true }); - await label.scrollIntoViewIfNeeded(); - - const keepAliveSwitch = label.locator("xpath=../..").getByRole("switch"); + const keepAliveSwitch = optionsPage.getByTestId("keep-alive-switch"); + await keepAliveSwitch.scrollIntoViewIfNeeded(); await expect(keepAliveSwitch).toBeVisible(); await expect(keepAliveSwitch).toHaveAttribute("aria-checked", "false"); + cdp = await context.newCDPSession(optionsPage); await expect .poll( async () => { - const { targetInfos } = await cdp.send("Target.getTargets"); + const { targetInfos } = await cdp!.send("Target.getTargets"); return targetInfos.some((target) => target.url.endsWith("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/src/offscreen.html")); }, { timeout: 15_000 } @@ -168,7 +179,7 @@ test.describe("Chrome MV3 service worker keep-alive", () => { .toBe(true); } finally { if (offscreenSessionId) { - await cdp.send("Target.detachFromTarget", { sessionId: offscreenSessionId }); + await cdp!.send("Target.detachFromTarget", { sessionId: offscreenSessionId }); } if (!optionsPage.isClosed()) await optionsPage.close(); } diff --git a/src/pages/options/routes/Setting/sections/RuntimeSection.tsx b/src/pages/options/routes/Setting/sections/RuntimeSection.tsx index 5fb6bb420..4a1d9ec3e 100644 --- a/src/pages/options/routes/Setting/sections/RuntimeSection.tsx +++ b/src/pages/options/routes/Setting/sections/RuntimeSection.tsx @@ -155,7 +155,7 @@ export function RuntimeSection({ register }: { register: (id: string) => (el: HT label={t("settings:keep_scripts_alive.title")} description={t("settings:keep_scripts_alive.description")} > - + )} @@ -165,7 +165,7 @@ export function RuntimeSection({ register }: { register: (id: string) => (el: HT label={t("settings:keep_scripts_alive.title")} description={t("settings:keep_scripts_alive.description")} > - + )} From 0585db229be30171970bb470874e4d4fd64bf747 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:29:05 +0900 Subject: [PATCH 4/8] test: prune redundant E2E smoke cases --- e2e/popup.spec.ts | 9 ++------- e2e/script-editor.spec.ts | 18 ------------------ e2e/script-management.spec.ts | 7 ------- 3 files changed, 2 insertions(+), 32 deletions(-) delete mode 100644 e2e/script-editor.spec.ts diff --git a/e2e/popup.spec.ts b/e2e/popup.spec.ts index 4e8a55d13..4ba45cf4e 100644 --- a/e2e/popup.spec.ts +++ b/e2e/popup.spec.ts @@ -1,14 +1,9 @@ import { test, expect } from "./fixtures"; import { openPopupPage } from "./utils"; -// new-ui popup(shadcn):标题 h1、全局 Radix Switch、Radix Accordion 分组、 -// 图标按钮(aria-label 设置/更多菜单)、Radix DropdownMenu(role=menuitem)。 +// new-ui popup(shadcn):全局 Radix Switch、Radix Accordion 分组、图标按钮(aria-label 设置/更多菜单)、 +// Radix DropdownMenu(role=menuitem)。 test.describe("Popup 页面", () => { - test("应加载并显示 ScriptCat 标题", async ({ context, extensionId }) => { - const page = await openPopupPage(context, extensionId); - await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 }); - }); - test("应显示全局脚本启用/禁用开关", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); // 顶部全局开关为 Radix Switch(role=switch) diff --git a/e2e/script-editor.spec.ts b/e2e/script-editor.spec.ts deleted file mode 100644 index 143e5f95f..000000000 --- a/e2e/script-editor.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from "./fixtures"; -import { openEditorPage, openOptionsPage, saveCurrentEditor } from "./utils"; - -// new-ui 脚本编辑器:路由 #/script/editor 加载空白模板(normal.tpl,含 ==UserScript==); -// Monaco 选择器(.monaco-editor/.view-lines) 为框架级不变;保存成功为 sonner toast。 -test.describe("Script 编辑器", () => { - test("保存后脚本应出现在列表中", async ({ context, extensionId }) => { - const editorPage = await openEditorPage(context, extensionId); - await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); - await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 }); - - await saveCurrentEditor(context, extensionId, editorPage); - - const listPage = await openOptionsPage(context, extensionId); - // 保存后列表非空(无空状态) - await expect(listPage.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); - }); -}); diff --git a/e2e/script-management.spec.ts b/e2e/script-management.spec.ts index 5988fbbe8..f702b8eb0 100644 --- a/e2e/script-management.spec.ts +++ b/e2e/script-management.spec.ts @@ -18,13 +18,6 @@ async function createScriptAndGoToList(context: BrowserContext, extensionId: str } test.describe("脚本管理", () => { - test("创建脚本后应出现在列表中", async ({ context, extensionId }) => { - const page = await createScriptAndGoToList(context, extensionId); - // 列表非空(无空状态) - await expect(page.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); - await expect(page.getByRole("switch").first()).toBeVisible({ timeout: 10_000 }); - }); - test("应能切换脚本的启用/禁用", async ({ context, extensionId }) => { const page = await createScriptAndGoToList(context, extensionId); From 5811589500bbced3aebd989bb5dd0d95d310c064 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:33:05 +0900 Subject: [PATCH 5/8] test: exclude development kit from Vitest --- vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index f21eb862f..01f9c8ef7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -31,7 +31,7 @@ const ISOLATED = [ "src/app/service/content/exec_script.test.ts", ]; -const BASE_EXCLUDE = ["**/node_modules/**", "**/.claude/**", "e2e/**"]; +const BASE_EXCLUDE = ["**/node_modules/**", "**/.claude/**", "**/.dev-kit/**", "e2e/**"]; // 页面层(React 渲染,含 .ts 的 renderHook 测试)用例的真实 solo 成本在覆盖率下可达 100–200ms, // 乘上 worker 并行负载后 340ms 预算必然偶发超时(本地满载观测峰值 ~630ms); From edd80320556e4abb26569ea9ea7b5f4ab1f5e7fb Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:40:28 +0900 Subject: [PATCH 6/8] test: prune message and filesystem redundancies --- packages/filesystem/s3/client.test.ts | 12 ----- packages/filesystem/s3/s3.test.ts | 22 ---------- packages/message/message_queue.test.ts | 39 +---------------- packages/message/server.test.ts | 58 ------------------------- packages/message/window_message.test.ts | 40 ----------------- 5 files changed, 1 insertion(+), 170 deletions(-) diff --git a/packages/filesystem/s3/client.test.ts b/packages/filesystem/s3/client.test.ts index 94bdf9ca3..bd9504278 100644 --- a/packages/filesystem/s3/client.test.ts +++ b/packages/filesystem/s3/client.test.ts @@ -14,18 +14,6 @@ describe("S3Error", () => { expect(err.message).toBe("The specified key does not exist"); expect(err.statusCode).toBe(404); }); - - it("应当可被 try/catch 捕获并通过 instanceof 判断", () => { - try { - throw new S3Error("AccessDenied", "Access Denied", 403); - } catch (e) { - expect(e).toBeInstanceOf(S3Error); - if (e instanceof S3Error) { - expect(e.code).toBe("AccessDenied"); - expect(e.statusCode).toBe(403); - } - } - }); }); // ---- S3Client 构造函数与 getter 方法 ---- diff --git a/packages/filesystem/s3/s3.test.ts b/packages/filesystem/s3/s3.test.ts index 99484cf8f..e9bbb39d1 100644 --- a/packages/filesystem/s3/s3.test.ts +++ b/packages/filesystem/s3/s3.test.ts @@ -123,21 +123,6 @@ describe("S3FileSystem", () => { // ---- open ---- describe("open", () => { - it("应当返回 S3FileReader", async () => { - const fileInfo: FileInfo = { - name: "test.txt", - path: "/docs", - size: 100, - digest: "abc", - createtime: 1000, - updatetime: 2000, - }; - const reader = await fs.open(fileInfo); - - expect(reader).toBeDefined(); - expect(reader.read).toBeTypeOf("function"); - }); - it("S3FileReader.read 应调用 client.request GET", async () => { const fileInfo: FileInfo = { name: "hello.txt", @@ -205,13 +190,6 @@ describe("S3FileSystem", () => { // ---- create ---- describe("create", () => { - it("应当返回 S3FileWriter", async () => { - const writer = await fs.create("test.txt"); - - expect(writer).toBeDefined(); - expect(writer.write).toBeTypeOf("function"); - }); - it("S3FileWriter.write 应调用 client.request PUT", async () => { (mockClient.request as ReturnType).mockResolvedValue(createMockResponse({ ok: true })); diff --git a/packages/message/message_queue.test.ts b/packages/message/message_queue.test.ts index 9db04729f..5d652c71a 100644 --- a/packages/message/message_queue.test.ts +++ b/packages/message/message_queue.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { MessageQueue, MessageQueueGroup, type IMessageQueue } from "./message_queue"; +import { MessageQueue, type IMessageQueue } from "./message_queue"; const nextTick = () => Promise.resolve().then(() => {}); @@ -16,11 +16,6 @@ describe("MessageQueueGroup", () => { }); describe("基本功能测试", () => { - it.concurrent("应该能够创建分组", () => { - const group = messageQueue.group("api-group"); - expect(group).toBeInstanceOf(MessageQueueGroup); - }); - it.concurrent("应该能够在分组中订阅和发布消息", () => { const group = messageQueue.group("api-sendBasic"); const handler = vi.fn(); @@ -269,36 +264,4 @@ describe("MessageQueueGroup", () => { expect(handler).toHaveBeenCalledTimes(1); }); }); - - describe("边界情况测试", () => { - it.concurrent("没有中间件的分组应该正常工作", () => { - const group = messageQueue.group("api-groupNoMiddleware"); - const handler = vi.fn(); - - group.subscribe("test-groupNoMiddleware", handler); - group.emit("test-groupNoMiddleware", { data: "test-groupNoMiddleware" }); - - expect(handler).toHaveBeenCalledWith({ data: "test-groupNoMiddleware" }); - }); - - it.concurrent("应该能够处理复杂的数据类型", () => { - const group = messageQueue.group("api-complexPayload"); - const handler = vi.fn(); - - const complexData = { - array: [1, 2, 3], - object: { nested: true }, - number: 42, - string: "test-complexPayload", - boolean: true, - null: null, - undefined: undefined, - }; - - group.subscribe("test-complexPayload", handler); - group.emit("test-complexPayload", complexData); - - expect(handler).toHaveBeenCalledWith(complexData); - }); - }); }); diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index b58037491..b7b1ff007 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -66,21 +66,6 @@ describe("Server", () => { expect(response.data).toBe("sync response"); }); - it.concurrent("应该能够处理异步函数", async () => { - const mockHandler = vi.fn().mockResolvedValue("async response"); - - server.on("on-async", mockHandler); - - const response = await client.sendMessage({ - action: "api/on-async", - data: { param: "value-async" }, - }); - - expect(mockHandler).toHaveBeenCalledWith({ param: "value-async" }, expect.any(SenderRuntime)); - expect(response.code).toBe(0); - expect(response.data).toBe("async response"); - }); - it.concurrent("应该能够处理函数抛出的错误", async () => { const error = new Error("test error"); const mockHandler = vi.fn().mockImplementation(() => { @@ -397,25 +382,6 @@ describe("Server", () => { expect(handler).toHaveBeenCalledTimes(1); }); - it("没有中间件的 Group 应该正常工作", async () => { - const group = server.group("api"); - - const handler = vi.fn(async (params: any) => { - return { data: params }; - }); - - group.on("nomiddle", handler); - - const response = await client.sendMessage({ - action: "api/api/nomiddle", - data: { message: "hello" }, - }); - - expect(response.code).toBe(0); - expect(response.data).toEqual({ data: { message: "hello" } }); - expect(handler).toHaveBeenCalledTimes(1); - }); - it("中间件应该能够处理异步错误", async () => { const errorMiddleware = vi.fn(async (params: any, con: any, next: any) => { if (params.throwError) { @@ -655,30 +621,6 @@ describe("Server", () => { expect(response.data).toBe("empty response"); }); - it.concurrent("应该能够处理复杂的数据类型", async () => { - const complexData = { - array: [1, 2, 3], - object: { nested: true }, - number: 42, - string: "test", - boolean: true, - null: null, - undefined: undefined, - }; - - const mockHandler = vi.fn().mockImplementation((params) => params); - - server.on("on-complex", mockHandler); - - const response = await client.sendMessage({ - action: "api/on-complex", - data: complexData, - }); - - expect(response.code).toBe(0); - expect(response.data).toEqual(complexData); - }); - it.concurrent("应该能够处理返回 undefined 的函数", async () => { const mockHandler = vi.fn().mockReturnValue(undefined); diff --git a/packages/message/window_message.test.ts b/packages/message/window_message.test.ts index 6f3d636b4..00be4f8d7 100644 --- a/packages/message/window_message.test.ts +++ b/packages/message/window_message.test.ts @@ -133,13 +133,6 @@ describe("ServiceWorkerMessageSend", () => { }); describe("ServiceWorkerClientMessage", () => { - it("controller 可用时直接使用", () => { - const clientMsg = new ServiceWorkerClientMessage(); - - expect((clientMsg as any).sw).not.toBeNull(); - expect((clientMsg as any).sw.postMessage).toBe(swPostMessageMock); - }); - it("controller 为 null 时通过 ready 获取 active SW", async () => { const readyPostMessage = vi.fn(); Object.defineProperty(navigator, "serviceWorker", { @@ -334,19 +327,6 @@ describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", return { swSend, clientMsg }; } - it("sendMessage: client→SW 请求并收到响应", async () => { - const { swSend, clientMsg } = createWiredPair(); - - // SW 端注册处理器 - swSend.onMessage((msg: any, sendResponse: any) => { - sendResponse({ code: 0, data: (msg.data as string) + " world" }); - return true; - }); - - const result = await clientMsg.sendMessage({ action: "test/echo", data: "hello" }); - expect(result).toEqual({ code: 0, data: "hello world" }); - }); - it("connect: 建立连接后双向通信", async () => { const { swSend, clientMsg } = createWiredPair(); @@ -402,26 +382,6 @@ describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", expect(serverDisconnected).toBe(true); }); - it("sendMessage: 支持传输复杂对象(模拟结构化克隆场景)", async () => { - const { swSend, clientMsg } = createWiredPair(); - - swSend.onMessage((msg: any, sendResponse: any) => { - // 原样返回,验证数据完整性 - sendResponse({ code: 0, data: msg.data }); - return true; - }); - - const complexData = { - array: [1, 2, 3], - nested: { a: { b: "deep" } }, - nullVal: null, - boolVal: true, - }; - - const result = await clientMsg.sendMessage({ action: "test/complex", data: complexData }); - expect((result as any).data).toEqual(complexData); - }); - it("与 Server 集成: forwardMessage 路径", async () => { const swSend = new ServiceWorkerMessageSend(); const clientMsg = new ServiceWorkerClientMessage(); From 8f005b0a3a7cc9e43125f8094878f57771fca574 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:47:59 +0900 Subject: [PATCH 7/8] test: prune redundant agent and UI cases --- .../service/agent/core/compact_prompt.test.ts | 9 ----- .../service/agent/core/content_utils.test.ts | 13 ------- .../agent/core/mcp_tool_executor.test.ts | 19 ---------- .../agent/core/session_tool_registry.test.ts | 22 ------------ .../agent/core/skill_script_executor.test.ts | 17 +++------ src/locales/locales.test.ts | 13 ------- src/pages/components/use-is-mobile.test.ts | 6 ---- src/pages/confirm/confirm-options.test.ts | 17 +-------- .../install/components/InstallStates.test.tsx | 35 +++++++------------ .../components/WatchingBanner.test.tsx | 5 --- src/pages/options/layout/Sidebar.test.tsx | 5 --- src/pages/options/onboarding/steps.test.ts | 13 +++---- .../routes/Agent/Chat/AskUserBlock.test.tsx | 5 --- .../routes/Agent/Chat/MessageItem.test.tsx | 5 --- .../routes/Agent/Chat/SubAgentBlock.test.tsx | 5 --- .../routes/Agent/Chat/TaskListBlock.test.tsx | 5 --- .../routes/Agent/Chat/ToolCallBlock.test.tsx | 5 --- .../options/routes/Agent/Mcp/McpCard.test.tsx | 6 ---- .../Agent/components/AgentCardMenu.test.tsx | 17 --------- .../routes/Agent/components/agentDocs.test.ts | 7 ---- 20 files changed, 24 insertions(+), 205 deletions(-) delete mode 100644 src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx diff --git a/src/app/service/agent/core/compact_prompt.test.ts b/src/app/service/agent/core/compact_prompt.test.ts index 31bf49415..572e6f9a9 100644 --- a/src/app/service/agent/core/compact_prompt.test.ts +++ b/src/app/service/agent/core/compact_prompt.test.ts @@ -19,15 +19,6 @@ describe("extractSummary", () => { it("handles empty tags", () => { expect(extractSummary("")).toBe(""); }); - - it("handles multiline content inside ", () => { - const response = ` -Line 1 -Line 2 -Line 3 -`; - expect(extractSummary(response)).toBe("Line 1\nLine 2\nLine 3"); - }); }); describe("buildCompactUserPrompt", () => { diff --git a/src/app/service/agent/core/content_utils.test.ts b/src/app/service/agent/core/content_utils.test.ts index 2dd6cce39..eb66fe331 100644 --- a/src/app/service/agent/core/content_utils.test.ts +++ b/src/app/service/agent/core/content_utils.test.ts @@ -8,10 +8,6 @@ describe("content_utils", () => { expect(getTextContent("hello world")).toBe("hello world"); }); - it("returns empty string for empty string", () => { - expect(getTextContent("")).toBe(""); - }); - it("extracts text from ContentBlock[]", () => { const blocks: ContentBlock[] = [ { type: "text", text: "Hello " }, @@ -29,10 +25,6 @@ describe("content_utils", () => { expect(getTextContent(blocks)).toBe(""); }); - it("returns empty string for empty ContentBlock[]", () => { - expect(getTextContent([])).toBe(""); - }); - it("handles audio blocks (skipped in text extraction)", () => { const blocks: ContentBlock[] = [ { type: "text", text: "Listen: " }, @@ -58,11 +50,6 @@ describe("content_utils", () => { ]; expect(normalizeContent(blocks)).toBe(blocks); }); - - it("returns empty array as-is", () => { - const blocks: ContentBlock[] = []; - expect(normalizeContent(blocks)).toBe(blocks); - }); }); describe("isContentBlocks", () => { diff --git a/src/app/service/agent/core/mcp_tool_executor.test.ts b/src/app/service/agent/core/mcp_tool_executor.test.ts index d01415e63..e8e5c1e74 100644 --- a/src/app/service/agent/core/mcp_tool_executor.test.ts +++ b/src/app/service/agent/core/mcp_tool_executor.test.ts @@ -19,16 +19,6 @@ describe("MCPToolExecutor", () => { expect(client.callTool).toHaveBeenCalledWith("search", { query: "hello" }); }); - it("应正确传递工具名", async () => { - const client = createMockClient({ data: [1, 2, 3] }); - const executor = new MCPToolExecutor(client, "fetch_data"); - - const result = await executor.execute({ limit: 10 }); - - expect(result).toEqual({ data: [1, 2, 3] }); - expect(client.callTool).toHaveBeenCalledWith("fetch_data", { limit: 10 }); - }); - it("callTool 抛出异常时应向上传播", async () => { const client = { callTool: vi.fn().mockRejectedValue(new Error("MCP error")), @@ -92,15 +82,6 @@ describe("MCPToolExecutor", () => { expect(result).toEqual(mcpContent); }); - it("非数组结果应原样返回", async () => { - const client = createMockClient("plain string result"); - const executor = new MCPToolExecutor(client, "simple_tool"); - - const result = await executor.execute({}); - - expect(result).toBe("plain string result"); - }); - it("image 缺少 mimeType 时应默认为 image/png", async () => { const mcpContent = [{ type: "image", data: "abc123" }]; const client = createMockClient(mcpContent); diff --git a/src/app/service/agent/core/session_tool_registry.test.ts b/src/app/service/agent/core/session_tool_registry.test.ts index f821086e0..97a5704d0 100644 --- a/src/app/service/agent/core/session_tool_registry.test.ts +++ b/src/app/service/agent/core/session_tool_registry.test.ts @@ -259,28 +259,6 @@ describe("SessionToolRegistry", () => { expect(resA[0].result).toBe("fetched"); expect(resB[0].result).toBe("fetched"); }); - - it("session 释放(GC)后 parent 不受影响", () => { - const parent = new ToolRegistry(); - parent.registerBuiltin( - builtinDef, - createExecutor(async () => "") - ); - - // 创建临时 session 并让其超出作用域 - { - const session = new SessionToolRegistry(parent); - session.register( - "session", - taskDef, - createExecutor(async () => "") - ); - expect(session.listSessionTools()).toHaveLength(1); - } - - // parent 无任何 session 工具痕迹 - expect(parent.getDefinitions().map((d) => d.name)).toEqual(["web_fetch"]); - }); }); describe("脚本工具 miss-then-callback", () => { diff --git a/src/app/service/agent/core/skill_script_executor.test.ts b/src/app/service/agent/core/skill_script_executor.test.ts index a6d90d710..033bae2f1 100644 --- a/src/app/service/agent/core/skill_script_executor.test.ts +++ b/src/app/service/agent/core/skill_script_executor.test.ts @@ -233,19 +233,12 @@ return result;`, }); }); -describe("getSkillScriptNameByUuid", () => { - it("未注册的 UUID 应返回空字符串", () => { - expect(getSkillScriptNameByUuid("skillscript-unknown-uuid")).toBe(""); - }); - - it("空字符串应返回空字符串", () => { - expect(getSkillScriptNameByUuid("")).toBe(""); - }); -}); - describe("getSkillScriptGrantsByUuid", () => { - it("未注册的 UUID 应返回空数组", () => { - expect(getSkillScriptGrantsByUuid("skillscript-unknown-uuid")).toEqual([]); + it("未注册的 UUID 应返回空工具名和权限列表", () => { + for (const uuid of ["unregistered", ""]) { + expect(getSkillScriptNameByUuid(uuid)).toBe(""); + expect(getSkillScriptGrantsByUuid(uuid)).toEqual([]); + } }); it("执行期间应能通过 UUID 获取 grants", async () => { diff --git a/src/locales/locales.test.ts b/src/locales/locales.test.ts index 1ac16cf16..4dbba146f 100644 --- a/src/locales/locales.test.ts +++ b/src/locales/locales.test.ts @@ -120,17 +120,4 @@ describe.concurrent("i18nDescription", () => { const result = i18nDescription(script); expect(result).toBe(""); }); - - it("description 字段为空数组时返回 空字串", () => { - i18n.language = "en-US"; - - const script = { - metadata: { - description: [], - } as SCMetadata, - }; - - const result = i18nDescription(script); - expect(result).toBe(""); - }); }); diff --git a/src/pages/components/use-is-mobile.test.ts b/src/pages/components/use-is-mobile.test.ts index 07e3e369f..b87a75b43 100644 --- a/src/pages/components/use-is-mobile.test.ts +++ b/src/pages/components/use-is-mobile.test.ts @@ -38,12 +38,6 @@ describe("useIsMobile 视口断点", () => { expect(result.current).toBe(true); }); - it("视口 ≥ 768px 时返回 false", () => { - stubMatchMedia(false); - const { result } = renderHook(() => useIsMobile()); - expect(result.current).toBe(false); - }); - it("监听 change 事件,视口变化时更新返回值", () => { const mql = stubMatchMedia(false); const { result } = renderHook(() => useIsMobile()); diff --git a/src/pages/confirm/confirm-options.test.ts b/src/pages/confirm/confirm-options.test.ts index 22739c1a3..f717938a4 100644 --- a/src/pages/confirm/confirm-options.test.ts +++ b/src/pages/confirm/confirm-options.test.ts @@ -1,11 +1,5 @@ import { describe, it, expect } from "vitest"; -import { - resolveConfirmType, - availableDurations, - canApplyToAll, - isSiteAccess, - isHighSensitive, -} from "./confirm-options"; +import { resolveConfirmType, availableDurations, canApplyToAll, isHighSensitive } from "./confirm-options"; import type { ConfirmParam } from "@App/app/service/service_worker/permission_verify"; const cp = (over: Partial = {}): ConfirmParam => ({ permission: "cors", ...over }); @@ -55,12 +49,3 @@ describe("授权选项 · 高敏感权限警示", () => { expect(isHighSensitive(cp({ permission: "file_storage" }))).toBe(false); }); }); - -describe("授权选项 · 站点访问识别", () => { - it("extension-site-access 应识别为站点访问(单按钮变体)", () => { - expect(isSiteAccess(cp({ permission: "extension-site-access" }))).toBe(true); - }); - it("其它权限不是站点访问", () => { - expect(isSiteAccess(cp({ permission: "cors" }))).toBe(false); - }); -}); diff --git a/src/pages/install/components/InstallStates.test.tsx b/src/pages/install/components/InstallStates.test.tsx index c8f561c00..ede3749f8 100644 --- a/src/pages/install/components/InstallStates.test.tsx +++ b/src/pages/install/components/InstallStates.test.tsx @@ -39,32 +39,23 @@ describe("InstallError 加载失败状态屏", () => { expect(screen.getByText("Error: Fetch failed with status 404")).toBeInTheDocument(); }); - it("保留顶部品牌栏(对照设计稿,失败态不丢失外壳)", () => { - render( {}} />); - expect(screen.getByTestId("install-top-bar")).toBeInTheDocument(); - }); - - it("提供 onRetry 时渲染重试按钮并可点击", () => { + it("提供重试和自定义标题时可分别触发重试与关闭", () => { const onRetry = vi.fn(); - render( {}} />); - fireEvent.click(screen.getByText("重试").closest("button")!); - expect(onRetry).toHaveBeenCalledTimes(1); - }); + const onClose = vi.fn(); + const { rerender } = render(); - it("未提供 onRetry 时不渲染重试按钮", () => { - render( {}} />); - expect(screen.queryByText("重试")).not.toBeInTheDocument(); - }); + expect(screen.getByText("无效安装地址")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "重试" })); + fireEvent.click(screen.getByRole("button", { name: "关闭" })); + expect(onRetry).toHaveBeenCalledOnce(); + expect(onClose).toHaveBeenCalledOnce(); - it("点击关闭触发 onClose", () => { - const onClose = vi.fn(); - render(); - fireEvent.click(screen.getByText("关闭").closest("button")!); - expect(onClose).toHaveBeenCalledTimes(1); + rerender(); + expect(screen.queryByRole("button", { name: "重试" })).not.toBeInTheDocument(); }); - it("可自定义标题(用于无效页面)", () => { - render( {}} />); - expect(screen.getByText("无效页面")).toBeInTheDocument(); + it("保留顶部品牌栏(对照设计稿,失败态不丢失外壳)", () => { + render( {}} />); + expect(screen.getByTestId("install-top-bar")).toBeInTheDocument(); }); }); diff --git a/src/pages/install/components/WatchingBanner.test.tsx b/src/pages/install/components/WatchingBanner.test.tsx index ce1cb4085..837746737 100644 --- a/src/pages/install/components/WatchingBanner.test.tsx +++ b/src/pages/install/components/WatchingBanner.test.tsx @@ -8,11 +8,6 @@ beforeAll(() => initTestLanguage("zh-CN")); afterEach(cleanup); describe("WatchingBanner 文件监听横幅", () => { - it("渲染监听横幅容器", () => { - render(); - expect(screen.getByTestId("watching-banner")).toBeInTheDocument(); - }); - it("提供最后同步时间时渲染时间戳区", () => { render(); expect(screen.getByTestId("watching-last-sync")).toBeInTheDocument(); diff --git a/src/pages/options/layout/Sidebar.test.tsx b/src/pages/options/layout/Sidebar.test.tsx index f89c21d09..4292fcf2e 100644 --- a/src/pages/options/layout/Sidebar.test.tsx +++ b/src/pages/options/layout/Sidebar.test.tsx @@ -36,11 +36,6 @@ const subLabels = () => [ ]; describe("Sidebar 侧边栏 AI Agent 菜单", () => { - it("渲染 AI Agent 子菜单入口", () => { - const { getByText } = renderSidebar(); - expect(getByText(t("agent:title"))).toBeInTheDocument(); - }); - it("默认折叠,点击 AI Agent 后展开显示 7 个子项", () => { const { getByText, queryByTestId, getByTestId } = renderSidebar(); expect(queryByTestId("sidebar-agent-submenu")).toBeNull(); diff --git a/src/pages/options/onboarding/steps.test.ts b/src/pages/options/onboarding/steps.test.ts index 53ebc4468..9d8946b21 100644 --- a/src/pages/options/onboarding/steps.test.ts +++ b/src/pages/options/onboarding/steps.test.ts @@ -2,14 +2,6 @@ import { describe, it, expect } from "vitest"; import { DESKTOP_STEPS, MOBILE_STEPS } from "./steps"; describe("巡览步骤配置", () => { - it("桌面应有 6 个步骤", () => { - expect(DESKTOP_STEPS).toHaveLength(6); - }); - - it("移动端应为更精简的 3 个步骤", () => { - expect(MOBILE_STEPS).toHaveLength(3); - }); - it("每个步骤都应带 guide 命名空间的标题与正文 key", () => { for (const s of [...DESKTOP_STEPS, ...MOBILE_STEPS]) { expect(s.titleKey.startsWith("guide:")).toBe(true); @@ -22,4 +14,9 @@ describe("巡览步骤配置", () => { const allowed = new Set([...DESKTOP_STEPS.map((s) => s.id), "subscribe"]); for (const s of MOBILE_STEPS) expect(allowed.has(s.id)).toBe(true); }); + + it("桌面与移动端分别保留 6 步和 3 步巡览", () => { + expect(DESKTOP_STEPS).toHaveLength(6); + expect(MOBILE_STEPS).toHaveLength(3); + }); }); diff --git a/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx b/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx index 17fd3e9d8..9af62320e 100644 --- a/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/AskUserBlock.test.tsx @@ -7,11 +7,6 @@ beforeAll(() => initTestLanguage("zh-CN")); afterEach(() => cleanup()); describe("用户提问块 AskUserBlock", () => { - it("展示问题文本", () => { - render(); - expect(screen.getByText("选择一个颜色")).toBeInTheDocument(); - }); - it("单选点击选项后立即提交该选项", () => { const onRespond = vi.fn(); render(); diff --git a/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx b/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx index 7e26f4c3f..9b8a131e6 100644 --- a/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx +++ b/src/pages/options/routes/Agent/Chat/MessageItem.test.tsx @@ -18,11 +18,6 @@ const msg = (over: Partial & Pick) }); describe("用户消息 UserMessageItem", () => { - it("展示用户文本气泡", () => { - render(); - expect(screen.getByText("你好世界")).toBeInTheDocument(); - }); - it("编辑后保存触发 onEdit 携带新文本", () => { const onEdit = vi.fn(); render(); diff --git a/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx b/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx index b037f8bf8..d7b222841 100644 --- a/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/SubAgentBlock.test.tsx @@ -19,11 +19,6 @@ const state = (over?: Partial): SubAgentState => ({ }); describe("子代理块 SubAgentBlock", () => { - it("展示子代理描述", () => { - render(); - expect(screen.getByText("搜索资料")).toBeInTheDocument(); - }); - it("依据 isRunning 标注运行/完成状态", () => { const { rerender } = render(); expect(screen.getByTestId("subagent-status").dataset.running).toBe("true"); diff --git a/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx b/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx index 38e464b3d..ea18481b2 100644 --- a/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/TaskListBlock.test.tsx @@ -30,9 +30,4 @@ describe("任务清单块 TaskListBlock", () => { expect(screen.getByTestId("task-a").dataset.status).toBe("completed"); expect(screen.getByTestId("task-b").dataset.status).toBe("pending"); }); - - it("展示每个任务的标题", () => { - render(); - expect(screen.getByText("抓取首页")).toBeInTheDocument(); - }); }); diff --git a/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx b/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx index 8bb90b4f1..1a86f570d 100644 --- a/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx +++ b/src/pages/options/routes/Agent/Chat/ToolCallBlock.test.tsx @@ -16,11 +16,6 @@ const tc = (overrides?: Partial): ToolCall => ({ }); describe("工具调用块 ToolCallBlock", () => { - it("始终展示工具名称", () => { - render(); - expect(screen.getByText("web_search")).toBeInTheDocument(); - }); - it("默认折叠,不展示参数", () => { render(); expect(screen.queryByText(/"query":"天气"/)).toBeNull(); diff --git a/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx b/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx index 05f71cbe3..e54de40b3 100644 --- a/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx +++ b/src/pages/options/routes/Agent/Mcp/McpCard.test.tsx @@ -18,12 +18,6 @@ const server = { function noop() {} describe("McpCard MCP 服务器卡片", () => { - it("展示名称与 URL", () => { - render(); - expect(screen.getByText("本地工具")).toBeInTheDocument(); - expect(screen.getByText("http://localhost:8080/mcp")).toBeInTheDocument(); - }); - it("点击开关触发 onToggle", () => { const onToggle = vi.fn(); render(); diff --git a/src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx b/src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx deleted file mode 100644 index 315cf4c07..000000000 --- a/src/pages/options/routes/Agent/components/AgentCardMenu.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, it, expect, vi, afterEach } from "vitest"; -import { render, cleanup, screen, fireEvent } from "@testing-library/react"; -import { Pencil } from "lucide-react"; -import { AgentCardMenu } from "./AgentCardMenu"; - -afterEach(() => cleanup()); - -describe("AgentCardMenu 卡片菜单", () => { - it("点击菜单项触发 onSelect", () => { - const onSelect = vi.fn(); - render(); - // Radix 触发器在 pointerdown(左键) 时展开菜单——真实点击即包含此事件 - fireEvent.pointerDown(screen.getByTestId("card-menu"), { button: 0 }); - fireEvent.click(screen.getByTestId("card-menu-edit")); - expect(onSelect).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/pages/options/routes/Agent/components/agentDocs.test.ts b/src/pages/options/routes/Agent/components/agentDocs.test.ts index 025e906e6..1e4f57257 100644 --- a/src/pages/options/routes/Agent/components/agentDocs.test.ts +++ b/src/pages/options/routes/Agent/components/agentDocs.test.ts @@ -10,11 +10,4 @@ describe("agentDocUrl 文档深链", () => { expect(agentDocUrl("opfs")).toBe("https://docs.scriptcat.org/docs/dev/agent/agent-opfs"); expect(agentDocUrl("settings")).toBe("https://docs.scriptcat.org/docs/dev/agent/agent"); }); - - it("文档链接均不是站点根(确保深链)", () => { - for (const page of ["provider", "skills", "mcp", "tasks", "opfs", "settings"] as const) { - expect(agentDocUrl(page)).not.toBe("https://docs.scriptcat.org"); - expect(agentDocUrl(page)).toContain("/docs/dev/agent/"); - } - }); }); From 9cb5a1227233620abaf6b22e9029345cd754a68d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 13 Aug 2026 05:54:43 +0900 Subject: [PATCH 8/8] test: prune redundant utility cases --- src/pkg/backup/backup.test.ts | 69 ----------------------------- src/pkg/utils/async_queue.test.ts | 20 --------- src/pkg/utils/match.test.ts | 3 -- src/pkg/utils/message_value.test.ts | 16 ------- src/pkg/utils/regex_to_glob.test.ts | 14 ------ src/pkg/utils/script.test.ts | 35 --------------- src/pkg/utils/skill-md.test.ts | 55 ----------------------- src/pkg/utils/skill-zip.test.ts | 32 ------------- src/pkg/utils/skill_script.test.ts | 44 ------------------ src/pkg/utils/url-utils.test.ts | 4 -- src/pkg/utils/url_matcher.test.ts | 9 ---- src/pkg/utils/utils.test.ts | 9 ---- 12 files changed, 310 deletions(-) diff --git a/src/pkg/backup/backup.test.ts b/src/pkg/backup/backup.test.ts index fec61d9c9..a58eb5b87 100644 --- a/src/pkg/backup/backup.test.ts +++ b/src/pkg/backup/backup.test.ts @@ -114,75 +114,6 @@ describe.concurrent("backup", () => { expect(resp).toEqual(data); }); - it.concurrent("export and import script - name and version only", async () => { - const zipFile = createJSZip(); - const fs = new ZipFileSystem(zipFile); - const data: BackupData = { - script: [ - { - code: `// ==UserScript== - // @name New Userscript - // @version 1 - // ==/UserScript== - - console.log('hello world')`, - options: { - options: {}, - meta: { - name: "test", - modified: 1, - file_url: "", - }, - settings: { - enabled: true, - position: 1, - }, - }, - resources: [ - { - meta: { name: "test1", mimetype: "text/plain" }, - base64: "data:text/plain;base64,aGVsbG8gd29ybGQ=", - source: "hello world", - }, - ], - requires: [ - { - meta: { name: "test2", mimetype: "text/plain" }, - base64: "data:text/plain;base64,aGVsbG8gd29ybGQ=", - source: "hello world", - }, - ], - requiresCss: [ - { - meta: { name: "test3", mimetype: "application/javascript" }, - base64: "data:application/javascript;base64,aGVsbG8gd29ybGQ=", - source: "hello world", - }, - ], - storage: { - ts: ts0 + 2, - data: { - num: 1, - str: "data", - bool: false, - }, - }, - lastModificationDate: expect.any(Number), - }, - ], - subscribe: [], - } as unknown as BackupData; - await new BackupExport(fs).export(data); - expect(data.script[0].storage.data.num).toEqual("n1"); - expect(data.script[0].storage.data.str).toEqual("sdata"); - expect(data.script[0].storage.data.bool).toEqual("bfalse"); - const resp = await parseBackupZipFile(zipFile); - data.script[0].storage.data.num = 1; - data.script[0].storage.data.str = "data"; - data.script[0].storage.data.bool = false; - expect(resp).toEqual(data); - }); - it.concurrent("export and import script - 2 scripts", async () => { const zipFile = createJSZip(); const fs = new ZipFileSystem(zipFile); diff --git a/src/pkg/utils/async_queue.test.ts b/src/pkg/utils/async_queue.test.ts index 22a70ce60..42623db50 100644 --- a/src/pkg/utils/async_queue.test.ts +++ b/src/pkg/utils/async_queue.test.ts @@ -347,24 +347,4 @@ describe.concurrent("stackAsyncTask 测试", () => { expect(order).toEqual([0, 1, 2, 3, 4]); expect(results).toEqual([0, 1, 2, 3, 4]); }); - - /* ------------------- 7. 跨 key 链接(正确 await 返回值) ------------------- */ - it.concurrent("【7】跨 key 链接:内部任务返回值可被外层 await(不 await stackAsyncTask)", async () => { - const kOuter = generateKey("outer"); - const kInner = generateKey("inner"); - - const pOuter = stackAsyncTask(kOuter, async () => { - const pInner = stackAsyncTask(kInner, async () => { - return "inner-data"; - }); - const data = await pInner; // 正确:await 返回值 - return `outer(${data})`; - }); - - setupBlockingTask(kOuter).resolve(); - setupBlockingTask(kInner).resolve(); - await flush(); - - await expect(pOuter).resolves.toBe("outer(inner-data)"); - }); }); diff --git a/src/pkg/utils/match.test.ts b/src/pkg/utils/match.test.ts index 6a13ceea4..a7f7ccbfd 100644 --- a/src/pkg/utils/match.test.ts +++ b/src/pkg/utils/match.test.ts @@ -205,9 +205,6 @@ describe.concurrent("UrlMatch-google", () => { expect(url.urlMatch("https://www.google.com/foo/baz/bar")).toEqual(["ok1", "ok2", "ok3"]); expect(url.urlMatch("https://docs.google.com/foobar")).toEqual(["ok1", "ok2", "ok3"]); }); - it.concurrent("match4", () => { - expect(url.urlMatch("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]); - }); it.concurrent("match5", () => { expect(url.urlMatch("http://127.0.0.1/")).toEqual(["ok5"]); expect(url.urlMatch("http://127.0.0.1/foo/bar.html")).toEqual(["ok5"]); diff --git a/src/pkg/utils/message_value.test.ts b/src/pkg/utils/message_value.test.ts index f0c10e01a..77f66d4da 100644 --- a/src/pkg/utils/message_value.test.ts +++ b/src/pkg/utils/message_value.test.ts @@ -55,13 +55,6 @@ describe.concurrent("encodeRValue 编码函数", () => { expect(encoded[0]).toBe(RType.STANDARD); expect(encoded[1]).toBe(big); }); - - it.concurrent("应正确处理联合类型的编码", () => { - const value: string | null = "联合类型测试"; - const encoded = encodeRValue(value); - expect(encoded[0]).toBe(RType.STANDARD); - expect(encoded[1]).toBe(value); - }); }); describe.concurrent("decodeRValue 解码函数", () => { @@ -145,13 +138,4 @@ describe.concurrent("encodeRValue 与 decodeRValue 组合行为", () => { } }); }); - - it.concurrent("应对联合类型值进行正确的往返编码解码", () => { - type Union = string | number | null | undefined; - const values: Union[] = [undefined, null, 1, 0, 123, "abc", ""]; - - const roundTrip = values.map((v) => decodeRValue(encodeRValue(v))); - - expect(roundTrip).toEqual(values); - }); }); diff --git a/src/pkg/utils/regex_to_glob.test.ts b/src/pkg/utils/regex_to_glob.test.ts index db7232715..81bd2cc7d 100644 --- a/src/pkg/utils/regex_to_glob.test.ts +++ b/src/pkg/utils/regex_to_glob.test.ts @@ -404,19 +404,5 @@ describe.concurrent("regexToGlob - comprehensive test suite (regrouped & comment ok("(cat|car|cap)\\.txt", "ca?.txt"); // prior pattern with fixed suffix ok("v(?:\\d{2}|latest)", "v??*"); // min 2 digits, or 'latest' -> '??*' }); - - it.concurrent("8.8 invalid regex from additional remain null", () => { - // 继续确保无效正则 → null - // Still invalid → null - // 注:呼叫regexToGlob前,已经用 new RegExp 生成,所以不会出现非法RegEx字串 - bad("(ab"); - bad("test\\"); - bad("([a-z]"); // unbalanced () and [] - // bad("(?P\\w+)"); // unsupported named group (PCRE-style) - // bad("(?'name'\\w+)"); // unsupported named group (alternate syntax) - // bad("(?|a|b)"); // branch reset group (PCRE), unsupported - // bad("a**"); // consecutive quantifiers invalid - // bad("*"); // bare quantifier invalid - }); }); }); diff --git a/src/pkg/utils/script.test.ts b/src/pkg/utils/script.test.ts index d21e95365..2c1cf044b 100644 --- a/src/pkg/utils/script.test.ts +++ b/src/pkg/utils/script.test.ts @@ -386,41 +386,6 @@ console.log('Hello World'); expect(result?.author).toEqual([""]); }); - it.concurrent("正確解析元数据(空version)", () => { - const code = ` -// ==UserScript== -// @name 测试脚本 -// @namespace http://tampermonkey.net/ -// @match https://example.org/* -// @match https://test.com/* -// @match https://demo.com/* -// @description -// @early-start -// @author -// @match https://example.com/* -// @grant - GM_setValue -// @grant GM_getValue -// ==/UserScript== -console.log('Hello World'); -`; - - const result = parseMetadata(code); - expect(result).not.toBeNull(); - expect(result?.name).toEqual(["测试脚本"]); - expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); - expect(result?.match).toEqual([ - "https://example.org/*", - "https://test.com/*", - "https://demo.com/*", - "https://example.com/*", - ]); - expect(result?.["early-start"]).toEqual([""]); - expect(result?.grant).toEqual(["", "GM_getValue"]); - expect(result?.description).toEqual([""]); - expect(result?.author).toEqual([""]); - }); - it.concurrent("正確解析元数据(換行空白1)", () => { const code = ` // ==UserScript== diff --git a/src/pkg/utils/skill-md.test.ts b/src/pkg/utils/skill-md.test.ts index 82a7685a0..1b93194d4 100644 --- a/src/pkg/utils/skill-md.test.ts +++ b/src/pkg/utils/skill-md.test.ts @@ -158,36 +158,6 @@ Prompt.`; expect(result.metadata.references).toBeUndefined(); }); - it("应正确解析完整的 SKILL.cat.md(含 version + scripts + references + config)", () => { - const content = `--- -name: price-compare -description: 多平台比价 -version: 2.0.0 -scripts: - - compare.js -references: - - api_docs.md -config: - api_key: - title: API Key - type: text - secret: true ---- - -# Price Compare - -比价工具使用说明。`; - - const result = parseSkillMd(content)!; - expect(result.metadata.name).toBe("price-compare"); - expect(result.metadata.version).toBe("2.0.0"); - expect(result.metadata.scripts).toEqual(["compare.js"]); - expect(result.metadata.references).toEqual(["api_docs.md"]); - expect(result.metadata.config).toBeDefined(); - expect(result.metadata.config!.api_key.secret).toBe(true); - expect(result.prompt).toContain("# Price Compare"); - }); - it("scripts 中过滤非字符串值", () => { const content = `--- name: filter-test @@ -324,29 +294,4 @@ Prompt.`; const result = parseSkillMd(content)!; expect(result.metadata.config).toBeUndefined(); }); - - it("应正确处理多行 prompt 内容", () => { - const content = `--- -name: multi-line -description: test ---- - -# Title - -Paragraph 1. - -## Subtitle - -- item 1 -- item 2 - -\`\`\`js -console.log("hello"); -\`\`\``; - - const result = parseSkillMd(content)!; - expect(result.prompt).toContain("# Title"); - expect(result.prompt).toContain("- item 1"); - expect(result.prompt).toContain('console.log("hello");'); - }); }); diff --git a/src/pkg/utils/skill-zip.test.ts b/src/pkg/utils/skill-zip.test.ts index cf1dd2bc0..3c1e1f13b 100644 --- a/src/pkg/utils/skill-zip.test.ts +++ b/src/pkg/utils/skill-zip.test.ts @@ -191,35 +191,6 @@ description: 淘宝购物助手 expect(toolMeta!.params[1].name).toBe("tabId"); }); - it("ZIP 解析输出结构与 installSkill 参数签名一致", async () => { - const zipData = await createTestZip({ - "SKILL.md": `---\nname: sig-test\ndescription: Signature test\n---\nPrompt.`, - "scripts/helper.js": VALID_SKILLSCRIPT_CODE, - "references/doc.md": "Doc content", - }); - - const result = await parseSkillZip(zipData); - - // 验证结构:skillMd 是 string,scripts 是 {name, code}[],references 是 {name, content}[] - expect(typeof result.skillMd).toBe("string"); - expect(Array.isArray(result.scripts)).toBe(true); - expect(Array.isArray(result.references)).toBe(true); - - for (const s of result.scripts) { - expect(typeof s.name).toBe("string"); - expect(typeof s.code).toBe("string"); - expect(s.name).toBeTruthy(); - expect(s.code).toBeTruthy(); - } - - for (const r of result.references) { - expect(typeof r.name).toBe("string"); - expect(typeof r.content).toBe("string"); - expect(r.name).toBeTruthy(); - expect(r.content).toBeTruthy(); - } - }); - it("嵌套目录 ZIP 的完整流程:解析 → 验证 SKILL.md → 验证 SkillScript", async () => { const zipData = await createTestZip({ "taobao-skill/SKILL.md": `---\nname: nested-skill\ndescription: 嵌套目录测试\n---\n嵌套 Skill 提示词。`, @@ -229,18 +200,15 @@ description: 淘宝购物助手 const zipResult = await parseSkillZip(zipData); - // Step 1: SKILL.md 正确 const parsed = parseSkillMd(zipResult.skillMd); expect(parsed).not.toBeNull(); expect(parsed!.metadata.name).toBe("nested-skill"); - // Step 2: SkillScript 正确 expect(zipResult.scripts).toHaveLength(1); const toolMeta = parseSkillScriptMetadata(zipResult.scripts[0].code); expect(toolMeta).not.toBeNull(); expect(toolMeta!.name).toBe("taobao_extract"); - // Step 3: references 正确 expect(zipResult.references).toHaveLength(1); expect(zipResult.references[0].name).toBe("guide.txt"); expect(zipResult.references[0].content).toBe("使用指南内容"); diff --git a/src/pkg/utils/skill_script.test.ts b/src/pkg/utils/skill_script.test.ts index 0faddd502..ad4d36754 100644 --- a/src/pkg/utils/skill_script.test.ts +++ b/src/pkg/utils/skill_script.test.ts @@ -71,20 +71,6 @@ return args.value * 2; expect(meta.params[1].required).toBe(false); }); - it("应正确解析无参数的工具", () => { - const code = ` -// ==SkillScript== -// @name ping -// @description 测试连通性 -// ==/SkillScript== -return "pong"; -`; - const meta = parseSkillScriptMetadata(code)!; - expect(meta.name).toBe("ping"); - expect(meta.params).toHaveLength(0); - expect(meta.grants).toHaveLength(0); - }); - it("应正确解析多个 @grant", () => { const code = ` // ==SkillScript== @@ -100,19 +86,6 @@ return "ok"; expect(meta.grants).toEqual(["GM.xmlHttpRequest", "GM.getValue", "GM.setValue"]); }); - it("应正确解析单个 @require URL", () => { - const code = ` -// ==SkillScript== -// @name xlsx_tool -// @description 生成 Excel -// @require https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js -// ==/SkillScript== -return XLSX.utils.book_new(); -`; - const meta = parseSkillScriptMetadata(code)!; - expect(meta.requires).toEqual(["https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"]); - }); - it("应正确解析多个 @require URL", () => { const code = ` // ==SkillScript== @@ -329,21 +302,4 @@ return x;`; const body = getSkillScriptBody(code); expect(body).toBe("const x = 1;\nreturn x;"); }); - - it("应保留元数据头后面的所有代码", () => { - const code = `// ==SkillScript== -// @name test -// @description 测试 -// @param city string [required] 城市 -// @grant GM.xmlHttpRequest -// ==/SkillScript== - -const result = await GM.xmlHttpRequest({url: "http://example.com/" + args.city}); -const data = JSON.parse(result.responseText); -return data;`; - const body = getSkillScriptBody(code); - expect(body).toContain("const result = await GM.xmlHttpRequest"); - expect(body).toContain("return data;"); - expect(body).not.toContain("==SkillScript=="); - }); }); diff --git a/src/pkg/utils/url-utils.test.ts b/src/pkg/utils/url-utils.test.ts index 13e29f457..263bea744 100644 --- a/src/pkg/utils/url-utils.test.ts +++ b/src/pkg/utils/url-utils.test.ts @@ -21,10 +21,6 @@ describe.concurrent("prettyUrl", () => { it.concurrent("should decode Emoji domains", () => { expect(prettyUrl("https://xn--vi8h.la/path")).toBe("https://🍕.la/path"); }); - - it.concurrent("should handle mixed Latin and Foreign scripts", () => { - expect(prettyUrl("http://xn--maana-pta.com")).toBe("http://mañana.com/"); - }); }); describe.concurrent("Path and Percent Encoding", () => { diff --git a/src/pkg/utils/url_matcher.test.ts b/src/pkg/utils/url_matcher.test.ts index bc3f36c81..62e7781fc 100644 --- a/src/pkg/utils/url_matcher.test.ts +++ b/src/pkg/utils/url_matcher.test.ts @@ -999,13 +999,4 @@ describe.concurrent("embeddedPatternChecker", () => { const code3 = embeddedPatternCheckerString('"https://example.com/secret/data"', JSON.stringify(reduced)); expect(eval(code3)).toBe(false); }); - - it.concurrent("embeddedPatternCheckerString 生成可执行代码", () => { - const patterns = extractUrlPatterns(["@match *://example.com/*"]); - const reduced = patterns.map(({ ruleType, ruleContent }) => ({ ruleType, ruleContent })); - const codeStr = embeddedPatternCheckerString("location.href", JSON.stringify(reduced)); - // 验证生成的是一个函数调用表达式字符串(IIFE 形式) - expect(typeof codeStr).toBe("string"); - expect(codeStr).toContain("location.href"); - }); }); diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index 3add3ea08..2a3762870 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -572,10 +572,6 @@ describe.concurrent("normalizeResponseHeaders", () => { expect(normalizeResponseHeaders("")).toBe(""); }); - it.concurrent("returns empty string for falsy-like empty string (only case possible with string type)", () => { - expect(normalizeResponseHeaders(String(""))).toBe(""); - }); - it.concurrent("keeps valid header lines and outputs name:value joined with CRLF", () => { const input = "Content-Type: text/plain\nX-Test: abc\n"; expect(normalizeResponseHeaders(input)).toBe("Content-Type:text/plain\r\nX-Test:abc"); @@ -601,11 +597,6 @@ describe.concurrent("normalizeResponseHeaders", () => { expect(normalizeResponseHeaders(input)).toBe("X-名前:値"); }); - it.concurrent("does not include a trailing CRLF at the end of output", () => { - const input = "A: 1\nB: 2\n"; - expect(normalizeResponseHeaders(input).endsWith("\r\n")).toBe(false); - }); - it.concurrent("standard test", () => { const input = `content-type: text/html; charset=utf-8\r\n server: Apache/2.4.41 (Ubuntu)\r\n