Archived
资料文件真产出来了;E04/I01 补到验收线,顺手填了几个「墙拦住却不给路」的坑
**资料文件全链**(用户:审核资料这一步要真产出 word/excel/md/html,前端能渲、点资料名能开)
接手时这一步一个文件都产不出来 —— S3 剧本白纸黑字写着「file_ref 本 MVP 阶段可留空」。
文件是台账那一行的投影,不另存一份:
· 声明 `file:{titleColumn,bodyColumn,formatColumn}`,它同时是授权面(没声明就点不开)
· 现造真字节 —— docx/xlsx 是自己打的 OOXML 包(zip-write:store+CRC32,零依赖)
· 读回来渲 —— 解的就是造出来那份字节,渲得出来才证明文件是真的
· 件上 `data-file-of` → 工作区二级界面(Word 纸面/Excel 网格/渲好的 md/沙箱 html)+ 下载原件
放行落地前把每一份冻进 `sys.task_asset_file`(迁移 0029):归档件点开的必须是签字那一刻
那一份,而资料落在主数据上、试跑单不落正式表 —— 不存这一份,收工后一个都点不开。
工作成果里「产出的资料」自成一摞。Markdown 富文本渲染顺带销账(仍零依赖)。
**Harness**
· 画面按「这一拍办完之后」在办的那一步挑 —— 从前拿拍首的局面决定拍尾的事:
收掉 S4、发出 S5 的卡,而屏上还摆着 S4 的只读件,人无处可填(墙要配路第四次现形)
· 验收「重做」真能退回重做,且打回后台账没动就不许再提交(软硬两段,不锁死单子)
· 立监听的授权面是清单声明;发卡的授权面是调人预算,不是计划项
· 读工具同参重调有兜底;空转写身后还有行动就不废串;finish 的 doneWhen 按写完后的草稿算
**界面**
· 翻件与翻页各归各位(从前二选一,多卡多页时页翻不动);没得翻写「—」不写 0/0
· 工作成果详情能翻份;表宽超出能横滑(E04/I01 抄版式时把 overflow-x 抄丢了)
· 查看面灰底滚过一屏变白 —— 两条独立的路都治了(沙箱内不同色 / 弹性子项不长个)
· 测试台「取不到」不再当成「还活着」
**活动**
E04 关系覆盖、I01 品牌内容各自补到验收线:判据全读基座数、判据指向的东西摆上卡面、
样例满载且名字 ≤14 字;提交前要裁的判据搬进说明书(模型会压拍,压过之后只有说明书还在)。
验收:e2e i01 4/4、e04 3/3 全部 released,资料在办中渲得出、放行后存了档点得开;
门禁 typecheck/lint/depcruise/1628 测全绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 产出的资料**存档** —— 放行那一刻冻住的那一份(2026-08-23 用户:
|
||||
* 「检查当前产出的附件是否存档,在工作成果里也需要有产出的附件,点击后可以预览」)。
|
||||
*
|
||||
* 存档之前的实情:文件是台账那一行现造的,而资料常落在**主数据**上,
|
||||
* 试跑单放行时主数据只留在活动缓存、不落正式表 —— 于是**放行之后一份都点不开**
|
||||
* (实测 404)。工作成果恰恰是收工之后才去看的地方,不存这一份,那一屏就是空的。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TaskId } from '@contracts';
|
||||
import { archiveAssetFiles, keyText } from '../asset-archive.ts';
|
||||
|
||||
const TASK = 't-1' as TaskId;
|
||||
|
||||
/** 记下发生过哪些写 —— SQL 不求值,只看「几笔、带了什么」 */
|
||||
function fakeDb() {
|
||||
const writes: string[] = [];
|
||||
return {
|
||||
writes,
|
||||
execute: async (query: unknown) => {
|
||||
// drizzle 的 sql`` 片段:把它的字面量与参数拼成一句好断言的字符串
|
||||
const q = query as { queryChunks?: unknown[] };
|
||||
writes.push(JSON.stringify(q.queryChunks ?? query).slice(0, 4000));
|
||||
return { rows: [] };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ids = (() => {
|
||||
let n = 0;
|
||||
return {
|
||||
next: () => {
|
||||
n += 1;
|
||||
return `id-${n}`;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
const ASSET = {
|
||||
domain: 'content',
|
||||
object: 'asset',
|
||||
fields: [{ name: 'content_id', primaryKey: true }, { name: 'title' }, { name: 'body_md' }, { name: 'file_format' }],
|
||||
file: { titleColumn: 'title', bodyColumn: 'body_md', formatColumn: 'file_format' },
|
||||
};
|
||||
|
||||
describe('keyText —— 主键拼法必须稳定', () => {
|
||||
it('按列名排序拼,写进去与取出来才对得上', () => {
|
||||
expect(keyText({ b: '2', a: '1' })).toBe('a=1,b=2');
|
||||
expect(keyText({ a: '1', b: '2' })).toBe('a=1,b=2');
|
||||
});
|
||||
|
||||
it('单列主键就是一句 `列=值` —— 与件上 data-file 那一串同形状', () => {
|
||||
expect(keyText({ content_id: 'CT-2026-0001' })).toBe('content_id=CT-2026-0001');
|
||||
});
|
||||
});
|
||||
|
||||
describe('archiveAssetFiles —— 存哪些、不存哪些', () => {
|
||||
it('一行一份,存下来的是真字节(存几份就写几笔)', async () => {
|
||||
const db = fakeDb();
|
||||
const saved = await archiveAssetFiles({
|
||||
db,
|
||||
taskId: TASK,
|
||||
ids,
|
||||
declarations: [ASSET],
|
||||
rowsOfAddress: async () => [
|
||||
{ content_id: 'CT-1', title: '白皮书', body_md: '# 白皮书\n\n正文', file_format: 'docx' },
|
||||
{ content_id: 'CT-2', title: '对照表', body_md: '| a | b |\n| - | - |\n| 1 | 2 |', file_format: 'xlsx' },
|
||||
],
|
||||
});
|
||||
expect(saved).toBe(2);
|
||||
expect(db.writes).toHaveLength(2);
|
||||
expect(db.writes.join()).toContain('白皮书');
|
||||
});
|
||||
|
||||
it('**没正文的行不存** —— 一份空壳存进去,人点开只会看见一张白纸', async () => {
|
||||
const db = fakeDb();
|
||||
const saved = await archiveAssetFiles({
|
||||
db,
|
||||
taskId: TASK,
|
||||
ids,
|
||||
declarations: [ASSET],
|
||||
rowsOfAddress: async () => [{ content_id: 'CT-1', title: '还没写', body_md: ' ', file_format: 'md' }],
|
||||
});
|
||||
expect(saved).toBe(0);
|
||||
expect(db.writes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('**主键不全的行不存** —— 存了也按不回那一行,取不回来', async () => {
|
||||
const db = fakeDb();
|
||||
const saved = await archiveAssetFiles({
|
||||
db,
|
||||
taskId: TASK,
|
||||
ids,
|
||||
declarations: [ASSET],
|
||||
rowsOfAddress: async () => [{ content_id: '', title: 'T', body_md: '正文', file_format: 'md' }],
|
||||
});
|
||||
expect(saved).toBe(0);
|
||||
});
|
||||
|
||||
it('没声明 `file` 段的表整张跳过 —— 存档也认那道授权面', async () => {
|
||||
const db = fakeDb();
|
||||
const saved = await archiveAssetFiles({
|
||||
db,
|
||||
taskId: TASK,
|
||||
ids,
|
||||
declarations: [{ ...ASSET, file: undefined }],
|
||||
rowsOfAddress: async () => [{ content_id: 'CT-1', title: 'T', body_md: '正文', file_format: 'md' }],
|
||||
});
|
||||
expect(saved).toBe(0);
|
||||
});
|
||||
|
||||
it('格式列填了个不认识的值:退 md,不整份丢掉', async () => {
|
||||
const db = fakeDb();
|
||||
const saved = await archiveAssetFiles({
|
||||
db,
|
||||
taskId: TASK,
|
||||
ids,
|
||||
declarations: [ASSET],
|
||||
rowsOfAddress: async () => [{ content_id: 'CT-1', title: 'T', body_md: '正文', file_format: 'pdf' }],
|
||||
});
|
||||
expect(saved).toBe(1);
|
||||
expect(db.writes.join()).toContain('"md"');
|
||||
});
|
||||
|
||||
it('大多数活动不产资料 —— 一张带 file 段的表都没有时返回 0,不报错', async () => {
|
||||
const db = fakeDb();
|
||||
expect(
|
||||
await archiveAssetFiles({ db, taskId: TASK, ids, declarations: [], rowsOfAddress: async () => [] }),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 资料文件现造 + 读回来 —— 两头合起来自证:**造出来的是真文件**。
|
||||
*
|
||||
* 断言的重点不是「函数返回了字节」,而是:
|
||||
* · docx / xlsx 是合法 ZIP,里面那几份 XML 一份不缺(Word / Excel 认的就是它们);
|
||||
* · 读回来的内容与写进去的一模一样 —— 渲得出来,才谈得上「前端能渲染」;
|
||||
* · 模型写的字全程转义 —— 正文里带 `<script>` 也只是几个字,不是标签。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
columnName,
|
||||
composeFile,
|
||||
inlineHtml,
|
||||
markdownBody,
|
||||
markdownSheetRows,
|
||||
parseMarkdown,
|
||||
safeFileName,
|
||||
} from '../file-compose.ts';
|
||||
import { docxBlocks, toFileDoc, xlsxRows } from '../file-doc.ts';
|
||||
import { readZip } from '../upload-extract.ts';
|
||||
|
||||
const MD = [
|
||||
'# 车规级 MCU 供货能力说明',
|
||||
'',
|
||||
'本文面向 **Itelma** 采购与供应链团队,说明我方在车规 MCU 上的供货保障。',
|
||||
'',
|
||||
'## 一、产能与库存',
|
||||
'',
|
||||
'- 常备安全库存:12 周',
|
||||
'- 二供已导入,切换周期 4 周',
|
||||
'',
|
||||
'## 二、价格与交期',
|
||||
'',
|
||||
'| 型号 | 交期(周) | 年用量 |',
|
||||
'| --- | --- | --- |',
|
||||
'| NE555P | 8 | 30000 |',
|
||||
'| DAG2400 | 12 | 25000 |',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'结论:可承诺 `98%` 的按期交付率。',
|
||||
].join('\n');
|
||||
|
||||
describe('parseMarkdown —— 认得出这几样,认不出的不吞字', () => {
|
||||
it('标题 / 段落 / 列表 / 表格 / 分隔线各归各位', () => {
|
||||
const blocks = parseMarkdown(MD);
|
||||
expect(blocks.map((b) => b.t)).toEqual(['h1', 'p', 'h2', 'ul', 'h2', 'table', 'hr', 'p']);
|
||||
});
|
||||
|
||||
it('表格的分隔行只说对齐,不算一行数据', () => {
|
||||
const table = parseMarkdown(MD).find((b) => b.t === 'table');
|
||||
expect(table).toBeDefined();
|
||||
if (table?.t !== 'table') throw new Error('不是表格块');
|
||||
expect(table.rows).toEqual([
|
||||
['型号', '交期(周)', '年用量'],
|
||||
['NE555P', '8', '30000'],
|
||||
['DAG2400', '12', '25000'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('认不出的行当段落原样留着 —— 宁可朴素也不吞内容', () => {
|
||||
const blocks = parseMarkdown('> 引用还不认\n\n普通段落');
|
||||
expect(blocks).toEqual([
|
||||
{ t: 'p', text: '> 引用还不认' },
|
||||
{ t: 'p', text: '普通段落' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('转义 —— 模型写的字进不了标签位', () => {
|
||||
it('正文里的 <script> 只是几个字', () => {
|
||||
const html = markdownBody('这里有 <script>alert(1)</script> 一段');
|
||||
expect(html).toContain('<script>');
|
||||
expect(html).not.toContain('<script>');
|
||||
});
|
||||
|
||||
it('先转义再上行内标记 —— 顺序反了就是把字当标签执行', () => {
|
||||
expect(inlineHtml('**粗** 与 `码` 与 <b>假标签</b>')).toBe(
|
||||
'<strong>粗</strong> 与 <code>码</code> 与 <b>假标签</b>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('md —— 原文即文件', () => {
|
||||
it('正文没有标题行就补一条:文件得自报家门', () => {
|
||||
const f = composeFile({ title: '供货能力说明', markdown: '正文直接开始', format: 'md' });
|
||||
expect(new TextDecoder().decode(f.bytes)).toBe('# 供货能力说明\n\n正文直接开始');
|
||||
expect(f.fileName).toBe('供货能力说明.md');
|
||||
});
|
||||
|
||||
it('正文自带标题就不再补', () => {
|
||||
const f = composeFile({ title: 'T', markdown: MD, format: 'md' });
|
||||
expect(new TextDecoder().decode(f.bytes).startsWith('# 车规级 MCU')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('html —— 整份文件,离线打开也是这个样子', () => {
|
||||
const f = composeFile({ title: '供货能力说明', markdown: MD, format: 'html' });
|
||||
const text = new TextDecoder().decode(f.bytes);
|
||||
|
||||
it('是一份完整文档:doctype + 自带样式 + 纸面容器', () => {
|
||||
expect(text.startsWith('<!doctype html>')).toBe(true);
|
||||
expect(text).toContain('<style>');
|
||||
expect(text).toContain('class="doc-paper"');
|
||||
});
|
||||
|
||||
it('表格是真表格,不是一段文字', () => {
|
||||
expect(text).toContain('<th>型号</th>');
|
||||
expect(text).toContain('<td>NE555P</td>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('docx —— 真 OOXML 包', () => {
|
||||
const f = composeFile({ title: '供货能力说明', markdown: MD, format: 'docx' });
|
||||
const zip = readZip(f.bytes);
|
||||
|
||||
it('该有的几份 XML 一份不缺(少一份 Word 就报文件损坏)', () => {
|
||||
expect([...zip.keys()].sort()).toEqual(
|
||||
['[Content_Types].xml', '_rels/.rels', 'word/_rels/document.xml.rels', 'word/document.xml', 'word/styles.xml'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('MIME 与扩展名是 Word 那一套', () => {
|
||||
expect(f.fileName).toBe('供货能力说明.docx');
|
||||
expect(f.mime).toContain('wordprocessingml.document');
|
||||
});
|
||||
|
||||
it('读回来:标题、正文、列表、表格都在,层级没丢', () => {
|
||||
const xml = new TextDecoder().decode(zip.get('word/document.xml') ?? new Uint8Array());
|
||||
const blocks = docxBlocks(xml);
|
||||
expect(blocks[0]).toEqual({ t: 'h1', text: '供货能力说明' });
|
||||
expect(blocks.some((b) => b.t === 'h2' && b.text === '一、产能与库存')).toBe(true);
|
||||
const list = blocks.find((b) => b.t === 'ul');
|
||||
expect(list?.t === 'ul' ? list.items : []).toEqual(['常备安全库存:12 周', '二供已导入,切换周期 4 周']);
|
||||
// 表格按行摊成 p:列用「 | 」连
|
||||
expect(blocks.some((b) => b.t === 'p' && b.text === 'NE555P | 8 | 30000')).toBe(true);
|
||||
});
|
||||
|
||||
it('行内标记不带进 Word —— 留着 ** 反而像乱码', () => {
|
||||
const xml = new TextDecoder().decode(zip.get('word/document.xml') ?? new Uint8Array());
|
||||
expect(xml).toContain('Itelma');
|
||||
expect(xml).not.toContain('**Itelma**');
|
||||
});
|
||||
});
|
||||
|
||||
describe('xlsx —— 真 OOXML 包,表格原样成行', () => {
|
||||
const f = composeFile({ title: '交期对照表', markdown: MD, format: 'xlsx' });
|
||||
|
||||
it('该有的几份 XML 一份不缺', () => {
|
||||
expect([...readZip(f.bytes).keys()].sort()).toEqual(
|
||||
['[Content_Types].xml', '_rels/.rels', 'xl/_rels/workbook.xml.rels', 'xl/workbook.xml', 'xl/worksheets/sheet1.xml'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('读回来:表格三行在,首行是表头', () => {
|
||||
const sheet = xlsxRows(f.bytes);
|
||||
expect(sheet?.sheet).toBe('交期对照表');
|
||||
const rows = sheet?.rows ?? [];
|
||||
expect(rows[0]).toEqual(['交期对照表']);
|
||||
expect(rows).toContainEqual(['型号', '交期(周)', '年用量']);
|
||||
expect(rows).toContainEqual(['NE555P', '8', '30000']);
|
||||
});
|
||||
|
||||
it('正文不丢:列表与段落各占一行', () => {
|
||||
const rows = markdownSheetRows('T', MD);
|
||||
expect(rows.some((r) => r[0] === '• 常备安全库存:12 周')).toBe(true);
|
||||
expect(rows.some((r) => (r[0] ?? '').startsWith('结论:可承诺'))).toBe(true);
|
||||
});
|
||||
|
||||
it('列号:0→A、25→Z、26→AA', () => {
|
||||
expect([columnName(0), columnName(25), columnName(26)]).toEqual(['A', 'Z', 'AA']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toFileDoc —— 前端拿到的就是这份文件读回来的样子', () => {
|
||||
const one = (format: 'md' | 'html' | 'docx' | 'xlsx') => {
|
||||
const f = composeFile({ title: '供货能力说明', markdown: MD, format });
|
||||
return toFileDoc({ name: f.fileName, title: '供货能力说明', format, bytes: f.bytes });
|
||||
};
|
||||
|
||||
it('四种格式各出各的查看形态', () => {
|
||||
expect(one('md')?.kind).toBe('markdown');
|
||||
expect(one('html')?.kind).toBe('html');
|
||||
expect(one('docx')?.kind).toBe('word');
|
||||
expect(one('xlsx')?.kind).toBe('excel');
|
||||
});
|
||||
|
||||
it('markdown 同时给原文与渲好的那一份(下载拿原文,屏上摆渲染版)', () => {
|
||||
const doc = one('md');
|
||||
if (doc?.kind !== 'markdown') throw new Error('不是 markdown');
|
||||
expect(doc.md).toContain('| NE555P | 8 | 30000 |');
|
||||
expect(doc.html).toContain('<td>NE555P</td>');
|
||||
});
|
||||
|
||||
it('**读不出来就返回 undefined** —— 不摆一个空壳假装渲染过', () => {
|
||||
const junk = new TextEncoder().encode('这不是 ZIP');
|
||||
expect(toFileDoc({ name: 'x.docx', title: 'x', format: 'docx', bytes: junk })).toBeUndefined();
|
||||
expect(toFileDoc({ name: 'x.xlsx', title: 'x', format: 'xlsx', bytes: junk })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('文件名', () => {
|
||||
it('路径分隔符这类字符换成下划线 —— 别让标题决定文件落在哪', () => {
|
||||
expect(safeFileName('a/b:c*d?"e<f>g|h', 'md')).toBe('a_b_c_d__e_f_g_h.md');
|
||||
});
|
||||
|
||||
it('标题整个是空白就退一个兜底名,不出「.docx」这种没名字的文件', () => {
|
||||
expect(safeFileName(' ', 'docx')).toBe('资料.docx');
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,7 @@
|
||||
* 抄一份的下场踩过 —— 生产改了、测没跟着改,测全绿而线上照旧。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { gateAddressesOf, gateOf, rowChanged, writesOf } from '../routes/desk.ts';
|
||||
import { findKnownRow, gateAddressesOf, gateOf, parseRowKey, resolveRowKey, rowChanged, writesOf } from '../routes/desk.ts';
|
||||
import { missingReadings } from '../wiring/engines.ts';
|
||||
|
||||
/** A1 受理记录的声明(裁到这一段用得着的几列):PN 与年用量必填 */
|
||||
@@ -68,6 +68,85 @@ describe('件上按保存:按地址收', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* **人在件上加的那一行,主键得凑得齐** —— 凑不齐就被静默丢掉,而屏上那一行还在
|
||||
* (界面说加上了,台账里没有 —— 最坏的一类:它不报错)。
|
||||
*
|
||||
* 三类主键各有各的凑法(2026-08-23,审 I01 需求清单时逮到的):
|
||||
* · **件上没摆的那一列**(如一单一个的案号):本单现有行全表一致 → 就是它;
|
||||
* · **件上摆了的**:人填了就用他填的;
|
||||
* · **序号型(整数主键)**:件上摆着,可「加一行」那一下所有格子都被清空
|
||||
* (`ViewContainer.addRow` 如此)——各行序号本就互不相同,「全表一致」永远取不出,
|
||||
* 于是整行被丢。序号的语义就是「本轮内递增」,该由基座续:现有最大值 + 1。
|
||||
* I01 的需求清单主键正是 `round_ref + req_seq`,不补这一手,人加的需求就消失。
|
||||
*/
|
||||
describe('件上加行:主键凑得齐才落得下', () => {
|
||||
const KEYS = ['round_ref', 'req_seq'];
|
||||
const TYPES = { round_ref: 'text', req_seq: 'integer' };
|
||||
const DRAFTS = [
|
||||
{ key: { round_ref: 'CTB-1', req_seq: 1 } },
|
||||
{ key: { round_ref: 'CTB-2', req_seq: 2 } },
|
||||
];
|
||||
const SAME_ROUND = [
|
||||
{ key: { round_ref: 'CTB-1', req_seq: 1 } },
|
||||
{ key: { round_ref: 'CTB-1', req_seq: 2 } },
|
||||
];
|
||||
|
||||
it('老行:件上摆了序号,案号从全表一致里补', () => {
|
||||
expect(resolveRowKey({ keys: KEYS, cells: { req_seq: '2' }, drafts: SAME_ROUND, types: TYPES })).toEqual({
|
||||
round_ref: 'CTB-1',
|
||||
req_seq: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('**新增行:序号空着 → 基座续号(现有最大 + 1)**,不再静默丢掉', () => {
|
||||
expect(resolveRowKey({ keys: KEYS, cells: { content_type: '案例集' }, drafts: SAME_ROUND, types: TYPES })).toEqual({
|
||||
round_ref: 'CTB-1',
|
||||
req_seq: '3',
|
||||
});
|
||||
});
|
||||
|
||||
it('空表上加第一行:序号从 1 起 —— 但案号取不出就还是不猜', () => {
|
||||
expect(resolveRowKey({ keys: KEYS, cells: { content_type: '公司介绍' }, drafts: [], types: TYPES })).toBeUndefined();
|
||||
expect(
|
||||
resolveRowKey({ keys: KEYS, cells: { round_ref: 'CTB-9', content_type: '公司介绍' }, drafts: [], types: TYPES }),
|
||||
).toEqual({ round_ref: 'CTB-9', req_seq: '1' });
|
||||
});
|
||||
|
||||
/*
|
||||
* **只有一行时,那一行的序号不是「全表一致的常量」**(2026-08-23 浏览器实测逮到:
|
||||
* 人在只有一行的清单上加一行,新行拿到 req_seq=1,**当场把原来那一行覆盖掉** ——
|
||||
* 屏上两行、库里一行,而且内容还成了新的那份)。序号逐行不同是它的定义,
|
||||
* 一致只是行数太少的假象。
|
||||
*/
|
||||
it('**表里只有一行:加的新行续成 2,不覆盖原来那一行**', () => {
|
||||
const one = [{ key: { round_ref: 'CTB-1', req_seq: 1 } }];
|
||||
expect(resolveRowKey({ keys: KEYS, cells: { content_type: '销售工具包' }, drafts: one, types: TYPES })).toEqual({
|
||||
round_ref: 'CTB-1',
|
||||
req_seq: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('老行照旧认得出来 —— 件上摆了序号(主键列必挂 data-col,它是行的钥匙)', () => {
|
||||
const one = [{ key: { round_ref: 'CTB-1', req_seq: 1 } }];
|
||||
expect(resolveRowKey({ keys: KEYS, cells: { req_seq: '1', scene: '展会' }, drafts: one, types: TYPES })).toEqual({
|
||||
round_ref: 'CTB-1',
|
||||
req_seq: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('案号本身取不出(一单跨了两个号)→ 不猜,如实跳过', () => {
|
||||
expect(resolveRowKey({ keys: KEYS, cells: { content_type: '案例集' }, drafts: DRAFTS, types: TYPES })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('**文本主键不续号** —— 序号能续是因为它本就是「第几条」,PN 这种续不出来', () => {
|
||||
const textKeys = ['rfq_ref', 'pn'];
|
||||
const textTypes = { rfq_ref: 'text', pn: 'text' };
|
||||
const drafts = [{ key: { rfq_ref: 'R1', pn: 'A' } }, { key: { rfq_ref: 'R1', pn: 'B' } }];
|
||||
expect(resolveRowKey({ keys: textKeys, cells: { eau: '100' }, drafts, types: textTypes })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('提交闸:必填没填就交不了', () => {
|
||||
const gate = (rows: Array<Record<string, unknown>>) =>
|
||||
gateOf(missingReadings({ 'mine:quote_intake.record': rows }, [RECORD]), [RECORD]);
|
||||
@@ -298,3 +377,64 @@ describe('自增主键:前缀-两位序号', () => {
|
||||
expect(gen([], {}, 4, ['seq'])).toEqual({ seq: '05' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('主键摆不全时认现有行:一格都没认出来的碎片不许命中随便第一行', () => {
|
||||
const drafts = [
|
||||
{ key: { round_ref: 'R1', contact_code: 'CU-0142-01' } },
|
||||
{ key: { round_ref: 'R1', contact_code: 'CU-0087-02' } },
|
||||
{ key: { round_ref: 'R1', contact_code: 'CU-0087-01' } },
|
||||
];
|
||||
const keys = ['round_ref', 'contact_code'];
|
||||
|
||||
it('至少一格对得上 → 命中那一行(正常路径)', () => {
|
||||
expect(findKnownRow(drafts, keys, { contact_code: 'CU-0087-02' })?.key).toEqual({
|
||||
round_ref: 'R1',
|
||||
contact_code: 'CU-0087-02',
|
||||
});
|
||||
});
|
||||
|
||||
it('**一格主键都没给的碎片**(如渲染多嵌一层导致 collect() 拆出的空键分组)→ 不命中第一行,回 undefined', () => {
|
||||
expect(findKnownRow(drafts, keys, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('给了但一个都对不上 → 同样不命中(不是「随便一行」,是「哪行都不是」)', () => {
|
||||
expect(findKnownRow(drafts, keys, { contact_code: 'CU-9999-01' })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* **点开一份资料文件时,那一行是怎么认出来的**(2026-08-23)。
|
||||
*
|
||||
* 件上传来的 `key` 是一串字符,而"哪一行"是主键说了算。这一段的要害是
|
||||
* **只认声明里的主键列** —— 不然一个精心构造的 key 就能把别的行当成"这一份资料"取走。
|
||||
*/
|
||||
describe('parseRowKey —— 件上传来的主键认哪一行', () => {
|
||||
it('单列主键:直接给值', () => {
|
||||
expect(parseRowKey('CT-2026-0001', ['content_id'])).toEqual({ content_id: 'CT-2026-0001' });
|
||||
});
|
||||
|
||||
it('多列主键:列=值 以逗号连,顺序无所谓', () => {
|
||||
expect(parseRowKey('req_seq=2,round_ref=BR-1', ['round_ref', 'req_seq'])).toEqual({
|
||||
round_ref: 'BR-1',
|
||||
req_seq: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('**声明里没有的列名一律不认** —— 别让一串 key 把别的行取走', () => {
|
||||
expect(parseRowKey('content_id=A,secret=1', ['content_id'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('主键给不全:不认(宁可打不开,也不打开错的那一份)', () => {
|
||||
expect(parseRowKey('round_ref=BR-1', ['round_ref', 'req_seq'])).toBeUndefined();
|
||||
expect(parseRowKey('round_ref=BR-1,req_seq=', ['round_ref', 'req_seq'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('多列主键却只给了个裸值:不认(猜不出它是哪一列)', () => {
|
||||
expect(parseRowKey('BR-1', ['round_ref', 'req_seq'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('空串 / 没有主键列:不认', () => {
|
||||
expect(parseRowKey('', ['content_id'])).toBeUndefined();
|
||||
expect(parseRowKey('X', [])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user