73712994 disabled voice-first on native by gating the TTS preload behind
!isNativeAppRuntime(), which left App users with the degraded text bubble
plus a 播报 button. The voice-line, 转文字 toggle and 收起全文 markup were
still present but unreachable.
Re-enable the preload on every platform and open the transcript when the
message is created, so the answer is readable while synthesis runs.
Collapse the transcript when playback starts rather than when synthesis
finishes: on-device synthesis settles in 2-6s, so collapsing on ready
pulled the answer away mid-sentence. transcriptPinned records a manual
toggle so the automatic collapse never overrides the reader.
Bound the preparing bubble at 20s (the upstream DashScope ceiling) so a
stalled synthesis degrades to the plain text bubble instead of showing an
endless placeholder duration.
Replace the tautological assertion that pinned the previous behaviour by
matching source strings with assertions on the behaviour itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
78 lines
3.4 KiB
JavaScript
78 lines
3.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
|
|
const source = async (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
|
|
|
test('问数字师傅统一使用 Agent 文本与媒体入口', async () => {
|
|
const [service, page] = await Promise.all([
|
|
source('../src/services/agent.ts'),
|
|
source('../src/pages/user/sop/index.vue')
|
|
]);
|
|
|
|
assert.match(service, /\/api\/aihr\/agent\/messages'/);
|
|
assert.match(service, /\/api\/aihr\/agent\/messages\/media/);
|
|
assert.doesNotMatch(service, /toolCode/);
|
|
assert.doesNotMatch(service, /\/api\/knowledge\/query/);
|
|
assert.match(page, /askAgent/);
|
|
assert.match(page, /askAgentWithMedia/);
|
|
assert.doesNotMatch(page, /await searchKnowledge\(/);
|
|
assert.doesNotMatch(page, /await queryKnowledgeTool\(/);
|
|
});
|
|
|
|
test('Agent 响应按结构化状态渲染来源、澄清和确认动作', async () => {
|
|
const [types, service, page] = await Promise.all([
|
|
source('../src/types/api.ts'),
|
|
source('../src/services/agent.ts'),
|
|
source('../src/pages/user/sop/index.vue')
|
|
]);
|
|
|
|
assert.match(types, /export type AgentStatus = 'COMPLETED' \| 'NEEDS_INPUT' \| 'NEEDS_CONFIRMATION'/);
|
|
assert.match(types, /sourceSummary\?: AgentSourceSummary\[\]/);
|
|
assert.match(types, /agentDraftId\?: string/);
|
|
assert.match(service, /status === 'NO_EVIDENCE'/);
|
|
assert.match(service, /agentDraftId: response\.actionDraft\.draftId/);
|
|
assert.match(page, /agent-source-chip/);
|
|
assert.match(page, /msg\.result\.agentStatus === 'NEEDS_INPUT'/);
|
|
assert.match(page, /consentWebResearch/);
|
|
assert.doesNotMatch(page, /result\.answer\.(?:includes|match|startsWith)/);
|
|
});
|
|
|
|
test('回复语音优先,但文字在合成期间可读且不覆盖读者选择', async () => {
|
|
const page = await source('../src/pages/user/sop/index.vue');
|
|
|
|
// Voice-first applies on every platform, not just H5.
|
|
assert.match(page, /const shouldPreloadVoice = Boolean\(spoken\);/);
|
|
assert.doesNotMatch(page, /shouldPreloadVoice = [^;]*isNativeAppRuntime/);
|
|
|
|
// Text is readable while the voice is still preparing.
|
|
assert.match(page, /transcriptOpen: true/);
|
|
|
|
// Finishing synthesis must not yank the text away; only starting playback folds it.
|
|
assert.match(
|
|
page,
|
|
/msg\.unread = false;\s*(?:\/\/[^\n]*\n\s*)*if \(!msg\.transcriptPinned\) msg\.transcriptOpen = false;\s*void speechPlayback\.toggle/
|
|
);
|
|
assert.doesNotMatch(
|
|
page,
|
|
/msg\.voiceStatus = 'ready';\s*if \(!msg\.transcriptPinned\)/
|
|
);
|
|
assert.match(page, /const toggleTranscript = [^}]*transcriptPinned = true;/s);
|
|
assert.match(page, /const collapseTranscript = [^}]*transcriptPinned = true;/s);
|
|
|
|
// A hanging synthesis degrades to the plain text bubble instead of an endless placeholder.
|
|
assert.match(page, /VOICE_PREPARE_TIMEOUT_MS/);
|
|
assert.match(page, /Promise\.race\(\[\s*speechPlayback\.preload\(spoken\)/);
|
|
});
|
|
|
|
test('Agent 确认卡通过不透明 draftId 调用动作端点', async () => {
|
|
const [assistantService, card] = await Promise.all([
|
|
source('../src/services/personal-assistant.ts'),
|
|
source('../src/components/chat/MemoryCandidateCard.vue')
|
|
]);
|
|
|
|
assert.match(assistantService, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/confirm/);
|
|
assert.match(assistantService, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/dismiss/);
|
|
assert.match(card, /props\.candidate\.agentDraftId \|\| props\.candidate\.id/);
|
|
});
|