2026-04-27 21:34:28 +08:00
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
|
|
vi.mock("electron/main", () => ({
|
|
|
|
|
|
ipcMain: {
|
|
|
|
|
|
on: vi.fn(),
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
2026-04-23 22:54:56 +08:00
|
|
|
|
|
|
|
|
|
|
import { __deepseekTestUtils } from "./deepseek";
|
|
|
|
|
|
import { getMonitorAdapter } from "./index";
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
extractQuestionText,
|
|
|
|
|
|
buildSourceItem,
|
|
|
|
|
|
buildDeepSeekPayload,
|
|
|
|
|
|
detectDeepSeekBusySignals,
|
|
|
|
|
|
dedupeSourceItems,
|
|
|
|
|
|
classifyDeepseekSources,
|
|
|
|
|
|
mergeDeepSeekRawSourceLinks,
|
|
|
|
|
|
isObservationComplete,
|
|
|
|
|
|
isDeepSeekToggleSelected,
|
|
|
|
|
|
isDeepSeekWebpagesTriggerText,
|
|
|
|
|
|
} = __deepseekTestUtils;
|
|
|
|
|
|
|
|
|
|
|
|
describe("deepseek adapter helpers", () => {
|
|
|
|
|
|
it("extracts question text from monitoring payload", () => {
|
|
|
|
|
|
expect(extractQuestionText({ question_text: "DeepSeek 怎么看 GEO?" })).toBe("DeepSeek 怎么看 GEO?");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("dedupes sources by normalized url without hash fragments", () => {
|
|
|
|
|
|
const items = dedupeSourceItems([
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/a#top",
|
|
|
|
|
|
title: "A",
|
|
|
|
|
|
normalized_url: "https://example.com/a#top",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/a#bottom",
|
|
|
|
|
|
title: "A later",
|
|
|
|
|
|
normalized_url: "https://example.com/a#bottom",
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
expect(items).toHaveLength(1);
|
|
|
|
|
|
expect(items[0]?.normalized_url).toBe("https://example.com/a");
|
|
|
|
|
|
expect(items[0]?.title).toBe("A");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("treats revealed search cards as citation sources and inline links as content citation candidates", () => {
|
|
|
|
|
|
const classified = classifyDeepseekSources({
|
|
|
|
|
|
inlineLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/citation",
|
|
|
|
|
|
title: "Citation",
|
|
|
|
|
|
normalized_url: "https://example.com/citation",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
sourcePanelLinks: [],
|
|
|
|
|
|
searchPanelLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/search",
|
|
|
|
|
|
title: "Search",
|
|
|
|
|
|
normalized_url: "https://example.com/search",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(classified.citations).toHaveLength(1);
|
|
|
|
|
|
expect(classified.inlineCitationCandidates).toHaveLength(1);
|
|
|
|
|
|
expect(classified.searchResults).toHaveLength(1);
|
|
|
|
|
|
expect(classified.citations[0]?.url).toBe("https://example.com/search");
|
|
|
|
|
|
expect(classified.inlineCitationCandidates[0]?.url).toBe("https://example.com/citation");
|
|
|
|
|
|
expect(classified.searchResults[0]?.url).toBe("https://example.com/search");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("enriches numeric inline citations with source metadata from revealed search cards", () => {
|
|
|
|
|
|
const classified = classifyDeepseekSources({
|
|
|
|
|
|
inlineLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/citation",
|
|
|
|
|
|
title: null,
|
|
|
|
|
|
text: "-1",
|
|
|
|
|
|
siteName: null,
|
|
|
|
|
|
kind: "inline",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
sourcePanelLinks: [],
|
|
|
|
|
|
searchPanelLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/citation",
|
|
|
|
|
|
title: "DeepSeek source article",
|
|
|
|
|
|
text: "Snippet",
|
|
|
|
|
|
siteName: "Example",
|
|
|
|
|
|
kind: "search",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(classified.inlineCitationCandidates).toHaveLength(1);
|
|
|
|
|
|
expect(classified.inlineCitationCandidates[0]?.title).toBe("DeepSeek source article");
|
|
|
|
|
|
expect(classified.inlineCitationCandidates[0]?.site_name).toBe("Example");
|
|
|
|
|
|
expect(classified.citations).toHaveLength(1);
|
|
|
|
|
|
expect(classified.searchResults).toHaveLength(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-24 23:37:16 +08:00
|
|
|
|
it("keeps reasoning search pages as citation sources separate from answer content citations", () => {
|
|
|
|
|
|
const classified = classifyDeepseekSources({
|
|
|
|
|
|
inlineLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/body-citation",
|
|
|
|
|
|
title: "Answer body citation",
|
|
|
|
|
|
text: "[15]",
|
|
|
|
|
|
siteName: "Body Site",
|
|
|
|
|
|
kind: "inline",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
sourcePanelLinks: [],
|
|
|
|
|
|
searchPanelLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/reasoning-search",
|
|
|
|
|
|
title: "Reasoning searched page",
|
|
|
|
|
|
text: "浏览页面",
|
|
|
|
|
|
siteName: "Search Site",
|
|
|
|
|
|
kind: "search",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
|
|
|
|
|
|
"https://example.com/body-citation",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(classified.citations.map((item) => item.url)).toEqual([
|
|
|
|
|
|
"https://example.com/reasoning-search",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(classified.searchResults.map((item) => item.url)).toEqual([
|
|
|
|
|
|
"https://example.com/reasoning-search",
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
it("retains all revealed DeepSeek search results instead of truncating after 32 items", () => {
|
|
|
|
|
|
const searchPanelLinks = Array.from({ length: 78 }, (_, index) => ({
|
|
|
|
|
|
url: `https://example.com/search-${index + 1}`,
|
|
|
|
|
|
title: `Search ${index + 1}`,
|
|
|
|
|
|
text: `Snippet ${index + 1}`,
|
|
|
|
|
|
siteName: "Example",
|
|
|
|
|
|
kind: "search" as const,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const classified = classifyDeepseekSources({
|
|
|
|
|
|
inlineLinks: [],
|
|
|
|
|
|
sourcePanelLinks: [],
|
|
|
|
|
|
searchPanelLinks,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(classified.citations).toHaveLength(78);
|
|
|
|
|
|
expect(classified.searchResults).toHaveLength(78);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("merges revealed sidebar search links with the snapshot search links by normalized url", () => {
|
|
|
|
|
|
const merged = mergeDeepSeekRawSourceLinks(
|
|
|
|
|
|
[
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/report#top",
|
|
|
|
|
|
title: null,
|
|
|
|
|
|
text: "Visible title",
|
|
|
|
|
|
siteName: null,
|
|
|
|
|
|
kind: "search",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/report#bottom",
|
|
|
|
|
|
title: "Merged title",
|
|
|
|
|
|
text: "Merged snippet",
|
|
|
|
|
|
siteName: "Example",
|
|
|
|
|
|
kind: "search",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
url: "https://example.com/extra",
|
|
|
|
|
|
title: "Extra result",
|
|
|
|
|
|
text: "Extra snippet",
|
|
|
|
|
|
siteName: "Example",
|
|
|
|
|
|
kind: "search",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(merged).toHaveLength(2);
|
|
|
|
|
|
expect(merged[0]).toEqual(expect.objectContaining({
|
|
|
|
|
|
url: "https://example.com/report",
|
|
|
|
|
|
title: "Merged title",
|
|
|
|
|
|
text: "Visible title",
|
|
|
|
|
|
siteName: "Example",
|
|
|
|
|
|
}));
|
|
|
|
|
|
expect(merged[1]?.url).toBe("https://example.com/extra");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("normalizes deepseek raw source links into monitoring source items", () => {
|
|
|
|
|
|
const item = buildSourceItem({
|
|
|
|
|
|
url: "https://example.com/report#cite-1",
|
|
|
|
|
|
title: null,
|
|
|
|
|
|
text: "DeepSeek source title",
|
|
|
|
|
|
siteName: "Example",
|
|
|
|
|
|
kind: "source",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(item).toEqual(expect.objectContaining({
|
|
|
|
|
|
url: "https://example.com/report",
|
|
|
|
|
|
normalized_url: "https://example.com/report",
|
|
|
|
|
|
title: "DeepSeek source title",
|
|
|
|
|
|
site_name: "Example",
|
|
|
|
|
|
host: "example.com",
|
|
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-24 23:37:16 +08:00
|
|
|
|
it("unwraps DeepSeek redirect URLs before storing source items", () => {
|
|
|
|
|
|
const item = buildSourceItem({
|
|
|
|
|
|
url: "https://chat.deepseek.com/api/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref",
|
|
|
|
|
|
title: "Redirected source",
|
|
|
|
|
|
text: null,
|
|
|
|
|
|
siteName: null,
|
|
|
|
|
|
kind: "search",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(item).toEqual(expect.objectContaining({
|
|
|
|
|
|
url: "https://example.com/source",
|
|
|
|
|
|
normalized_url: "https://example.com/source",
|
|
|
|
|
|
host: "example.com",
|
|
|
|
|
|
}));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
it("does not use numeric citation markers as source titles", () => {
|
|
|
|
|
|
const item = buildSourceItem({
|
|
|
|
|
|
url: "https://example.com/report#cite-1",
|
|
|
|
|
|
title: null,
|
|
|
|
|
|
text: "-1",
|
|
|
|
|
|
siteName: null,
|
|
|
|
|
|
kind: "inline",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(item?.title).toBeNull();
|
|
|
|
|
|
expect(item?.site_name).toBe("example.com");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("treats a stable answer as complete even without citations", () => {
|
|
|
|
|
|
expect(isObservationComplete({
|
|
|
|
|
|
answer: "这是最终答案",
|
|
|
|
|
|
busy: false,
|
|
|
|
|
|
loginRequired: false,
|
|
|
|
|
|
challengeRequired: false,
|
|
|
|
|
|
signature: "sig-1",
|
|
|
|
|
|
})).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("ignores source-logo loading placeholders when computing busy signals", () => {
|
|
|
|
|
|
expect(detectDeepSeekBusySignals({
|
|
|
|
|
|
bodyText: "已阅读 10 个网页",
|
|
|
|
|
|
descriptors: [
|
|
|
|
|
|
"site_logo_loading site_logo_fallback",
|
|
|
|
|
|
"site_logo_loading site_logo_fallback",
|
|
|
|
|
|
],
|
|
|
|
|
|
})).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("recognizes deepseek toggle selected state from class names", () => {
|
|
|
|
|
|
expect(isDeepSeekToggleSelected("ds-toggle-button ds-toggle-button--selected")).toBe(true);
|
|
|
|
|
|
expect(isDeepSeekToggleSelected("ds-toggle-button")).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("only treats the bottom 网页 pill as the DeepSeek source trigger", () => {
|
|
|
|
|
|
expect(isDeepSeekWebpagesTriggerText("57 个网页")).toBe(true);
|
|
|
|
|
|
expect(isDeepSeekWebpagesTriggerText("已阅读 57 个网页")).toBe(true);
|
|
|
|
|
|
expect(isDeepSeekWebpagesTriggerText("搜索到 39 个网页")).toBe(false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-24 23:37:16 +08:00
|
|
|
|
it("normalizes DeepSeek model labels without mistaking search toggles for models", () => {
|
|
|
|
|
|
const snapshot = {
|
|
|
|
|
|
url: "https://chat.deepseek.com/",
|
|
|
|
|
|
title: "DeepSeek",
|
|
|
|
|
|
loginRequired: false,
|
|
|
|
|
|
loginReason: null,
|
|
|
|
|
|
challengeRequired: false,
|
|
|
|
|
|
challengeReason: null,
|
|
|
|
|
|
busy: false,
|
|
|
|
|
|
busySignals: [],
|
|
|
|
|
|
composerValue: null,
|
|
|
|
|
|
sendDisabled: false,
|
|
|
|
|
|
answer: "Final answer",
|
|
|
|
|
|
answerText: "Final answer",
|
|
|
|
|
|
reasoning: null,
|
|
|
|
|
|
signature: "sig-1",
|
|
|
|
|
|
currentModelLabel: "深度思考 (R1)",
|
|
|
|
|
|
providerRequestID: null,
|
|
|
|
|
|
requestID: null,
|
|
|
|
|
|
inlineLinks: [],
|
|
|
|
|
|
sourcePanelLinks: [],
|
|
|
|
|
|
searchPanelLinks: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
const sources = {
|
|
|
|
|
|
citations: [],
|
|
|
|
|
|
inlineCitationCandidates: [],
|
|
|
|
|
|
searchResults: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const r1Payload = buildDeepSeekPayload(snapshot, sources, "Final answer");
|
|
|
|
|
|
const searchPayload = buildDeepSeekPayload({
|
|
|
|
|
|
...snapshot,
|
|
|
|
|
|
currentModelLabel: "联网搜索",
|
|
|
|
|
|
}, sources, "Final answer");
|
|
|
|
|
|
|
|
|
|
|
|
expect(r1Payload.provider_model).toBe("DeepSeek-R1");
|
|
|
|
|
|
expect(searchPayload.provider_model).toBeNull();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-23 22:54:56 +08:00
|
|
|
|
it("builds json-safe payloads with null placeholders instead of undefined", () => {
|
|
|
|
|
|
const payload = buildDeepSeekPayload({
|
|
|
|
|
|
url: "https://chat.deepseek.com/",
|
|
|
|
|
|
title: "DeepSeek",
|
|
|
|
|
|
loginRequired: false,
|
|
|
|
|
|
loginReason: null,
|
|
|
|
|
|
challengeRequired: false,
|
|
|
|
|
|
challengeReason: null,
|
|
|
|
|
|
busy: false,
|
|
|
|
|
|
busySignals: [],
|
|
|
|
|
|
composerValue: null,
|
|
|
|
|
|
sendDisabled: false,
|
|
|
|
|
|
answer: "Final answer",
|
|
|
|
|
|
answerText: "Final answer",
|
|
|
|
|
|
reasoning: null,
|
|
|
|
|
|
signature: "sig-1",
|
|
|
|
|
|
currentModelLabel: null,
|
|
|
|
|
|
providerRequestID: null,
|
|
|
|
|
|
requestID: null,
|
|
|
|
|
|
inlineLinks: [],
|
|
|
|
|
|
sourcePanelLinks: [],
|
|
|
|
|
|
searchPanelLinks: [],
|
|
|
|
|
|
}, {
|
|
|
|
|
|
citations: [],
|
|
|
|
|
|
inlineCitationCandidates: [],
|
|
|
|
|
|
searchResults: [],
|
|
|
|
|
|
}, "Final answer");
|
|
|
|
|
|
|
|
|
|
|
|
expect(payload.provider_model).toBeNull();
|
|
|
|
|
|
expect(payload.provider_request_id).toBeNull();
|
|
|
|
|
|
expect(payload.request_id).toBeNull();
|
|
|
|
|
|
expect((payload.raw_response_json as Record<string, unknown>).provider_model).toBeNull();
|
|
|
|
|
|
expect((payload.raw_response_json as Record<string, unknown>).provider_request_id).toBeNull();
|
|
|
|
|
|
expect((payload.raw_response_json as Record<string, unknown>).request_id).toBeNull();
|
|
|
|
|
|
expect((payload.raw_response_json as Record<string, unknown>).signature).toBe("sig-1");
|
|
|
|
|
|
expect((payload.raw_response_json as Record<string, unknown>).inline_citation_candidates).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe("monitor adapter registry", () => {
|
|
|
|
|
|
it("registers deepseek as a monitor adapter", () => {
|
|
|
|
|
|
expect(getMonitorAdapter("deepseek")?.provider).toBe("deepseek");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|