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
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:
@@ -5,7 +5,9 @@ import { __doubaoTestUtils } from './doubao'
|
||||
const {
|
||||
buildSourceItem,
|
||||
dedupeSourceItems,
|
||||
doubaoModeKindFromLabelText,
|
||||
extractQuestionText,
|
||||
isDoubaoRecoverablePageError,
|
||||
isNonCitationAssetDomain,
|
||||
isNonCitationAssetUrl,
|
||||
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', () => {
|
||||
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.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', () => {
|
||||
const sourceChunkPayload = {
|
||||
patch_op: [
|
||||
|
||||
@@ -491,13 +491,27 @@ function looksLikeSourceUrl(value: string | null): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(normalized) || /^\/\//.test(normalized)) {
|
||||
if (/https?:\/\//i.test(normalized) || /^\/\//.test(normalized)) {
|
||||
return true
|
||||
}
|
||||
|
||||
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 {
|
||||
if (depth > 3) {
|
||||
return null
|
||||
@@ -550,7 +564,8 @@ function normalizeSourceUrl(value: string | null, depth = 0): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
const candidates = [input]
|
||||
const embeddedURL = extractEmbeddedHttpUrl(input)
|
||||
const candidates = embeddedURL && embeddedURL !== input ? [embeddedURL, input] : [input]
|
||||
try {
|
||||
const decoded = decodeURIComponent(input)
|
||||
if (decoded && decoded !== input) {
|
||||
@@ -1313,12 +1328,7 @@ async function ensurePageOnDoubaoChat(page: PlaywrightPage, signal: AbortSignal)
|
||||
}
|
||||
|
||||
const currentURL = safePageURL(page)
|
||||
if (!currentURL.startsWith('https://www.doubao.com/')) {
|
||||
await page.goto(DOUBAO_BOOTSTRAP_URL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
|
||||
})
|
||||
} else if (!currentURL.startsWith('https://www.doubao.com/chat')) {
|
||||
if (currentURL !== DOUBAO_BOOTSTRAP_URL) {
|
||||
await page.goto(DOUBAO_BOOTSTRAP_URL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
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): {
|
||||
captures: DoubaoCaptureRecord[]
|
||||
flush: (timeoutMs: number) => Promise<void>
|
||||
@@ -1483,7 +1513,16 @@ function doubaoModeProviderModel(mode: DoubaoAnswerModeKind | null): string | nu
|
||||
}
|
||||
|
||||
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 '快速':
|
||||
return 'fast'
|
||||
case '思考':
|
||||
@@ -1491,6 +1530,9 @@ function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind |
|
||||
case '专家':
|
||||
return 'expert'
|
||||
default:
|
||||
if (label?.includes('深度思考') || label?.includes('研究级智能模型')) {
|
||||
return 'expert'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1505,7 +1547,7 @@ function resolveProviderModel(pageResult: DoubaoPageQueryResult): string {
|
||||
return label
|
||||
}
|
||||
if (pageResult.thinkingModeApplied) {
|
||||
return 'doubao-web-thinking'
|
||||
return pageResult.modeLabel === '专家' ? 'doubao-web-expert' : 'doubao-web-thinking'
|
||||
}
|
||||
return pageResult.deepThinkEnabled ? 'doubao-web-thinking' : 'doubao-web'
|
||||
}
|
||||
@@ -1807,13 +1849,22 @@ const doubaoQueryInPage = async (
|
||||
if (label === ANSWER_MODE_LABELS.expert) {
|
||||
return 'expert'
|
||||
}
|
||||
if (label.includes('专家')) {
|
||||
return 'expert'
|
||||
}
|
||||
if (label.includes('思考') || label.includes('深度思考')) {
|
||||
return 'thinking'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const isThinkingAnswerMode = (mode: AnswerModeKind | null): boolean =>
|
||||
mode === 'thinking' || mode === 'expert'
|
||||
|
||||
const classifyAnswerModeText = (value: unknown): AnswerModeKind | null => {
|
||||
const multiline = normalizeMultiline(typeof value === 'string' ? value : null)
|
||||
const normalized = multiline ?? normalize(value)
|
||||
if (!normalized || /深度思考|正在思考|思考中/.test(normalized)) {
|
||||
if (!normalized || /正在思考|思考中/.test(normalized)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1845,7 +1896,7 @@ const doubaoQueryInPage = async (
|
||||
if (compact.startsWith(label)) {
|
||||
return answerModeKindFromLabel(label)
|
||||
}
|
||||
return null
|
||||
return answerModeKindFromLabel(label)
|
||||
}
|
||||
|
||||
const readElementAnswerMode = (element: Element | null | undefined): AnswerModeSnapshot => {
|
||||
@@ -2049,7 +2100,7 @@ const doubaoQueryInPage = async (
|
||||
): Promise<AnswerModeSnapshot & { applied: boolean | null }> => {
|
||||
const initialTrigger = findAnswerModeTrigger(shell, composer)
|
||||
const initialMode = readElementAnswerMode(initialTrigger)
|
||||
if (initialMode.kind === 'thinking') {
|
||||
if (isThinkingAnswerMode(initialMode.kind)) {
|
||||
return { ...initialMode, applied: true }
|
||||
}
|
||||
if (!initialTrigger) {
|
||||
@@ -2059,12 +2110,14 @@ const doubaoQueryInPage = async (
|
||||
clickModeElement(initialTrigger)
|
||||
await wait(260)
|
||||
|
||||
const thinkingOption = findAnswerModeOption('thinking', initialTrigger)
|
||||
const thinkingOption =
|
||||
findAnswerModeOption('expert', initialTrigger) ??
|
||||
findAnswerModeOption('thinking', initialTrigger)
|
||||
if (!thinkingOption) {
|
||||
pressEscape()
|
||||
await wait(120)
|
||||
const current = readCurrentAnswerMode(shell, composer)
|
||||
return { ...current, applied: current.kind === 'thinking' ? true : null }
|
||||
return { ...current, applied: isThinkingAnswerMode(current.kind) ? true : null }
|
||||
}
|
||||
|
||||
clickModeElement(thinkingOption)
|
||||
@@ -2073,7 +2126,7 @@ const doubaoQueryInPage = async (
|
||||
const current = readCurrentAnswerMode(shell, composer)
|
||||
return {
|
||||
...current,
|
||||
applied: current.kind === 'thinking' ? true : null,
|
||||
applied: isThinkingAnswerMode(current.kind) ? true : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2442,25 +2495,76 @@ const doubaoQueryInPage = async (
|
||||
}
|
||||
const links: DoubaoDomLink[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const node of Array.from(scope.querySelectorAll('a[href]'))) {
|
||||
if (!(node instanceof HTMLAnchorElement) || !isVisible(node)) {
|
||||
continue
|
||||
}
|
||||
const href = normalizeHref(node.getAttribute('href') ?? node.href)
|
||||
const pushLink = (
|
||||
hrefCandidate: string | null | undefined,
|
||||
element: HTMLElement,
|
||||
fallbackText: string | null,
|
||||
): void => {
|
||||
const href = normalizeHref(hrefCandidate ?? null)
|
||||
if (!href || !isExternalHref(href) || seen.has(href)) {
|
||||
continue
|
||||
return
|
||||
}
|
||||
seen.add(href)
|
||||
const visibleText = normalize(node.innerText) ?? normalize(node.textContent)
|
||||
const visibleText = normalize(element.innerText) ?? normalize(element.textContent) ?? fallbackText
|
||||
links.push({
|
||||
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,
|
||||
siteName:
|
||||
normalize(node.getAttribute('data-site-name')) ??
|
||||
normalize(node.dataset.siteName ?? null),
|
||||
normalize(element.getAttribute('data-site-name')) ??
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2535,6 +2639,21 @@ const doubaoQueryInPage = async (
|
||||
|
||||
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).
|
||||
const EMPTY_CHAT_TOKENS = [
|
||||
'快速',
|
||||
@@ -2580,7 +2699,30 @@ const doubaoQueryInPage = async (
|
||||
.split(/\n+/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
let inReferenceBlock = false
|
||||
let referenceIntroBudget = 0
|
||||
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)) {
|
||||
return false
|
||||
}
|
||||
@@ -2632,11 +2774,31 @@ const doubaoQueryInPage = async (
|
||||
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 => {
|
||||
expandReferenceControls()
|
||||
|
||||
const questionAnchor = findQuestionAnchor()
|
||||
const anchorRect = questionAnchor?.getBoundingClientRect() ?? null
|
||||
const referencePanelLinks = collectReferencePanelLinks()
|
||||
|
||||
const candidates: Array<{
|
||||
element: HTMLElement
|
||||
@@ -2708,6 +2870,9 @@ const doubaoQueryInPage = async (
|
||||
if (questionAnchor && rect.top > (anchorRect?.bottom ?? 0)) {
|
||||
score += 1_000
|
||||
}
|
||||
if (rawText && isReferenceHeaderLine(rawText)) {
|
||||
score -= 1_200
|
||||
}
|
||||
|
||||
if (score <= 0) {
|
||||
continue
|
||||
@@ -2801,6 +2966,7 @@ const doubaoQueryInPage = async (
|
||||
}
|
||||
|
||||
const scopedReferenceLinks = mergeLinks(
|
||||
referencePanelLinks,
|
||||
...orderedCandidates
|
||||
.filter((candidate) => {
|
||||
if (!candidate.text) {
|
||||
@@ -2856,7 +3022,7 @@ const doubaoQueryInPage = async (
|
||||
title: normalize(document.title),
|
||||
modelLabel: resolveModelLabel(),
|
||||
modeLabel,
|
||||
thinkingModeApplied: modeKind === 'thinking' ? true : (modeSnapshot.applied ?? null),
|
||||
thinkingModeApplied: isThinkingAnswerMode(modeKind) ? true : (modeSnapshot.applied ?? null),
|
||||
deepThinkEnabled: toggleState(deepThinkButton),
|
||||
domAnswer: snapshot.answerText,
|
||||
domReasoning: snapshot.reasoningText,
|
||||
@@ -2973,7 +3139,9 @@ const doubaoQueryInPage = async (
|
||||
answerMode = {
|
||||
kind: currentMode.kind ?? answerMode.kind,
|
||||
label: currentMode.label ?? answerMode.label,
|
||||
applied: (currentMode.kind ?? answerMode.kind) === 'thinking' ? true : answerMode.applied,
|
||||
applied: isThinkingAnswerMode(currentMode.kind ?? answerMode.kind)
|
||||
? true
|
||||
: answerMode.applied,
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
@@ -3077,6 +3245,10 @@ export const doubaoAdapter: MonitorAdapter = {
|
||||
const hasRecoveredContent = hasRecoveredAnswer || searchResults.length > 0
|
||||
const retryableStreamError = isDoubaoRetryableErrorMessage(streamSummary.error_message)
|
||||
|
||||
if (pageResult.ok === false && pageRecoverableError && !hasRecoveredAnswer) {
|
||||
await resetDoubaoPageAfterRecoverableFailure(page, pageResult.error, context.signal)
|
||||
}
|
||||
|
||||
if (pageResult.ok === false) {
|
||||
const failed = pageResult
|
||||
const detail = normalizeText(failed.detail) ?? 'unknown'
|
||||
@@ -3255,9 +3427,11 @@ export const doubaoAdapter: MonitorAdapter = {
|
||||
export const __doubaoTestUtils = {
|
||||
buildSourceItem,
|
||||
dedupeSourceItems,
|
||||
doubaoModeKindFromLabelText,
|
||||
extractQuestionText,
|
||||
isNonCitationAssetDomain,
|
||||
isNonCitationAssetUrl,
|
||||
isDoubaoRecoverablePageError,
|
||||
parseDoubaoStreamBody,
|
||||
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")
|
||||
}
|
||||
|
||||
finalStatus := normalizeDesktopTaskTerminalStatus(req.Status)
|
||||
finalStatus := normalizeDesktopTaskCompletionStatus(req.Status)
|
||||
resultJSON, err := marshalOptionalJSON(req.Payload)
|
||||
if err != nil {
|
||||
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,
|
||||
Kind: kind,
|
||||
Payload: json.RawMessage(payload),
|
||||
Status: normalizeDesktopTaskTerminalStatus(status),
|
||||
Status: normalizeDesktopTaskViewStatus(kind, status),
|
||||
PublishJobStatus: publishJobStatus,
|
||||
ComplianceBlockedRecordID: complianceBlockedRecordID,
|
||||
ComplianceBlockedAt: complianceBlockedAt,
|
||||
@@ -1598,7 +1598,7 @@ func buildDesktopTaskView(task *repository.DesktopTask) DesktopTaskView {
|
||||
Platform: task.Platform,
|
||||
Kind: task.Kind,
|
||||
Payload: json.RawMessage(task.Payload),
|
||||
Status: normalizeDesktopTaskTerminalStatus(task.Status),
|
||||
Status: normalizeDesktopTaskViewStatus(task.Kind, task.Status),
|
||||
DedupKey: task.DedupKey,
|
||||
ActiveAttemptID: activeAttemptID,
|
||||
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
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user