feat(aihr): close functional checklist gaps

This commit is contained in:
key
2026-08-02 12:15:45 +08:00
parent 699cc08050
commit 6ad8a017a4
80 changed files with 4930 additions and 430 deletions
+2
View File
@@ -90,6 +90,8 @@ test('消息咨询只以消息 ID 作为受控上下文,并让每轮文字或
assert.match(page, /broadcastMessageId \? undefined : currentProject\.value\?\.projectCode/);
assert.match(page, /const agentOptions = \{[\s\S]*?broadcastMessageId,[\s\S]*?\}/);
assert.match(page, /askAgent\(text, agentOptions\)/);
assert.match(page, /transcribeSpeechCapture\([\s\S]*?\n[\s\S]*?void sendQuestion\(text, \{ voice: true \}\)/);
assert.match(page, /transcribeSpeechFile\([\s\S]*?\n[\s\S]*?void sendQuestion\(text, \{ voice: true \}\)/);
assert.match(service, /broadcastMessageId\?: number/);
assert.match(service, /broadcastMessageId: options\.broadcastMessageId/);
assert.match(service, /\/api\/aihr\/agent\/messages/);
@@ -4,16 +4,22 @@ import test from 'node:test';
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8');
test('公司文件在员工端展示摘要、受控原文件入口并继续复用消息ID追问', async () => {
test('公司文件在员工端优先在线阅读、按授权下载并继续复用消息ID追问', async () => {
const [service, detail] = await Promise.all([
read('../src/services/broadcast.ts'),
read('../src/pages/user/broadcast/detail.vue')
]);
assert.match(service, /interface BroadcastAttachment/);
assert.match(service, /downloadUrl: string/);
assert.match(service, /viewUrl: string/);
assert.match(service, /downloadUrl\?: string/);
assert.match(service, /allowDownload: boolean/);
assert.match(service, /getBroadcastAttachmentView/);
assert.match(detail, /公司文件/);
assert.match(detail, /attachment\.summary/);
assert.match(detail, /authenticatedFileUrl/);
assert.match(detail, /item.attachment.allowDownload && item.attachment.downloadUrl/);
assert.match(detail, /在线阅读/);
assert.match(detail, /watermark-layer/);
assert.match(detail, /saveBroadcastQuestionContext\(item\.value\.id/);
});
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
const service = fs.readFileSync(new URL('../src/services/questions.ts', import.meta.url), 'utf8');
const page = fs.readFileSync(new URL('../src/pages/user/sop/index.vue', import.meta.url), 'utf8');
test('negative feedback bridge sends only opaque source and idempotency references', () => {
assert.match(service, /questions\/from-answer-feedback/);
assert.match(service, /data:\s*\{\s*requestId: data\.requestId,\s*idempotencyKey: data\.idempotencyKey\s*\}/);
assert.doesNotMatch(service, /publishQuestionFromAnswerFeedback[\s\S]*queryText:/);
assert.doesNotMatch(service, /publishQuestionFromAnswerFeedback[\s\S]*fragmentIds:/);
});
test('human-help conversion is explicit, privacy-aware, and remains pending moderation', () => {
assert.match(page, /转人工求助/);
assert.match(page, /审核通过后其他员工可见/);
assert.match(page, /不含住户姓名、电话、房号等隐私信息/);
assert.match(page, /已提交审核/);
assert.doesNotMatch(page, /已公开|已发布到问题榜/);
});
+109
View File
@@ -0,0 +1,109 @@
import assert from 'node:assert/strict';
import { after, test } from 'node:test';
import { fileURLToPath } from 'node:url';
import { createServer } from 'vite';
const storage = new Map([
['aihr_mobile_access_token', 'expired-token'],
['aihr_mobile_client_id', 'app-client'],
['aihr_mobile_phone', '13800000000'],
['aihr_mobile_tenant_id', '000000']
]);
const redirects = [];
let uploadShouldFail = false;
globalThis.plus = {};
globalThis.getCurrentPages = () => [{
route: 'pages/user/sop/index',
$page: { fullPath: '/pages/user/sop/index?source=rc006' }
}];
globalThis.uni = {
getStorageSync: (key) => storage.get(key) || '',
setStorageSync: (key, value) => storage.set(key, value),
removeStorageSync: (key) => storage.delete(key),
reLaunch: ({ url }) => redirects.push(url),
request: ({ fail }) => {
queueMicrotask(() => fail({ errMsg: 'request:fail network' }));
return { abort: () => {} };
},
uploadFile: ({ fail, success }) => {
queueMicrotask(() => uploadShouldFail
? fail({ errMsg: 'uploadFile:fail network' })
: success({ statusCode: 200, data: '{"code":200,"data":{"text":"ok"}}' }));
return { abort: () => {} };
}
};
const vite = await createServer({
root: fileURLToPath(new URL('..', import.meta.url)),
configFile: false,
optimizeDeps: { noDiscovery: true },
server: { middlewareMode: true, hmr: false },
appType: 'custom'
});
after(async () => {
await vite.close();
delete globalThis.plus;
delete globalThis.uni;
delete globalThis.window;
delete globalThis.getCurrentPages;
});
test('ASR upload failure has a stable retryable result', async () => {
uploadShouldFail = true;
const { transcribeSpeechFile } = await vite.ssrLoadModule('/src/services/speech.ts');
await assert.rejects(
transcribeSpeechFile({ path: '/tmp/voice.mp3', name: 'voice.mp3', size: 3 }),
/语音上传失败/
);
});
test('TTS network failure falls back to device speech when supported', async () => {
const spoken = [];
globalThis.window = {
SpeechSynthesisUtterance: class {
constructor(text) { this.text = text; }
},
speechSynthesis: {
speak: (utterance) => spoken.push(utterance.text),
cancel: () => {}
}
};
const states = [];
const errors = [];
const { createSpeechPlaybackController } = await vite.ssrLoadModule('/src/services/speech.ts');
const controller = createSpeechPlaybackController(
(state) => states.push(state.status),
(message) => errors.push(message)
);
await controller.toggle('answer-1', '请先确认问题,再说明处理时限');
assert.deepEqual(spoken, ['请先确认问题,再说明处理时限']);
assert.ok(states.includes('loading'));
assert.ok(states.includes('playing'));
assert.deepEqual(errors, []);
controller.destroy();
});
test('401 clears the expired session and relaunches login while 403 does not', async () => {
const { ApiError, readPayload } = await vite.ssrLoadModule('/src/services/api.ts');
assert.throws(
() => readPayload(401, { code: 401, msg: 'login expired' }),
(error) => error instanceof ApiError && error.statusCode === 401
);
assert.equal(storage.has('aihr_mobile_access_token'), false);
assert.equal(storage.get('aihr_mobile_login_redirect'), '/pages/user/sop/index?source=rc006');
assert.deepEqual(redirects, ['/pages/auth/login/index']);
storage.set('aihr_mobile_access_token', 'new-token');
assert.throws(
() => readPayload(403, { code: 403, msg: 'forbidden' }),
(error) => error instanceof ApiError && error.statusCode === 403
);
assert.equal(storage.get('aihr_mobile_access_token'), 'new-token');
assert.deepEqual(redirects, ['/pages/auth/login/index']);
});