Files
geo/apps/desktop-client/src/main/adapters/kimi.test.ts
T
root 0f3a9a977e
Desktop Client Build / Resolve Build Metadata (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Frontend CI / Frontend (push) Successful in 6m6s
fix(kimi): keep monitor robust against K2.6 redesign noise
K2.6 introduced new UI surfaces that polluted the answer extraction and
the busy-signal probe, breaking monitor runs even though Kimi rendered a
correct answer:

- collectCandidates now drops elements that contain a chat composer child
  (<textarea>, non-hidden <input>, [contenteditable]). This filters
  wrapper containers like .chat-detail-content that the wide
  [class*="content"] fallback would otherwise pick, pulling editor
  placeholder text ("可快捷使用技能", "Kimi K2.6 已就位") into the answer.
- isAuxiliaryPanelElement skips elements with role="dialog" /
  "alertdialog", aria-modal="true", or the <dialog> tag, so the K2.6
  launch-announcement popover ("Kimi K2.6, 已就位 / 一键部署 OpenClaw")
  no longer counts as answer content.
- Drop the class-based loading-indicator probe entirely. The previous
  global [class*="loading"]/"stream"/"typing"/"spinner" sweep matched
  K2.6's always-on text-stream wrapper and held busy=true forever, which
  caused kimi_query_timeout even after the assistant turn had fully
  rendered. The textual busySignalsPattern (思考中/搜索中/生成中…) is
  the actual semantic signal and survives any UI rename.
- Surface a capacityNotice from the assistant turn when Kimi shows its
  short capacity-exhausted message ("算力不够 / 请稍后再试"). The top
  level falls back to it when answerText is empty so the SaaS pipeline
  records the user-visible message instead of an unknown row, with
  capacity_limited / capacity_notice flagged in raw_response_json.

All filters lean on HTML form / ARIA semantics and visible text rather
than Kimi-specific BEM classes, so they survive future redesigns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:51:49 +08:00

171 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { __kimiTestUtils } from './kimi'
const {
buildSourceItem,
classifyKimiSources,
isKimiAnswerComplete,
isKimiPromotionalAnswerNoise,
mergeKimiSourceLinksIntoContentSnapshot,
normalizeUrl,
} = __kimiTestUtils
function buildSnapshot(
overrides: Partial<Parameters<typeof classifyKimiSources>[0]>,
): Parameters<typeof classifyKimiSources>[0] {
return {
url: 'https://www.kimi.com/chat/test',
title: 'Kimi',
currentModelLabel: 'K2.6 思考',
loginRequired: false,
loginReason: null,
busy: false,
busySignals: [],
sendDisabled: null,
answerText: '答案',
answerBlocks: ['答案'],
reasoningText: null,
reasoningBlocks: [],
capacityNotice: null,
questionMatched: true,
candidateAnswerCount: 1,
candidateReasoningCount: 0,
citationLinks: [],
panelCitationLinks: [],
inlineCitationCandidateLinks: [],
searchResultLinks: [],
signature: 'sig',
...overrides,
}
}
describe('kimi adapter helpers', () => {
it('unwraps Kimi redirect URLs before storing source items', () => {
expect(
normalizeUrl('https://www.kimi.com/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref'),
).toBe('https://example.com/source')
expect(
buildSourceItem({
url: 'https://www.kimi.com/redirect?target=https%3A%2F%2Fsource.example.com%2Fnews%3Fid%3D1',
title: '搜索结果标题',
text: null,
siteName: '来源站点',
}),
).toMatchObject({
url: 'https://source.example.com/news?id=1',
normalized_url: 'https://source.example.com/news?id=1',
title: '搜索结果标题',
site_name: '来源站点',
host: 'source.example.com',
})
})
it('uses search web results as citation sources and footer references as content citation candidates', () => {
const classified = classifyKimiSources(
buildSnapshot({
searchResultLinks: [
{
url: 'https://search.example.com/a',
title: '搜索网页 A',
text: '搜索网页 A',
siteName: '搜索站点',
},
],
panelCitationLinks: [
{
url: 'https://content.example.com/cited',
title: '页尾引用文章',
text: '页尾引用文章',
siteName: '内容站点',
},
],
inlineCitationCandidateLinks: [
{
url: 'https://content.example.com/site',
title: null,
text: '内容站点',
siteName: '内容站点',
},
],
}),
)
expect(classified.citations).toHaveLength(1)
expect(classified.citations[0]?.url).toBe('https://search.example.com/a')
expect(classified.searchResults[0]?.url).toBe('https://search.example.com/a')
expect(classified.panelCitations[0]?.url).toBe('https://content.example.com/cited')
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
'https://content.example.com/cited',
'https://content.example.com/site',
])
})
it('falls back to footer references as citation sources when no search web results are visible', () => {
const classified = classifyKimiSources(
buildSnapshot({
panelCitationLinks: [
{
url: 'https://content.example.com/cited',
title: '页尾引用文章',
text: '页尾引用文章',
siteName: '内容站点',
},
],
}),
)
expect(classified.searchResults).toHaveLength(0)
expect(classified.citations).toHaveLength(1)
expect(classified.citations[0]?.url).toBe('https://content.example.com/cited')
})
it('keeps the answer text from the content snapshot when merging side-panel sources', () => {
const merged = mergeKimiSourceLinksIntoContentSnapshot(
buildSnapshot({
answerText: '真实回答正文',
answerBlocks: ['真实回答正文'],
}),
buildSnapshot({
answerText: '搜索网页 48\nhttps://example.com/a 很长的侧栏文本',
answerBlocks: ['搜索网页 48\nhttps://example.com/a 很长的侧栏文本'],
searchResultLinks: [
{
url: 'https://example.com/a',
title: '搜索结果 A',
text: '搜索结果 A',
siteName: 'Example',
},
],
}),
)
expect(merged.answerText).toBe('真实回答正文')
expect(merged.answerBlocks).toEqual(['真实回答正文'])
expect(merged.searchResultLinks).toHaveLength(1)
expect(merged.searchResultLinks[0]?.url).toBe('https://example.com/a')
})
it('rejects Kimi K2.6 marketing copy as an answer', () => {
const promotionalText = [
'Kimi K2.6,已就位!',
'Coding、Agent 集群、Kimi Claw 同步升级',
'Agent 集群,升级了',
'多技能调用,多产物类型一次性交付',
'Kimi Claw 已就位!',
'一键部署 OpenClawKimi 24小时为你干活',
].join('\n')
expect(isKimiPromotionalAnswerNoise(promotionalText)).toBe(true)
expect(
isKimiAnswerComplete(
buildSnapshot({
answerText: promotionalText,
answerBlocks: [promotionalText],
}),
),
).toBe(false)
})
})