Files
X-Financial/web/tests/settings-expense-memory-section.test.mjs

191 lines
7.0 KiB
JavaScript
Raw Permalink Normal View History

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]*<ExpenseMemorySettingsPanel \/>/)
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/)
})