Files
zszq-trs/YLErpWeb/fe-tests/closePercentContract.test.js
hjhan e188ba5038 test(swap): 新增 ClosePercent 口径A产品契约测试,防止平仓比例被改为占剩余口径
新增前后端回归测试,锁定产品需求:ClosePercent 永远是占期初名义本金的比例。后端 CPC_001~010 装配测试验证 SwapDealService 真实执行 ToRemainingClosePercent/ToOriginalClosePercent 转换;前端 16 项接线测试验证 unwindSwapTrade.js 使用 NotionalValue 作为分母且 postData 携带 notionalValue 字段。测试背景:c9071a4e 曾将 ClosePercent 改为占剩余口径导致回归但现有测试全通过,本测试通过源码装配与契约验证弥补该缺口。
2026-07-27 13:18:32 +08:00

169 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* closePercentContract.test.js — 平仓比例口径A(占期初名义本金)前端接线守卫
* ============================================================================
* 产品需求(不可违背):ClosePercent 永远是"占期初名义本金(NotionalValue)的比例",即口径A。
*
* 本文件是"接线测试"wiring test):读 unwindSwapTrade.js 源码文本,断言关键逻辑
* 仍然使用口径A的公式。如果有人把公式改成口径B(如 c9071a4e 曾犯的错误),
* 对应断言会立即变红。
*
* 与 swapCalc.test.js 的区别:
* swapCalc.test.js 测 SwapCalc 纯函数本身正确性(零件级)
* 本文件测 unwindSwapTrade.js 确实在调用这些函数/使用正确公式(装配级)
* ============================================================================
*/
const fs = require('fs');
const path = require('path');
const unwindSrc = fs.readFileSync(
path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'unwindSwapTrade.js'),
'utf8'
);
describe('口径A产品契约:unwindSwapTrade.js 接线守卫', () => {
// ====================================================================
// 契约1oriClosePercent 必须用 PosiNotionalValue / NotionalValue 计算
// 不能硬编码为 1c9071a4e 的错误)
// ====================================================================
describe('oriClosePercent 必须为剩余/期初', () => {
test('源码中 oriClosePercent 必须包含 PosiNotionalValue / NotionalValue 公式', () => {
// 正确代码:this.oriClosePercent = ... PosiNotionalValue / ... NotionalValue
expect(unwindSrc).toMatch(/oriClosePercent.*PosiNotionalValue.*\/.*NotionalValue/s);
});
test('源码中 oriClosePercent 不能被硬编码为 1', () => {
// c9071a4e 的错误:this.deal.ClosePercent = 1 (直接覆盖)
// 检查 initDeal 中不存在 oriClosePercent = 1 的硬编码
const initDealSection = unwindSrc.match(/initDeal\(\)[\s\S]*?\},/);
expect(initDealSection).toBeTruthy();
// 不应出现 oriClosePercent = 1 或 ClosePercent = 1 的硬编码
//CloseMethod===1 时设置 ClosePercent=1 是允许的,但 oriClosePercent 不应被设为1
expect(initDealSection[0]).not.toMatch(/oriClosePercent\s*=\s*1\b/);
});
});
// ====================================================================
// 契约2calcCloseQtyByPercent 必须调用 SwapCalc.calcCloseQtyByOriginalPercent
// 不能直接 PositionQty × ClosePercentc9071a4e 的错误)
// ====================================================================
describe('calcCloseQtyByPercent 必须调用 SwapCalc', () => {
test('源码中 calcCloseQtyByPercent 必须调用 SwapCalc.calcCloseQtyByOriginalPercent', () => {
expect(unwindSrc).toContain('SwapCalc.calcCloseQtyByOriginalPercent');
});
test('changeCloseMethod 的部分平仓分支必须调用 calcCloseQtyByPercent', () => {
// 正确代码:this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
expect(unwindSrc).toMatch(/calcCloseQtyByPercent\s*\(this\.deal\.ClosePercent\)/);
});
test('changeClosePercent 必须调用 calcCloseQtyByPercent', () => {
expect(unwindSrc).toMatch(/this\.deal\.CloseQty\s*=\s*this\.calcCloseQtyByPercent/);
});
test('changeCloseNotionalValue 必须调用 calcCloseQtyByPercent', () => {
expect(unwindSrc).toMatch(/this\.deal\.CloseQty\s*=\s*this\.calcCloseQtyByPercent/);
});
});
// ====================================================================
// 契约3CloseNotionalValue 必须用 NotionalValue 做乘数/分母(口径A
// 不能用 PosiNotionalValuec9071a4e 的错误)
// ====================================================================
describe('CloseNotionalValue 必须基于 NotionalValue(口径A', () => {
test('changeCloseQty: CloseNotionalValue = ClosePercent × NotionalValue', () => {
// 正确:ClosePercent × parseFloat(this.deal.NotionalValue)
// 错误:ClosePercent × parseFloat(this.deal.PosiNotionalValue)
const changeCloseQtySection = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/);
expect(changeCloseQtySection).toBeTruthy();
expect(changeCloseQtySection[0]).toMatch(/CloseNotionalValue.*NotionalValue/);
expect(changeCloseQtySection[0]).not.toMatch(/CloseNotionalValue.*PosiNotionalValue/);
});
test('changeClosePercent: CloseNotionalValue = ClosePercent × NotionalValue', () => {
const changeClosePercentSection = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/);
expect(changeClosePercentSection).toBeTruthy();
expect(changeClosePercentSection[0]).toMatch(/CloseNotionalValue.*NotionalValue/);
expect(changeClosePercentSection[0]).not.toMatch(/CloseNotionalValue.*PosiNotionalValue/);
});
});
// ====================================================================
// 契约4changeCloseQty 的 ClosePercent 必须乘以 oriClosePercent(口径A→B→A 转换)
// 不能直接 CloseQty / PositionQtyc9071a4e 的错误)
// ====================================================================
describe('changeCloseQty 的 ClosePercent 必须乘以 oriClosePercent', () => {
test('ClosePercent = (CloseQty/PositionQty) × oriClosePercent', () => {
// 正确:× ori(把占剩余比例转回占期初口径)
// 错误:不乘 ori(直接用占剩余比例作为 ClosePercent
const changeCloseQtySection = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/);
expect(changeCloseQtySection).toBeTruthy();
expect(changeCloseQtySection[0]).toMatch(/oriClosePercent/);
expect(changeCloseQtySection[0]).toMatch(/\*\s*ori/);
});
test('changeCloseNotionalValue 的 ClosePercent 必须除以 NotionalValue', () => {
// 正确:ClosePercent = CloseNotionalValue / NotionalValue
// 错误:ClosePercent = CloseNotionalValue / PosiNotionalValue
const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
expect(changeCloseNotionalSection).toBeTruthy();
expect(changeCloseNotionalSection[0]).toMatch(/ClosePercent.*NotionalValue/);
expect(changeCloseNotionalSection[0]).not.toMatch(/ClosePercent.*PosiNotionalValue/);
});
});
// ====================================================================
// 契约5getInterestList 必须传 notionalValue 和 posiNotionalValue 给后端
// 后端 GetUnwindInterestList 需要这两个值做 A→B 转换
// 如果删掉(c9071a4e 的错误),后端不转换,利息用错口径计算
// ====================================================================
describe('getInterestList 必须传 notionalValue/posiNotionalValue', () => {
test('postData 必须包含 notionalValue', () => {
expect(unwindSrc).toMatch(/notionalValue:\s*thisObj\.deal\.NotionalValue/);
});
test('postData 必须包含 posiNotionalValue', () => {
expect(unwindSrc).toMatch(/posiNotionalValue:\s*thisObj\.deal\.PosiNotionalValue/);
});
test('getInterestList 的 postData 不能只有 closePercent 而缺少 notionalValue', () => {
// 精确匹配 getInterestList 方法定义(以 getInterestList() { 开头,到 main.post 结束)
// 匹配模式:方法名+参数列表+花括号开始,一直到包含 main.post 的 postData 定义
const methodMatch = unwindSrc.match(
/getInterestList\(\)\s*\{[\s\S]*?var\s+postData\s*=\s*\{[^}]*\}/
);
expect(methodMatch).toBeTruthy();
const postData = methodMatch[0];
expect(postData).toContain('notionalValue');
expect(postData).toContain('posiNotionalValue');
});
});
// ====================================================================
// 契约6CloseMethod 赋值对象必须是 deal(不是 floatPosition
// c9071a4e 在 changeClosePercent 中误赋值到 floatPosition.CloseMethod
// ====================================================================
describe('CloseMethod 必须赋值给 deal', () => {
test('changeClosePercent 的 CloseMethod 必须赋值给 this.deal', () => {
const changeClosePercentSection = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/);
expect(changeClosePercentSection).toBeTruthy();
expect(changeClosePercentSection[0]).toMatch(/this\.deal\.CloseMethod\s*=/);
expect(changeClosePercentSection[0]).not.toMatch(/this\.floatPosition\.CloseMethod\s*=/);
});
test('changeCloseNotionalValue 的 CloseMethod 必须赋值给 this.deal', () => {
const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
expect(changeCloseNotionalSection).toBeTruthy();
expect(changeCloseNotionalSection[0]).toMatch(/this\.deal\.CloseMethod\s*=/);
expect(changeCloseNotionalSection[0]).not.toMatch(/this\.floatPosition\.CloseMethod\s*=/);
});
test('changeCloseNotionalValue 必须设置 CloseMethod(不能删除)', () => {
// c9071a4e 完全删除了 changeCloseNotionalValue 中的 CloseMethod 判断
const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
expect(changeCloseNotionalSection).toBeTruthy();
expect(changeCloseNotionalSection[0]).toMatch(/CloseMethod/);
});
});
});