20260927215912

This commit is contained in:
oneao committed 2026-09-27 21:59:13 +08:00
1 parent 27e49e2e45
commit f22e67fc85
294 files changed
+29247 -3576

No files matched your search

@@ -0,0 +1,147 @@
/**
* 公式引擎单测:直接跑共享测试向量(tests/fixtures/formula-vectors.json),
* 同一份向量由 Java 端 JUnit 复用,保证前后端语义一致。
*/
import { describe, expect, it } from 'vitest'
import vectors from '../fixtures/formula-vectors.json'
import {
buildGraph,
cycleText,
evaluate,
fieldNodeId,
formulaRevision,
parse,
refsOf,
validateFormula,
validateFormulas,
} from '@/components/fms-module-common/formula'
import { rawValue } from '@/components/fms-module-common/formula/values'
import { fieldTypeMapOf } from '@/components/fms-module-common/formula'
const fieldTypes = fieldTypeMapOf(vectors.fields)
const fieldMeta = Object.fromEntries(vectors.fields.map((field) => [field.b_field, field]))
describe('公式引擎 · 求值向量', () => {
for (const testCase of vectors.evaluate) {
it(`${testCase.id}:${testCase.formula}`, () => {
const { ast, errors } = parse(testCase.formula)
expect(errors).toEqual([])
const { value, error } = evaluate(ast, { fields: testCase.values || {}, fieldTypes })
if (testCase.expectError) {
expect(error?.code).toBe(testCase.expectError)
return
}
expect(error).toBeNull()
expect(rawValue(value)).toEqual(testCase.expect)
})
}
})
describe('公式引擎 · 校验向量', () => {
for (const testCase of vectors.validate) {
it(`${testCase.id}:${testCase.formula}`, () => {
const { errors } = validateFormula(testCase.formula, {
fields: fieldMeta,
targetType: testCase.targetType,
})
expect(errors.map((issue) => issue.code)).toEqual(testCase.expect)
})
}
})
describe('公式引擎 · 依赖图', () => {
it('链式公式检测出循环引用', () => {
const config = vectors.graph.chain
const nodes = Object.entries(config.formulas).map(([field, formula]) => ({ field, formula }))
const { graph } = validateFormulas(nodes, { fields: fieldMeta })
expect(graph.hasCycle).toBe(config.expectCycle)
expect(graph.cycles.length).toBeGreaterThan(0)
expect(cycleText(graph.cycles[0]).length).toBeGreaterThan(0)
})
it('无环公式给出拓扑序,节点 ID 带作用域', () => {
const config = vectors.graph.acyclic
const nodes = Object.entries(config.formulas).map(([field, formula]) => ({ field, formula }))
const { graph, errors } = validateFormulas(nodes, { fields: fieldMeta })
expect(errors).toEqual([])
expect(graph.hasCycle).toBe(config.expectCycle)
expect(graph.order).toEqual(config.expectOrder)
})
it('依赖抽取带作用域前缀', () => {
const { ast } = parse('b_qty * b_price')
const { refs, usesRow } = refsOf(ast)
expect(usesRow).toBe(false)
expect(refs.map((ref) => ref.id)).toEqual([
fieldNodeId('main', null, 'b_qty'),
fieldNodeId('main', null, 'b_price'),
])
})
it('buildGraph 对自环给出环路', () => {
const graph = buildGraph([{ id: 'main:a', deps: ['main:a'] }])
expect(graph.hasCycle).toBe(true)
expect(graph.order).toEqual([])
})
})
describe('公式引擎 · 配置摘要', () => {
it('与共享向量一致(Java 端使用同一期望值)', () => {
expect(formulaRevision(vectors.revision.nodes)).toBe(vectors.revision.expect)
})
it('公式变化摘要变化,节点顺序不影响摘要', () => {
const nodes = vectors.revision.nodes
const reordered = [...nodes].reverse()
expect(formulaRevision(reordered)).toBe(formulaRevision(nodes))
const changed = nodes.map((node, index) =>
index === 0 ? { ...node, formula: 'b_qty * b_price + 1' } : node,
)
expect(formulaRevision(changed)).not.toBe(formulaRevision(nodes))
})
})
describe('公式引擎 · 运行模型', () => {
it('按依赖顺序重算下游字段,且只算受影响字段', async () => {
const { createFormulaModel } = await import('@/components/fms-module-common/formula')
const editSchema = {
children: [
{ type: 'row', children: [{ field: 'b_qty' }, { field: 'b_price' }] },
{
type: 'row',
children: [{ field: 'b_amount', formula: 'ROUND(b_qty * b_price, 2)', readonly: true }],
},
{
type: 'row',
children: [
{ field: 'b_rate', formula: 'b_amount / 2', readonly: true, overridable: true },
],
},
],
}
const model = createFormulaModel({ editSchema, fields: vectors.fields })
expect(model.errors).toEqual([])
expect(model.revision.startsWith('fnv1a64:')).toBe(true)
const ctx = { fields: { b_qty: 3, b_price: '12.50' } }
expect(model.affectedFields(['b_qty'])).toEqual(['b_amount', 'b_rate'])
const { values, errors } = model.recalculate(['b_qty'], ctx)
expect(errors).toEqual([])
expect(values.get('b_amount')).toBe('37.5')
expect(values.get('b_rate')).toBe('18.75')
expect(model.affectedFields(['b_name'])).toEqual([])
})
it('循环引用时模型报告错误且不静默通过', async () => {
const { createFormulaModel } = await import('@/components/fms-module-common/formula')
const editSchema = {
children: [
{ type: 'row', children: [{ field: 'b_amount', formula: 'b_price + 1', readonly: true }] },
{ type: 'row', children: [{ field: 'b_price', formula: 'b_amount + 1', readonly: true }] },
],
}
const model = createFormulaModel({ editSchema, fields: vectors.fields })
expect(model.hasCycle).toBe(true)
expect(model.errors.some((issue) => issue.code === 'E_CYCLE')).toBe(true)
})
})