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); });