| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- // Copyright (c) 未来飞马
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
- //
- // Trademark Notice:
- // The MPL-2.0 license grants copyright permissions for source code only.
- // It does NOT grant any rights to use trademarks including "未来飞马",
- // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
- // Any use of these trademarks requires separate written permission.
- /**
- * 分组 / 顺序 单元测试:`node --test`
- */
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import {
- aspectSimilarity, groupMaterials, inferLayout, orderWithinGroup,
- textSimilarity, timeProximity, applyGroupOverrides, DEFAULT_ORDERING_RULE,
- } from '../grouping.mjs';
- import { reorderAssets } from '../lib.mjs';
- import { makeTinyPng } from '../case-intake.mjs';
- const BASE = 1_700_000_000_000;
- function grid(count, overrides = {}) {
- return Array.from({ length: count }, (_, i) => ({
- kind: 'image',
- localPath: `/tmp/g${String(i + 1).padStart(2, '0')}.png`,
- width: 400,
- height: 400,
- mtimeMs: BASE + i * 1000,
- ocrText: '帝国理工 计算机科学 考前冲刺 提分',
- ...overrides,
- }));
- }
- test('textSimilarity:完全相同为 1,无关文本接近 0', () => {
- assert.equal(textSimilarity('帝国理工提分', '帝国理工提分'), 1);
- assert.ok(textSimilarity('帝国理工提分案例', '帝国理工提分反馈') > 0.3);
- assert.ok(textSimilarity('帝国理工提分', '完全无关的另一段内容') < 0.2);
- });
- test('aspectSimilarity:同比例为 1,缺失尺寸返回 null', () => {
- assert.equal(aspectSimilarity({ width: 400, height: 400 }, { width: 800, height: 800 }), 1);
- assert.equal(aspectSimilarity({ width: 400, height: 400 }, {}), null);
- assert.ok(aspectSimilarity({ width: 400, height: 400 }, { width: 1600, height: 900 }) < 0.8);
- });
- test('timeProximity:窗口内衰减,窗口外为 0', () => {
- const a = { mtimeMs: BASE };
- assert.equal(timeProximity(a, { mtimeMs: BASE }, 60_000), 1);
- assert.ok(timeProximity(a, { mtimeMs: BASE + 30_000 }, 60_000) === 0.5);
- assert.equal(timeProximity(a, { mtimeMs: BASE + 120_000 }, 60_000), 0);
- });
- test('inferLayout:9→3x3 / 4→2x2 / 6→3x2 / 1→1x1', () => {
- assert.equal(inferLayout(9), '3x3');
- assert.equal(inferLayout(4), '2x2');
- assert.equal(inferLayout(6), '3x2');
- assert.equal(inferLayout(1), '1x1');
- });
- test('九宫格:9 张同批图 → 1 组,layout 3x3,order 1..9(左上→右下)', () => {
- const groups = groupMaterials(grid(9));
- assert.equal(groups.length, 1);
- assert.equal(groups[0].layout, '3x3');
- assert.equal(groups[0].count, 9);
- assert.equal(groups[0].orderingRule, DEFAULT_ORDERING_RULE);
- assert.deepEqual(groups[0].items.map((i) => i.order), [1, 2, 3, 4, 5, 6, 7, 8, 9]);
- assert.deepEqual(groups[0].items.map((i) => i.groupId), Array(9).fill(groups[0].groupId));
- });
- test('6 张连发图 → 1 组 3x2(同事连发 6 张是同一个素材)', () => {
- const groups = groupMaterials(grid(6));
- assert.equal(groups.length, 1);
- assert.equal(groups[0].layout, '3x2');
- });
- test('不同批次不误并组:尺寸/时间/内容都不同 → 2 组', () => {
- const items = [
- ...grid(3),
- {
- kind: 'image', localPath: '/tmp/z1.png', width: 1600, height: 900,
- mtimeMs: BASE - 5 * 86_400_000, ocrText: '香港大学 申诉',
- },
- ];
- const groups = groupMaterials(items);
- assert.equal(groups.length, 2);
- assert.deepEqual(groups.map((g) => g.count).sort(), [1, 3]);
- });
- test('尺寸 + 内容两个信号一致 → 并组(时间是弱信号,不足即被 2/3 规则覆盖)', () => {
- const items = grid(2);
- items[1] = { ...items[1], mtimeMs: BASE + 5 * 86_400_000 };
- const groups = groupMaterials(items);
- assert.equal(groups.length, 1, 'aspect + text 两个信号一致即同组');
- });
- test('只有 1 个信号一致 → 不并组(避免把不同素材误并成一组)', () => {
- const items = [
- ...grid(1),
- {
- kind: 'image', localPath: '/tmp/other.png', width: 1600, height: 900,
- mtimeMs: BASE + 5 * 86_400_000, ocrText: '帝国理工 计算机科学 考前冲刺 提分',
- },
- ];
- assert.equal(groupMaterials(items).length, 2, '仅内容相似、尺寸与时间都不同 → 判为两组');
- });
- test('裸图无 OCR:靠尺寸 + 时间仍能成组', () => {
- const items = grid(4, { ocrText: '' });
- const groups = groupMaterials(items);
- assert.equal(groups.length, 1);
- assert.equal(groups[0].count, 4);
- });
- test('orderWithinGroup:按时间升序还原发送顺序;无时间时文件名自然序', () => {
- const withTime = [
- { localPath: '/tmp/b.png', mtimeMs: BASE + 2000 },
- { localPath: '/tmp/a.png', mtimeMs: BASE + 1000 },
- ];
- assert.deepEqual(orderWithinGroup(withTime).map((i) => i.localPath), ['/tmp/a.png', '/tmp/b.png']);
- const noTime = [
- { localPath: '/tmp/img10.png' },
- { localPath: '/tmp/img2.png' },
- ];
- assert.deepEqual(orderWithinGroup(noTime).map((i) => i.localPath), ['/tmp/img2.png', '/tmp/img10.png']);
- });
- test('reorderAssets:把第 9 张移到第 1 位后重新编号 1..9,元素完整', () => {
- const groups = groupMaterials(grid(9));
- const before = groups[0].items;
- const after = reorderAssets(before, 9, 1);
- assert.equal(after[0].localPath, '/tmp/g09.png');
- assert.deepEqual(after.map((a) => a.order), [1, 2, 3, 4, 5, 6, 7, 8, 9]);
- assert.equal(after.length, 9);
- assert.deepEqual(new Set(after.map((a) => a.localPath)).size, 9);
- });
- test('reorderAssets:越界报错', () => {
- assert.throws(() => reorderAssets(grid(3), 5, 1), /越界/);
- });
- test('applyGroupOverrides:人工指定分组与顺序优先', () => {
- const groups = groupMaterials(grid(4));
- const overridden = applyGroupOverrides(groups, {
- groups: [{
- groupId: 'manual',
- items: [
- { localPath: '/tmp/g04.png', order: 1 },
- { localPath: '/tmp/g01.png', order: 2 },
- ],
- }],
- });
- const manual = overridden.find((g) => g.groupId === 'manual');
- assert.ok(manual);
- assert.deepEqual(manual.items.map((i) => i.localPath), ['/tmp/g04.png', '/tmp/g01.png']);
- assert.deepEqual(manual.reasons, ['manual-override']);
- });
- test('makeTinyPng 产出可被 image-size 识别的合法 PNG', async () => {
- const fs = await import('node:fs');
- const os = await import('node:os');
- const path = await import('node:path');
- const { readImageInfo } = await import('../image-size.mjs');
- const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'caseget-')), 'p.png');
- fs.writeFileSync(file, makeTinyPng(320, 240));
- const info = readImageInfo(file);
- assert.equal(info.ok, true);
- assert.equal(info.ext, 'png');
- assert.equal(info.width, 320);
- assert.equal(info.height, 240);
- });
|