fix: preserve monitor unknown and adapt doubao references
Desktop Client Build / Resolve Build Metadata (push) Successful in 32s
Backend CI / Backend (push) Successful in 15m37s
Desktop Client Build / Build Desktop Client (push) Successful in 22m53s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s

This commit is contained in:
2026-06-22 13:16:38 +08:00
parent 8c6789dca6
commit 365a41597e
4 changed files with 271 additions and 31 deletions
@@ -5,7 +5,9 @@ import { __doubaoTestUtils } from './doubao'
const { const {
buildSourceItem, buildSourceItem,
dedupeSourceItems, dedupeSourceItems,
doubaoModeKindFromLabelText,
extractQuestionText, extractQuestionText,
isDoubaoRecoverablePageError,
isNonCitationAssetDomain, isNonCitationAssetDomain,
isNonCitationAssetUrl, isNonCitationAssetUrl,
parseDoubaoStreamBody, parseDoubaoStreamBody,
@@ -20,6 +22,19 @@ describe('doubao adapter helpers', () => {
) )
}) })
it('treats query timeouts as recoverable page failures', () => {
expect(isDoubaoRecoverablePageError('doubao_query_timeout')).toBe(true)
expect(isDoubaoRecoverablePageError('doubao_page_navigation_interrupted')).toBe(true)
expect(isDoubaoRecoverablePageError('doubao_login_expired')).toBe(false)
})
it('maps the new expert/deep-thinking mode label to expert mode', () => {
expect(doubaoModeKindFromLabelText('专家')).toBe('expert')
expect(doubaoModeKindFromLabelText('专家 深度思考 研究级智能模型')).toBe('expert')
expect(doubaoModeKindFromLabelText('思考')).toBe('thinking')
expect(doubaoModeKindFromLabelText('快速')).toBe('fast')
})
it('filters byteimg and bytednsdoc asset CDN links from source items', () => { it('filters byteimg and bytednsdoc asset CDN links from source items', () => {
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true) expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true)
expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true) expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true)
@@ -99,6 +114,22 @@ describe('doubao adapter helpers', () => {
}) })
}) })
it('extracts source URLs embedded in Doubao thinking-panel text', () => {
const item = buildSourceItem({
url: '1. 合肥全屋定制榜单 https://www.doubao.com/link?target=https%3A%2F%2Fexample.com%2Fsource%3Ffrom%3Dthink#card',
title: '思考链路里的来源',
site_name: '示例站点',
})
expect(item).toMatchObject({
url: 'https://example.com/source?from=think',
normalized_url: 'https://example.com/source?from=think',
host: 'example.com',
title: '思考链路里的来源',
site_name: '示例站点',
})
})
it('parses Doubao answer deltas and source cards from the completion SSE stream', () => { it('parses Doubao answer deltas and source cards from the completion SSE stream', () => {
const sourceChunkPayload = { const sourceChunkPayload = {
patch_op: [ patch_op: [
+202 -28
View File
@@ -491,13 +491,27 @@ function looksLikeSourceUrl(value: string | null): boolean {
return false return false
} }
if (/^https?:\/\//i.test(normalized) || /^\/\//.test(normalized)) { if (/https?:\/\//i.test(normalized) || /^\/\//.test(normalized)) {
return true return true
} }
return /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(normalized) return /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(normalized)
} }
function extractEmbeddedHttpUrl(value: string | null): string | null {
const input = normalizeText(value)
if (!input) {
return null
}
const match = input.match(/https?:\/\/[^\s"'<>\\]+/i)?.[0] ?? null
if (!match) {
return null
}
return match.replace(/[),;]+$/u, '')
}
function extractDoubaoEmbeddedSourceUrl(url: URL, depth = 0): string | null { function extractDoubaoEmbeddedSourceUrl(url: URL, depth = 0): string | null {
if (depth > 3) { if (depth > 3) {
return null return null
@@ -550,7 +564,8 @@ function normalizeSourceUrl(value: string | null, depth = 0): string | null {
return null return null
} }
const candidates = [input] const embeddedURL = extractEmbeddedHttpUrl(input)
const candidates = embeddedURL && embeddedURL !== input ? [embeddedURL, input] : [input]
try { try {
const decoded = decodeURIComponent(input) const decoded = decodeURIComponent(input)
if (decoded && decoded !== input) { if (decoded && decoded !== input) {
@@ -1313,12 +1328,7 @@ async function ensurePageOnDoubaoChat(page: PlaywrightPage, signal: AbortSignal)
} }
const currentURL = safePageURL(page) const currentURL = safePageURL(page)
if (!currentURL.startsWith('https://www.doubao.com/')) { if (currentURL !== DOUBAO_BOOTSTRAP_URL) {
await page.goto(DOUBAO_BOOTSTRAP_URL, {
waitUntil: 'domcontentloaded',
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
})
} else if (!currentURL.startsWith('https://www.doubao.com/chat')) {
await page.goto(DOUBAO_BOOTSTRAP_URL, { await page.goto(DOUBAO_BOOTSTRAP_URL, {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS, timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
@@ -1337,6 +1347,26 @@ async function ensurePageOnDoubaoChat(page: PlaywrightPage, signal: AbortSignal)
} }
} }
async function resetDoubaoPageAfterRecoverableFailure(
page: PlaywrightPage,
error: string,
signal: AbortSignal,
): Promise<void> {
if (!isDoubaoRecoverablePageError(error) || signal.aborted) {
return
}
try {
await page.goto(DOUBAO_BOOTSTRAP_URL, {
waitUntil: 'domcontentloaded',
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
})
await page.waitForLoadState('networkidle', { timeout: 3_000 }).catch(() => undefined)
} catch {
// Best effort: timeout recovery must not mask the original unknown result.
}
}
function attachDoubaoResponseCapture(page: PlaywrightPage): { function attachDoubaoResponseCapture(page: PlaywrightPage): {
captures: DoubaoCaptureRecord[] captures: DoubaoCaptureRecord[]
flush: (timeoutMs: number) => Promise<void> flush: (timeoutMs: number) => Promise<void>
@@ -1483,7 +1513,16 @@ function doubaoModeProviderModel(mode: DoubaoAnswerModeKind | null): string | nu
} }
function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind | null { function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind | null {
switch (normalizeText(value)) { return doubaoModeKindFromLabelText(value)
}
function isDoubaoThinkingMode(mode: DoubaoAnswerModeKind | null): boolean {
return mode === 'thinking' || mode === 'expert'
}
function doubaoModeKindFromLabelText(value: string | null): DoubaoAnswerModeKind | null {
const label = normalizeText(value)
switch (label) {
case '快速': case '快速':
return 'fast' return 'fast'
case '思考': case '思考':
@@ -1491,6 +1530,9 @@ function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind |
case '专家': case '专家':
return 'expert' return 'expert'
default: default:
if (label?.includes('深度思考') || label?.includes('研究级智能模型')) {
return 'expert'
}
return null return null
} }
} }
@@ -1505,7 +1547,7 @@ function resolveProviderModel(pageResult: DoubaoPageQueryResult): string {
return label return label
} }
if (pageResult.thinkingModeApplied) { if (pageResult.thinkingModeApplied) {
return 'doubao-web-thinking' return pageResult.modeLabel === '专家' ? 'doubao-web-expert' : 'doubao-web-thinking'
} }
return pageResult.deepThinkEnabled ? 'doubao-web-thinking' : 'doubao-web' return pageResult.deepThinkEnabled ? 'doubao-web-thinking' : 'doubao-web'
} }
@@ -1807,13 +1849,22 @@ const doubaoQueryInPage = async (
if (label === ANSWER_MODE_LABELS.expert) { if (label === ANSWER_MODE_LABELS.expert) {
return 'expert' return 'expert'
} }
if (label.includes('专家')) {
return 'expert'
}
if (label.includes('思考') || label.includes('深度思考')) {
return 'thinking'
}
return null return null
} }
const isThinkingAnswerMode = (mode: AnswerModeKind | null): boolean =>
mode === 'thinking' || mode === 'expert'
const classifyAnswerModeText = (value: unknown): AnswerModeKind | null => { const classifyAnswerModeText = (value: unknown): AnswerModeKind | null => {
const multiline = normalizeMultiline(typeof value === 'string' ? value : null) const multiline = normalizeMultiline(typeof value === 'string' ? value : null)
const normalized = multiline ?? normalize(value) const normalized = multiline ?? normalize(value)
if (!normalized || /深度思考|正在思考|思考中/.test(normalized)) { if (!normalized || /正在思考|思考中/.test(normalized)) {
return null return null
} }
@@ -1845,7 +1896,7 @@ const doubaoQueryInPage = async (
if (compact.startsWith(label)) { if (compact.startsWith(label)) {
return answerModeKindFromLabel(label) return answerModeKindFromLabel(label)
} }
return null return answerModeKindFromLabel(label)
} }
const readElementAnswerMode = (element: Element | null | undefined): AnswerModeSnapshot => { const readElementAnswerMode = (element: Element | null | undefined): AnswerModeSnapshot => {
@@ -2049,7 +2100,7 @@ const doubaoQueryInPage = async (
): Promise<AnswerModeSnapshot & { applied: boolean | null }> => { ): Promise<AnswerModeSnapshot & { applied: boolean | null }> => {
const initialTrigger = findAnswerModeTrigger(shell, composer) const initialTrigger = findAnswerModeTrigger(shell, composer)
const initialMode = readElementAnswerMode(initialTrigger) const initialMode = readElementAnswerMode(initialTrigger)
if (initialMode.kind === 'thinking') { if (isThinkingAnswerMode(initialMode.kind)) {
return { ...initialMode, applied: true } return { ...initialMode, applied: true }
} }
if (!initialTrigger) { if (!initialTrigger) {
@@ -2059,12 +2110,14 @@ const doubaoQueryInPage = async (
clickModeElement(initialTrigger) clickModeElement(initialTrigger)
await wait(260) await wait(260)
const thinkingOption = findAnswerModeOption('thinking', initialTrigger) const thinkingOption =
findAnswerModeOption('expert', initialTrigger) ??
findAnswerModeOption('thinking', initialTrigger)
if (!thinkingOption) { if (!thinkingOption) {
pressEscape() pressEscape()
await wait(120) await wait(120)
const current = readCurrentAnswerMode(shell, composer) const current = readCurrentAnswerMode(shell, composer)
return { ...current, applied: current.kind === 'thinking' ? true : null } return { ...current, applied: isThinkingAnswerMode(current.kind) ? true : null }
} }
clickModeElement(thinkingOption) clickModeElement(thinkingOption)
@@ -2073,7 +2126,7 @@ const doubaoQueryInPage = async (
const current = readCurrentAnswerMode(shell, composer) const current = readCurrentAnswerMode(shell, composer)
return { return {
...current, ...current,
applied: current.kind === 'thinking' ? true : null, applied: isThinkingAnswerMode(current.kind) ? true : null,
} }
} }
@@ -2442,25 +2495,76 @@ const doubaoQueryInPage = async (
} }
const links: DoubaoDomLink[] = [] const links: DoubaoDomLink[] = []
const seen = new Set<string>() const seen = new Set<string>()
for (const node of Array.from(scope.querySelectorAll('a[href]'))) { const pushLink = (
if (!(node instanceof HTMLAnchorElement) || !isVisible(node)) { hrefCandidate: string | null | undefined,
continue element: HTMLElement,
} fallbackText: string | null,
const href = normalizeHref(node.getAttribute('href') ?? node.href) ): void => {
const href = normalizeHref(hrefCandidate ?? null)
if (!href || !isExternalHref(href) || seen.has(href)) { if (!href || !isExternalHref(href) || seen.has(href)) {
continue return
} }
seen.add(href) seen.add(href)
const visibleText = normalize(node.innerText) ?? normalize(node.textContent) const visibleText = normalize(element.innerText) ?? normalize(element.textContent) ?? fallbackText
links.push({ links.push({
url: href, url: href,
title: normalize(node.title) ?? normalize(node.getAttribute('aria-label')) ?? visibleText, title:
normalize(element.getAttribute('title')) ??
normalize(element.getAttribute('aria-label')) ??
visibleText,
text: visibleText, text: visibleText,
siteName: siteName:
normalize(node.getAttribute('data-site-name')) ?? normalize(element.getAttribute('data-site-name')) ??
normalize(node.dataset.siteName ?? null), normalize(element.dataset.siteName ?? null) ??
normalize(element.getAttribute('data-source-name')) ??
normalize(element.dataset.sourceName ?? null),
}) })
} }
const selector = [
'a[href]',
'[data-url]',
'[data-href]',
'[data-link]',
'[data-target-url]',
'[data-targetUrl]',
'[data-source-url]',
'[data-sourceUrl]',
'[data-jump-url]',
'[data-jumpUrl]',
'[aria-label*="http"]',
'[title*="http"]',
].join(',')
for (const node of Array.from(scope.querySelectorAll(selector))) {
if (!(node instanceof HTMLElement) || !isVisible(node)) {
continue
}
const candidates = [
node.getAttribute('href'),
node.getAttribute('data-url'),
node.getAttribute('data-href'),
node.getAttribute('data-link'),
node.getAttribute('data-target-url'),
node.getAttribute('data-targetUrl'),
node.getAttribute('data-source-url'),
node.getAttribute('data-sourceUrl'),
node.getAttribute('data-jump-url'),
node.getAttribute('data-jumpUrl'),
node.getAttribute('aria-label'),
node.getAttribute('title'),
]
for (const candidate of candidates) {
pushLink(candidate, node, null)
}
}
const text = normalizeMultiline(scope.innerText)
for (const match of text?.matchAll(/https?:\/\/[^\s"'<>\\]+/gi) ?? []) {
pushLink(match[0], scope, null)
}
return links return links
} }
@@ -2535,6 +2639,21 @@ const doubaoQueryInPage = async (
const normalizedQuestion = normalizeMultiline(questionText) const normalizedQuestion = normalizeMultiline(questionText)
const isReferenceHeaderLine = (line: string): boolean =>
/搜索\s*\d+\s*个关键词|参考\s*\d+\s*篇资料|查看\d+篇资料|参考资料|资料来源/.test(line)
const isReferenceQueryLine = (line: string): boolean =>
/[“"「].+[”"」]/.test(line) ||
/、/.test(line) ||
/关键词|搜索词|查询词/.test(line)
const isReferenceItemLine = (line: string): boolean =>
/^\d+[.、]\s*\S+/.test(line) &&
(/https?:\/\//i.test(line) ||
/新闻|新浪|搜狐|网易|腾讯|百度|知乎|什么值得买|列举网|装修网|家居|资料|来源|推荐|排名|榜|厂家|品牌|公司|工厂|门店/.test(
line,
))
// Tokens that only appear on Doubao's empty-chat surface (prompt templates, nav, feature chips). // Tokens that only appear on Doubao's empty-chat surface (prompt templates, nav, feature chips).
const EMPTY_CHAT_TOKENS = [ const EMPTY_CHAT_TOKENS = [
'快速', '快速',
@@ -2580,7 +2699,30 @@ const doubaoQueryInPage = async (
.split(/\n+/) .split(/\n+/)
.map((line) => line.trim()) .map((line) => line.trim())
.filter(Boolean) .filter(Boolean)
let inReferenceBlock = false
let referenceIntroBudget = 0
const kept = lines.filter((line) => { const kept = lines.filter((line) => {
if (/^-{4,}$/.test(line.replace(/\s+/g, ''))) {
inReferenceBlock = false
referenceIntroBudget = 0
return false
}
if (isReferenceHeaderLine(line)) {
inReferenceBlock = true
referenceIntroBudget = 1
return false
}
if (inReferenceBlock) {
if (referenceIntroBudget > 0 && isReferenceQueryLine(line)) {
referenceIntroBudget -= 1
return false
}
referenceIntroBudget = 0
if (isReferenceItemLine(line)) {
return false
}
inReferenceBlock = false
}
if (EMPTY_CHAT_TOKENS.includes(line)) { if (EMPTY_CHAT_TOKENS.includes(line)) {
return false return false
} }
@@ -2632,11 +2774,31 @@ const doubaoQueryInPage = async (
return best?.element ?? null return best?.element ?? null
} }
const collectReferencePanelLinks = (): DoubaoDomLink[] => {
const groups: DoubaoDomLink[][] = []
const nodes = Array.from(document.querySelectorAll('article, section, div, main, li'))
for (const node of nodes) {
if (!(node instanceof HTMLElement) || !isVisible(node)) {
continue
}
const text = normalizeMultiline(node.innerText)
if (!text || !isReferenceHeaderLine(text)) {
continue
}
const links = collectLinks(node)
if (links.length > 0) {
groups.push(links)
}
}
return mergeLinks(...groups)
}
const snapshotConversation = (composerTop: number | null): ConversationSnapshot => { const snapshotConversation = (composerTop: number | null): ConversationSnapshot => {
expandReferenceControls() expandReferenceControls()
const questionAnchor = findQuestionAnchor() const questionAnchor = findQuestionAnchor()
const anchorRect = questionAnchor?.getBoundingClientRect() ?? null const anchorRect = questionAnchor?.getBoundingClientRect() ?? null
const referencePanelLinks = collectReferencePanelLinks()
const candidates: Array<{ const candidates: Array<{
element: HTMLElement element: HTMLElement
@@ -2708,6 +2870,9 @@ const doubaoQueryInPage = async (
if (questionAnchor && rect.top > (anchorRect?.bottom ?? 0)) { if (questionAnchor && rect.top > (anchorRect?.bottom ?? 0)) {
score += 1_000 score += 1_000
} }
if (rawText && isReferenceHeaderLine(rawText)) {
score -= 1_200
}
if (score <= 0) { if (score <= 0) {
continue continue
@@ -2801,6 +2966,7 @@ const doubaoQueryInPage = async (
} }
const scopedReferenceLinks = mergeLinks( const scopedReferenceLinks = mergeLinks(
referencePanelLinks,
...orderedCandidates ...orderedCandidates
.filter((candidate) => { .filter((candidate) => {
if (!candidate.text) { if (!candidate.text) {
@@ -2856,7 +3022,7 @@ const doubaoQueryInPage = async (
title: normalize(document.title), title: normalize(document.title),
modelLabel: resolveModelLabel(), modelLabel: resolveModelLabel(),
modeLabel, modeLabel,
thinkingModeApplied: modeKind === 'thinking' ? true : (modeSnapshot.applied ?? null), thinkingModeApplied: isThinkingAnswerMode(modeKind) ? true : (modeSnapshot.applied ?? null),
deepThinkEnabled: toggleState(deepThinkButton), deepThinkEnabled: toggleState(deepThinkButton),
domAnswer: snapshot.answerText, domAnswer: snapshot.answerText,
domReasoning: snapshot.reasoningText, domReasoning: snapshot.reasoningText,
@@ -2973,7 +3139,9 @@ const doubaoQueryInPage = async (
answerMode = { answerMode = {
kind: currentMode.kind ?? answerMode.kind, kind: currentMode.kind ?? answerMode.kind,
label: currentMode.label ?? answerMode.label, label: currentMode.label ?? answerMode.label,
applied: (currentMode.kind ?? answerMode.kind) === 'thinking' ? true : answerMode.applied, applied: isThinkingAnswerMode(currentMode.kind ?? answerMode.kind)
? true
: answerMode.applied,
} }
return { return {
ok: true, ok: true,
@@ -3077,6 +3245,10 @@ export const doubaoAdapter: MonitorAdapter = {
const hasRecoveredContent = hasRecoveredAnswer || searchResults.length > 0 const hasRecoveredContent = hasRecoveredAnswer || searchResults.length > 0
const retryableStreamError = isDoubaoRetryableErrorMessage(streamSummary.error_message) const retryableStreamError = isDoubaoRetryableErrorMessage(streamSummary.error_message)
if (pageResult.ok === false && pageRecoverableError && !hasRecoveredAnswer) {
await resetDoubaoPageAfterRecoverableFailure(page, pageResult.error, context.signal)
}
if (pageResult.ok === false) { if (pageResult.ok === false) {
const failed = pageResult const failed = pageResult
const detail = normalizeText(failed.detail) ?? 'unknown' const detail = normalizeText(failed.detail) ?? 'unknown'
@@ -3255,9 +3427,11 @@ export const doubaoAdapter: MonitorAdapter = {
export const __doubaoTestUtils = { export const __doubaoTestUtils = {
buildSourceItem, buildSourceItem,
dedupeSourceItems, dedupeSourceItems,
doubaoModeKindFromLabelText,
extractQuestionText, extractQuestionText,
isNonCitationAssetDomain, isNonCitationAssetDomain,
isNonCitationAssetUrl, isNonCitationAssetUrl,
isDoubaoRecoverablePageError,
parseDoubaoStreamBody, parseDoubaoStreamBody,
resolveDoubaoSubmissionAnswer, resolveDoubaoSubmissionAnswer,
} }
@@ -947,7 +947,7 @@ func (s *DesktopTaskService) Complete(ctx context.Context, client *repository.De
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required") return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
} }
finalStatus := normalizeDesktopTaskTerminalStatus(req.Status) finalStatus := normalizeDesktopTaskCompletionStatus(req.Status)
resultJSON, err := marshalOptionalJSON(req.Payload) resultJSON, err := marshalOptionalJSON(req.Payload)
if err != nil { if err != nil {
return nil, response.ErrBadRequest(40086, "invalid_desktop_task_payload", "payload must be serializable") return nil, response.ErrBadRequest(40086, "invalid_desktop_task_payload", "payload must be serializable")
@@ -1520,7 +1520,7 @@ func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
Platform: platform, Platform: platform,
Kind: kind, Kind: kind,
Payload: json.RawMessage(payload), Payload: json.RawMessage(payload),
Status: normalizeDesktopTaskTerminalStatus(status), Status: normalizeDesktopTaskViewStatus(kind, status),
PublishJobStatus: publishJobStatus, PublishJobStatus: publishJobStatus,
ComplianceBlockedRecordID: complianceBlockedRecordID, ComplianceBlockedRecordID: complianceBlockedRecordID,
ComplianceBlockedAt: complianceBlockedAt, ComplianceBlockedAt: complianceBlockedAt,
@@ -1598,7 +1598,7 @@ func buildDesktopTaskView(task *repository.DesktopTask) DesktopTaskView {
Platform: task.Platform, Platform: task.Platform,
Kind: task.Kind, Kind: task.Kind,
Payload: json.RawMessage(task.Payload), Payload: json.RawMessage(task.Payload),
Status: normalizeDesktopTaskTerminalStatus(task.Status), Status: normalizeDesktopTaskViewStatus(task.Kind, task.Status),
DedupKey: task.DedupKey, DedupKey: task.DedupKey,
ActiveAttemptID: activeAttemptID, ActiveAttemptID: activeAttemptID,
LeaseExpiresAt: task.LeaseExpiresAt, LeaseExpiresAt: task.LeaseExpiresAt,
@@ -1637,6 +1637,22 @@ func normalizeDesktopTaskTerminalStatus(status string) string {
} }
} }
func normalizeDesktopTaskCompletionStatus(status string) string {
switch strings.TrimSpace(status) {
case "succeeded", "failed", "unknown":
return strings.TrimSpace(status)
default:
return strings.TrimSpace(status)
}
}
func normalizeDesktopTaskViewStatus(kind string, status string) string {
if strings.TrimSpace(kind) == "publish" {
return normalizeDesktopTaskTerminalStatus(status)
}
return strings.TrimSpace(status)
}
type desktopTaskRecoveryMode string type desktopTaskRecoveryMode string
const ( const (
@@ -138,6 +138,25 @@ func TestResolvePublishRecoveryOutcome(t *testing.T) {
} }
} }
func TestNormalizeDesktopTaskCompletionStatusPreservesUnknown(t *testing.T) {
t.Parallel()
if got := normalizeDesktopTaskCompletionStatus("unknown"); got != "unknown" {
t.Fatalf("normalizeDesktopTaskCompletionStatus(unknown) = %q, want unknown", got)
}
}
func TestNormalizeDesktopTaskViewStatusOnlyMapsPublishUnknownToFailed(t *testing.T) {
t.Parallel()
if got := normalizeDesktopTaskViewStatus("monitor", "unknown"); got != "unknown" {
t.Fatalf("monitor unknown view status = %q, want unknown", got)
}
if got := normalizeDesktopTaskViewStatus("publish", "unknown"); got != "failed" {
t.Fatalf("publish unknown view status = %q, want failed", got)
}
}
func TestIsComplianceInvalidArticleVersionError(t *testing.T) { func TestIsComplianceInvalidArticleVersionError(t *testing.T) {
t.Parallel() t.Parallel()