Files
memind/release-gate/excel-continuity.test.mjs
john b1577a16e9
Memind CI / Test, build, and release guards (push) Failing after 12m3s
fix: harden release gate and page delivery
2026-07-26 14:32:01 +08:00

177 lines
6.6 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import ExcelJS from 'exceljs';
import {
createExcelAnalysisEngine,
excelAnalysisInternals,
} from '../excel-analysis-engine.mjs';
async function writeWorkbook(root, rows = [
['华东', '苹果', 100],
['华南', '香蕉', 80],
]) {
fs.mkdirSync(path.join(root, 'oa'), { recursive: true });
const workbook = new ExcelJS.Workbook();
const sales = workbook.addWorksheet('销售');
sales.addRow(['地区', '产品', '销售额']);
for (const row of rows) sales.addRow(row);
const inventory = workbook.addWorksheet('库存');
inventory.addRow(['产品', '库存']);
inventory.addRow(['苹果', 20]);
inventory.addRow(['香蕉', 30]);
await workbook.xlsx.writeFile(path.join(root, 'oa/input.xlsx'));
}
test('XLS-06 bounded scan, sampling and file budgets fail fast', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-budget-'));
try {
await writeWorkbook(root);
const tinyFileBudget = createExcelAnalysisEngine({ workspaceRoot: root, maxFileBytes: 32 });
await assert.rejects(
() => tinyFileBudget.inspect({ path: 'oa/input.xlsx' }),
(error) => error?.code === 'FILE_TOO_LARGE',
);
const engine = createExcelAnalysisEngine({ workspaceRoot: root });
await assert.rejects(
() => engine.inspect({ path: 'oa/input.xlsx', max_cells: 2 }),
(error) => error?.code === 'WORKBOOK_TOO_LARGE',
);
const sampled = await engine.inspect({ path: 'oa/input.xlsx', sample_rows: 1 });
assert.equal(sampled.sheets[0].samples.length, 1);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('XLS-08 follow-up analysis changes dimension while reusing the same uploaded path', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-followup-'));
try {
await writeWorkbook(root);
const engine = createExcelAnalysisEngine({ workspaceRoot: root });
const byRegion = await engine.analyze({
path: 'oa/input.xlsx',
sheet: '销售',
operation: 'group_by',
group_by: '地区',
metric: '销售额',
});
const byProduct = await engine.analyze({
path: byRegion.path,
sheet: '销售',
operation: 'group_by',
group_by: '产品',
metric: '销售额',
});
assert.equal(byRegion.path, 'oa/input.xlsx');
assert.equal(byProduct.path, byRegion.path);
assert.deepEqual(byProduct.result.items.map((item) => item.group), ['苹果', '香蕉']);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('XLS-09 report keeps source explanation and a safe original-file download entry', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-source-link-'));
try {
await writeWorkbook(root);
const engine = createExcelAnalysisEngine({ workspaceRoot: root });
const report = await engine.report({
path: 'oa/input.xlsx',
sheet: '销售',
metric: '销售额',
dimensions: ['地区', '产品'],
output_path: 'public/dashboard.html',
source_download_url: '/api/mindspace/v1/assets/asset-fixture/download',
});
const html = fs.readFileSync(path.join(root, report.outputPath), 'utf8');
assert.match(html, /数据来源:oa\/input\.xlsx/);
assert.match(html, /class="source-download"/);
assert.match(html, /下载原始 Excel/);
assert.throws(
() => excelAnalysisInternals.safeSourceDownloadUrl('http://127.0.0.1/private.xlsx'),
(error) => error?.code === 'INVALID_SOURCE_DOWNLOAD_URL',
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('XLS-11 oversized/tool-timeout fallback never fabricates numbers and delivers a real artifact', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-fallback-'));
try {
await writeWorkbook(root);
const engine = createExcelAnalysisEngine({ workspaceRoot: root });
const verified = await engine.analyze({
path: 'oa/input.xlsx',
sheet: '销售',
operation: 'group_by',
group_by: '地区',
metric: '销售额',
});
const largePath = path.join(root, 'oa/input-large.xlsx');
const originalBytes = fs.readFileSync(path.join(root, 'oa/input.xlsx'));
fs.writeFileSync(largePath, Buffer.concat([
originalBytes,
Buffer.alloc((5 * 1024 * 1024) - originalBytes.length + 1024),
]));
const boundedTool = createExcelAnalysisEngine({
workspaceRoot: root,
maxFileBytes: 5 * 1024 * 1024,
});
await assert.rejects(
() => boundedTool.inspect({ path: 'oa/input-large.xlsx' }),
(error) => error?.code === 'FILE_TOO_LARGE',
);
const fallbackPath = path.join(root, 'public/fallback.json');
fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
fs.writeFileSync(fallbackPath, `${JSON.stringify({
source: verified.path,
verified: true,
items: verified.result.items,
warning: '完整工作簿工具超时,交付已验证摘要',
})}\n`);
const delivered = JSON.parse(fs.readFileSync(fallbackPath, 'utf8'));
assert.equal(delivered.verified, true);
assert.equal(delivered.items.reduce((sum, item) => sum + item.value, 0), 180);
assert.ok(fs.statSync(fallbackPath).size > 100);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('XLS-12 incremental workbook update preserves dashboard path, old data and confirmed dimensions', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-incremental-'));
try {
await writeWorkbook(root);
const engine = createExcelAnalysisEngine({ workspaceRoot: root });
const args = {
path: 'oa/input.xlsx',
sheet: '销售',
metric: '销售额',
dimensions: ['地区', '产品'],
output_path: 'public/stable-dashboard.html',
title: '用户确认:区域销售看板',
};
const initial = await engine.report(args);
assert.equal(initial.model.totals.metric, 180);
await writeWorkbook(root, [
['华东', '苹果', 100],
['华南', '香蕉', 80],
['华北', '苹果', 40],
]);
const updated = await engine.report(args);
assert.equal(updated.outputPath, initial.outputPath);
assert.equal(updated.model.totals.metric, 220);
assert.deepEqual(updated.model.fields.dimensions, ['地区', '产品']);
const html = fs.readFileSync(path.join(root, updated.outputPath), 'utf8');
assert.match(html, /用户确认:区域销售看板/);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});