fix: harden release gate and page delivery
Memind CI / Test, build, and release guards (push) Failing after 12m3s
Memind CI / Test, build, and release guards (push) Failing after 12m3s
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user