import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { buildOrganizationExpenseMemoryCreatePayload, buildOrganizationExpenseMemoryRevokePayload, buildOrganizationExpenseMemoryUpdatePayload, createOrganizationExpenseMemory, fetchOrganizationExpenseMemories, normalizeOrganizationExpenseMemory, revokeOrganizationExpenseMemory, updateOrganizationExpenseMemory } from '../src/services/expenseApplicationMemories.js' const settingsModel = readFileSync(new URL('../src/utils/settingsModelHelper.js', import.meta.url), 'utf8') const settingsView = readFileSync(new URL('../src/views/SettingsView.vue', import.meta.url), 'utf8') const settingsScript = readFileSync(new URL('../src/views/scripts/SettingsView.js', import.meta.url), 'utf8') const memoryPanel = readFileSync(new URL('../src/views/ExpenseMemorySettingsPanel.vue', import.meta.url), 'utf8') test('组织记忆 payload 限制低敏枚举、有效期和必填理由', () => { assert.deepEqual(buildOrganizationExpenseMemoryCreatePayload({ scopeType: 'enterprise', scopeId: 'must-not-be-sent', value: '火车', expiresInDays: 180, reason: '统一企业基线', requestId: 'create-enterprise-1' }), { scope_type: 'enterprise', value: '火车', expires_in_days: 180, reason: '统一企业基线', request_id: 'create-enterprise-1' }) assert.deepEqual(buildOrganizationExpenseMemoryCreatePayload({ scopeType: 'department', scopeId: 'organization-unit-stable-id', value: '飞机', expiresInDays: 90, reason: '部门差旅基线', requestId: 'create-department-1' }), { scope_type: 'department', scope_id: 'organization-unit-stable-id', value: '飞机', expires_in_days: 90, reason: '部门差旅基线', request_id: 'create-department-1' }) assert.deepEqual(buildOrganizationExpenseMemoryUpdatePayload({ value: '轮船', expiresInDays: 120, expectedGeneration: 3, reason: '更新制度', requestId: 'update-memory-3' }), { value: '轮船', expires_in_days: 120, expected_generation: 3, reason: '更新制度', request_id: 'update-memory-3' }) assert.deepEqual(buildOrganizationExpenseMemoryRevokePayload({ expectedGeneration: 3, reason: '制度已停用', requestId: 'revoke-memory-3' }), { expected_generation: 3, reason: '制度已停用', request_id: 'revoke-memory-3' }) assert.throws( () => buildOrganizationExpenseMemoryCreatePayload({ scopeType: 'enterprise', value: '网约车', expiresInDays: 180, reason: '不应通过', requestId: 'invalid-value-1' }), /仅支持飞机、火车或轮船/ ) assert.throws( () => buildOrganizationExpenseMemoryCreatePayload({ scopeType: 'enterprise', value: '火车', expiresInDays: 29, reason: '不应通过', requestId: 'invalid-days-1' }), /30 到 365 天/ ) assert.throws( () => buildOrganizationExpenseMemoryCreatePayload({ scopeType: 'enterprise', value: '火车', expiresInDays: 180, reason: '', requestId: 'invalid-reason-1' }), /操作理由/ ) }) test('组织记忆 service 使用管理员接口并保留作用域和版本解释', async () => { const requests = [] const originalFetch = globalThis.fetch const responseMemory = { id: 'memory/enterprise 1', field_key: 'transport_mode', value: '火车', status: 'active', scope_type: 'enterprise', scope_id: 'tenant-1', scope_label: '企业统一规则', source: 'enterprise_policy_memory', origin_type: 'admin_managed', generation: 2, policy_version: 'expense_application_transport_org_memory.v1', expires_at: '2026-12-31T00:00:00Z', managed_by: 'private-admin-identifier', can_revoke: true } globalThis.fetch = async (url, options = {}) => { requests.push({ url: String(url), options }) return { ok: true, status: 200, async json() { return requests.length === 1 ? { items: [responseMemory] } : responseMemory } } } try { const items = await fetchOrganizationExpenseMemories() await createOrganizationExpenseMemory({ scopeType: 'enterprise', value: '火车', expiresInDays: 180, reason: '创建企业记忆', requestId: 'create-memory-1' }) await updateOrganizationExpenseMemory('memory/enterprise 1', { value: '轮船', expiresInDays: 120, expectedGeneration: 2, reason: '更新记忆', requestId: 'update-memory-1' }) await revokeOrganizationExpenseMemory('memory/enterprise 1', { expectedGeneration: 2, reason: '撤销记忆', requestId: 'revoke-memory-1' }) assert.equal(items[0].scopeType, 'enterprise') assert.equal(items[0].generation, 2) assert.equal(items[0].sourceLabel, '管理员显式维护') assert.equal('managedBy' in items[0], false) } finally { globalThis.fetch = originalFetch } assert.equal(requests[0].url, '/api/v1/expense-application-memories/organization') assert.equal(requests[1].options.method, 'POST') assert.equal( requests[2].url, '/api/v1/expense-application-memories/organization/memory%2Fenterprise%201' ) assert.equal(requests[2].options.method, 'PUT') assert.equal( requests[3].url, '/api/v1/expense-application-memories/organization/memory%2Fenterprise%201/revoke' ) assert.equal(requests[3].options.method, 'POST') }) test('组织记忆规范化不将管理员标识带入展示模型', () => { const memory = normalizeOrganizationExpenseMemory({ id: 'memory-department-1', scope_type: 'department', scope_id: 'organization-unit-1', scope_label: '部门规则(财务部)', status: 'active', value: '飞机', origin_type: 'admin_managed', managed_by: 'sensitive-user-id' }) assert.equal(memory.scopeId, 'organization-unit-1') assert.equal(memory.scopeLabel, '部门规则(财务部)') assert.equal(memory.sourceLabel, '管理员显式维护') assert.equal('managedBy' in memory, false) }) test('设置页接入无顶部保存按钮的 AI 记忆独立面板', () => { assert.match(settingsModel, /id:\s*'aiMemory'[\s\S]*label:\s*'AI 记忆'[\s\S]*actionLabel:\s*''/) assert.match(settingsModel, /aiMemory:\s*true/) assert.match(settingsScript, /import ExpenseMemorySettingsPanel/) assert.match(settingsScript, /ExpenseMemorySettingsPanel,/) assert.match(settingsView, /activeSection === 'aiMemory'[\s\S]*/) assert.match(memoryPanel, /fetchEmployeeMeta\(\)/) assert.match(memoryPanel, /item\?\.id && item\?\.unitType === 'department'/) assert.match(memoryPanel, /value:\s*String\(item\.id\)/) assert.match(memoryPanel, /飞机[、\s\S]*火车[、\s\S]*轮船/) assert.match(memoryPanel, /min="30" max="365"/) assert.match(memoryPanel, /第 \{\{ memory\.generation \}\} 版/) assert.match(memoryPanel, /memory\.sourceLabel/) assert.match(memoryPanel, /organization-memory-revoke:/) assert.match(memoryPanel, /requestId:\s*revokeRequestId\.value/) assert.doesNotMatch(memoryPanel, /managedBy|managed_by|managementReason|management_reason/) })