- model_search: 3사이트 통합 검색(Thingiverse는 토큰 필요, Printables/MakerWorld는 익명 검색 실측 확인) - model_download: 슬라이싱 가능한 STL/3MF 중 최대 파일 1개 워크스페이스 저장(Printables getDownloadLink 익명 동작, Thingiverse /v2/files download, MakerWorld는 Bambu 토큰 필요) - print3d_model에 stl_path 추가 — model_download로 받아둔 파일 바로 슬라이싱 - 토큰은 vault secret(print3d.*_token/cookie), Thingiverse App Token 등록 완료 - 게이트: ws_ 세션 항상 + 메인 채팅은 3D 키워드 Co-Authored-By: Claude Code <noreply@anthropic.com>
117 lines
5.3 KiB
TypeScript
117 lines
5.3 KiB
TypeScript
// model-search.ts 순수함수 단위 테스트 (2026-09-23)
|
|
// 네트워크는 건드리지 않는다 — 사이트 클라이언트의 응답 매핑/파싱만 검증.
|
|
// 픽스처는 2026-09-23 실측 curl 응답(축약)을 그대로 쓴다.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
parseModelRef, resolveSitePlan, mapThingiverseResults, mapPrintablesResults,
|
|
mapMakerWorldResults, formatModelResults, downloadErrorDetail, pickSlicableFile,
|
|
} from '../src/tools/model-search.js';
|
|
|
|
test('parseModelRef: 숫자 id 그대로 통과', () => {
|
|
assert.deepEqual(parseModelRef('printables', '1138497'), { id: '1138497' });
|
|
assert.deepEqual(parseModelRef('thingiverse', '2473'), { id: '2473' });
|
|
});
|
|
|
|
test('parseModelRef: 모델 페이지 URL 해석 (3 사이트)', () => {
|
|
assert.deepEqual(parseModelRef('printables', 'https://www.printables.com/model/1138497-mecanum-wheel'), { id: '1138497' });
|
|
assert.deepEqual(parseModelRef('printables', 'printables.com/print/952992'), { id: '952992' });
|
|
assert.deepEqual(parseModelRef('makerworld', 'https://makerworld.com/en/models/716411'), { id: '716411' });
|
|
assert.deepEqual(parseModelRef('thingiverse', 'https://www.thingiverse.com/thing:2473'), { id: '2473' });
|
|
assert.deepEqual(parseModelRef('thingiverse', 'thingiverse.com/things/2473'), { id: '2473' });
|
|
});
|
|
|
|
test('parseModelRef: 가비지는 에러', () => {
|
|
assert.ok('error' in parseModelRef('printables', ''));
|
|
assert.ok('error' in parseModelRef('printables', 'https://example.com/thing/123'));
|
|
assert.ok('error' in parseModelRef('makerworld', 'not-a-url'));
|
|
});
|
|
|
|
test('resolveSitePlan: Thingiverse는 토큰 없으면 스킵, 나머지는 익명 가능', () => {
|
|
const plan = resolveSitePlan({}, {});
|
|
assert.deepEqual(plan.attempt, ['printables', 'makerworld']);
|
|
assert.equal(plan.skipped.length, 1);
|
|
assert.equal(plan.skipped[0].site, 'thingiverse');
|
|
assert.ok(plan.skipped[0].reason.includes('thingiverse_token'));
|
|
});
|
|
|
|
test('resolveSitePlan: 토큰 있으면 3개 전부 시도', () => {
|
|
const plan = resolveSitePlan({}, { thingiverse: 'tok' });
|
|
assert.deepEqual(plan.attempt, ['thingiverse', 'printables', 'makerworld']);
|
|
assert.equal(plan.skipped.length, 0);
|
|
});
|
|
|
|
test('resolveSitePlan: site 지정이 그 사이트만', () => {
|
|
const plan = resolveSitePlan({ site: 'printables' }, {});
|
|
assert.deepEqual(plan.attempt, ['printables']);
|
|
// 지정 사이트에 토큰이 필요한데 없으면 전부 스킵
|
|
const plan2 = resolveSitePlan({ site: 'thingiverse' }, {});
|
|
assert.deepEqual(plan2.attempt, []);
|
|
assert.equal(plan2.skipped[0].site, 'thingiverse');
|
|
});
|
|
|
|
test('mapThingiverseResults: 실측 응답(축약) 매핑', () => {
|
|
const mapped = mapThingiverseResults({
|
|
total: 221,
|
|
hits: [{
|
|
id: 2473, name: 'Mecanum Wheel MK2', like_count: 1323,
|
|
public_url: 'https://www.thingiverse.com/thing:2473',
|
|
creator: { name: 'Zaggo' },
|
|
}],
|
|
});
|
|
assert.equal(mapped.length, 1);
|
|
assert.equal(mapped[0].site, 'thingiverse');
|
|
assert.equal(mapped[0].id, '2473');
|
|
assert.equal(mapped[0].likes, 1323);
|
|
assert.equal(mapped[0].author, 'Zaggo');
|
|
assert.equal(mapped[0].url, 'https://www.thingiverse.com/thing:2473');
|
|
});
|
|
|
|
test('mapPrintablesResults: 실측 응답 매핑 + 빈 필드 허용', () => {
|
|
const mapped = mapPrintablesResults({
|
|
items: [
|
|
{ id: '1138497', name: 'Mecanum Wheel', slug: 'mecanum-wheel', likesCount: 104, downloadCount: 366, filesCount: 6, user: { publicUsername: 'Evolv3D' } },
|
|
{ id: '952992', name: 'x', user: {} },
|
|
],
|
|
});
|
|
assert.equal(mapped.length, 2);
|
|
assert.equal(mapped[0].fileCount, 6);
|
|
assert.equal(mapped[1].likes, 0);
|
|
assert.equal(mapped[1].author, '');
|
|
});
|
|
|
|
test('mapMakerWorldResults: 실측 응답 매핑', () => {
|
|
const mapped = mapMakerWorldResults({
|
|
hits: [{ id: 716411, title: 'Omnidirectional Ball Caster Wheel', likeCount: 1591, downloadCount: 2521, designCreator: { name: 'Claymuh' } }],
|
|
});
|
|
assert.equal(mapped[0].id, '716411');
|
|
assert.equal(mapped[0].site, 'makerworld');
|
|
assert.equal(mapped[0].likes, 1591);
|
|
});
|
|
|
|
test('formatModelResults: 스킵 진단 라인 포함', () => {
|
|
const out = formatModelResults([], [{ site: 'thingiverse', reason: '토큰 미설정' }], 'mecanum wheel');
|
|
assert.ok(out.includes('스킵됨: thingiverse'));
|
|
assert.ok(out.includes('영문'));
|
|
});
|
|
|
|
test('downloadErrorDetail: 사이트별 토큰 만료 안내', () => {
|
|
assert.ok(downloadErrorDetail('thingiverse', 401, '').includes('thingiverse_token'));
|
|
assert.ok(downloadErrorDetail('makerworld', 401, '').includes('90일'));
|
|
assert.ok(downloadErrorDetail('makerworld', 418, '').includes('캡차'));
|
|
assert.ok(downloadErrorDetail('printables', 403, '').includes('printables_cookie'));
|
|
assert.ok(downloadErrorDetail('printables', 500, 'oops').includes('HTTP 500'));
|
|
});
|
|
|
|
test('pickSlicableFile: stl/3mf만, 큰 것 우선, .stp 배제', () => {
|
|
// 09-23 실측: Printables stls 리스트에 .stp 파일이 섞여 온다
|
|
const pick = pickSlicableFile([
|
|
{ name: 'mecanum-wheel-Part1.stp', size: 189000 },
|
|
{ name: 'mecanum-wheel-Part2.stp', size: 10699 },
|
|
{ name: 'small.stl', size: 5000 },
|
|
{ name: 'big.3mf', size: 9000 },
|
|
{ name: 'biggest.stl', size: 12000 },
|
|
]);
|
|
assert.equal(pick?.name, 'biggest.stl');
|
|
assert.equal(pickSlicableFile([{ name: 'a.stp', size: 100 }, { name: 'b.jpg', size: 50 }]), null);
|
|
}); |