aps-agent/tests/node/planning-schedule-entry.tes...

117 lines
6.4 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import test from 'node:test';
const requireWeb = createRequire(new URL('../../apps/web/package.json', import.meta.url));
const ts = requireWeb('typescript');
const React = requireWeb('react');
const { renderToStaticMarkup } = requireWeb('react-dom/server');
const source = readFileSync(new URL('../../apps/web/src/chat/PlanningDataCard.tsx', import.meta.url), 'utf8');
const compiled = ts.transpileModule(source, { compilerOptions: {
jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022,
}}).outputText;
const exported = {};
new Function('require', 'exports', compiled)(requireWeb, exported);
const action = { id: 'start_schedule', label: '开始排产', command: '立即排产', enabled: true };
const missingAction = { id: 'review_missing', label: '查看缺少资料', command: '排产还缺什么', enabled: true,
reason: '请先补齐订单、工艺、工时、设备或班次等排产资料。' };
const trialAction = { id: 'trial_schedule', label: '试排', command: '立即排产', enabled: true,
reason: '按当前主数据生成草稿试排,并列出未安排订单和原因;不会发布或下发。' };
const block = { type: 'folder-pack', props: { reviewOnly: true, adopted: true, canSchedule: false,
projectName: '任意工厂', nextActions: [action], snapshotMode: 'current',
coverageDetail: [{ key: 'orders', count: 2 }, { key: 'equipment', count: 7 }] } };
const render = (props = {}) => renderToStaticMarkup(React.createElement(exported.PlanningDataCard, {
block, active: true, onSend() {}, formatSize: String, ...props,
}));
test('unready data never exposes a scheduling action', () => {
const html = render({ actionsReady: false });
assert.doesNotMatch(html, /data-planning-action="start_schedule"/);
assert.doesNotMatch(html, /开始排产/);
assert.equal((html.match(/aria-label="查看[^\"]+明细"/g) || []).length, 4);
});
test('historical or restored review card has no executable scheduling entry', () => {
const html = render({ active: false });
assert.match(html, /历史记录/);
assert.doesNotMatch(html, /data-planning-action="start_schedule"|开始排产/);
const legacy = render({ active: false, block: { ...block, props: { reviewOnly: true } } });
assert.doesNotMatch(legacy, /开始排产/);
assert.deepEqual(exported.planningActions({ reviewOnly: true }), []);
});
test('backend actions are authoritative and an empty list does not invent another action', () => {
const disabled = render({ block: { ...block, props: { ...block.props, canSchedule: true,
nextActions: [{ ...action, enabled: false, reason: '请先处理来源冲突' }] } } });
assert.match(disabled, /data-planning-action="start_schedule"[^>]*disabled/);
assert.match(disabled, /请先处理来源冲突/);
const missing = render({ block: { ...block, props: { ...block.props, nextActions: [missingAction] } } });
assert.match(missing, /data-planning-action="review_missing"[^>]*>查看缺少资料/);
assert.doesNotMatch(missing, /开始排产/);
const empty = render({ block: { ...block, props: { ...block.props, nextActions: [] } } });
assert.doesNotMatch(empty, /开始排产|确认数据,生成方案/);
assert.deepEqual(exported.planningActions({ nextActions: [{ id: 'x', label: '不能猜', command: 'x' }] }), []);
});
test('adopted data with real blockers exposes a draft-only trial action', () => {
const html = render({ block: { ...block, props: { ...block.props, trialAvailable: true,
nextActions: [missingAction, trialAction] } } });
assert.match(html, /data-planning-action="trial_schedule"[^>]*>试排/);
assert.match(html, /不会发布或下发/);
assert.doesNotMatch(html, /data-planning-action="start_schedule"/);
assert.deepEqual(exported.planningActions({ trialAvailable: false, nextActions: [trialAction] }), []);
});
test('busy and outstanding confirmation disable a ready scheduling action', () => {
for (const props of [{ busy: true }, { pendingConfirmation: true }]) {
const html = render({ ...props, block: { ...block, props: { ...block.props, canSchedule: true,
nextActions: [action] } } });
assert.match(html, /data-planning-action="start_schedule"[^>]*disabled/);
assert.equal((html.match(/aria-label="查看[^\"]+明细"/g) || []).length, 4);
}
assert.match(render({ pendingConfirmation: true, block: { ...block, props: { ...block.props,
canSchedule: true, nextActions: [action] } } }), /已有待确认请求/);
});
test('current plan review remains alongside the ready scheduling action', () => {
const html = render({ viewCommand: '查看排产结果', block: { ...block, props: { ...block.props,
canSchedule: true, nextActions: [action] } } });
assert.match(html, /开始排产/);
assert.match(html, /查看当前方案/);
});
test('unrelated later text does not hide an outstanding confirmation; newer handled card supersedes old one', () => {
const card = (id, handled = false) => ({ blockId: id, type: 'confirm-card',
props: { action: 'import.commit', confirmId: id, handled } });
assert.equal(exported.pendingPlanningConfirmation([{ blocks: [card('old')] }, { text: '状态消息' }]), true);
assert.equal(exported.pendingPlanningConfirmation([{ blocks: [card('old')] }, { blocks: [card('new', true)] }]), false);
assert.equal(exported.pendingPlanningConfirmation([{ blocks: [card('old')] }, { blocks: [
{ type: 'confirm-card', props: { action: 'folder.schedule', handled: true } },
] }]), false);
assert.equal(exported.pendingPlanningConfirmation([{ blocks: [{ type: 'confirm-card', props: { action: 'unrelated' } }] }]), false);
});
test('shared sender submits once until the real request finishes and recovers after failure', async () => {
const commands = [];
let release;
let fail = false;
const sender = exported.createPlanningActionSender(async command => {
commands.push(command);
if (fail) throw new Error('service unavailable');
await new Promise(resolve => { release = resolve; });
});
const first = sender('立即排产');
await sender('立即排产');
assert.deepEqual(commands, ['立即排产']);
release();
await first;
fail = true;
await assert.rejects(sender('立即排产'), /service unavailable/);
fail = false;
const retry = sender('立即排产');
release();
await retry;
assert.equal(commands.length, 3);
});