- 계기: 작업실 화면에서 CAD/ 폴더를 지우면 되돌릴 수 없었다(LeRobot SO-101 프로젝트 CAD/ 소실, 2026-09-25 실측). - 삭제 시 workshop/.trash/<projectId>/<타임스탬프-접미사>/ 로 rename만 (같은 볼륨이라 즉시, UI 변경 없음). 14일 지난 항목은 다음 삭제 시 자동 정리. - 프로젝트 삭제 시 첨부 폴더도 휴지통으로(기존엔 통째 rm). - 휴지통 이동 실패 시에만 기존 rm으로 폴백. 스탬프에 고유 접미사로 연속 삭제 덮어쓰기 방지. 테스트 5건 추가. Co-Authored-By: Claude Code <noreply@anthropic.com>
65 lines
3.5 KiB
TypeScript
65 lines
3.5 KiB
TypeScript
// 작업실 휴지통(soft delete, 2026-09-25) 테스트.
|
|
// 계기: 작업실 화면에서 CAD/ 폴더를 지우면 rm이라 되돌릴 수 없었다(LeRobot SO-101
|
|
// 프로젝트 CAD/ 소실). 삭제는 workshop/.trash/<projectId>/<타임스탬프>/ 이동이고,
|
|
// 14일 지난 항목은 다음 삭제 시점에 자동 영구 삭제된다.
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import { moveToWorkshopTrash } from '../src/gateway/routes/routes-workshop';
|
|
|
|
describe('moveToWorkshopTrash — 작업실 삭제를 휴지통 이동으로', () => {
|
|
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-trash-'));
|
|
const pid = 'proj_test1234';
|
|
const projDir = path.join(ws, 'workshop', pid);
|
|
const trashProj = path.join(ws, 'workshop', '.trash', pid);
|
|
const RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
|
|
test('폴더 삭제 → 휴지통 이동(내용물 보존, 원래 자리는 비움)', () => {
|
|
fs.mkdirSync(path.join(projDir, 'CAD'), { recursive: true });
|
|
fs.writeFileSync(path.join(projDir, 'CAD', 'part.stl'), 'BINARY');
|
|
const ok = moveToWorkshopTrash(ws, pid, path.join(projDir, 'CAD'));
|
|
assert.ok(ok);
|
|
assert.ok(!fs.existsSync(path.join(projDir, 'CAD')), '원래 폴더가 남아있음');
|
|
const stamps = fs.readdirSync(trashProj);
|
|
assert.equal(stamps.length, 1);
|
|
const moved = path.join(trashProj, stamps[0], 'CAD', 'part.stl');
|
|
assert.ok(fs.existsSync(moved), '휴지통에 내용물이 없음');
|
|
assert.equal(fs.readFileSync(moved, 'utf8'), 'BINARY');
|
|
});
|
|
|
|
test('단일 파일 삭제도 휴지통으로', () => {
|
|
fs.writeFileSync(path.join(projDir, 'note.txt'), 'x');
|
|
assert.ok(moveToWorkshopTrash(ws, pid, path.join(projDir, 'note.txt')));
|
|
assert.ok(!fs.existsSync(path.join(projDir, 'note.txt')));
|
|
assert.equal(fs.readdirSync(trashProj).length, 2, '타임스탬프 폴더가 삭제 건수만큼');
|
|
});
|
|
|
|
test('보존기간(14일) 지난 항목은 다음 삭제 시점에 자동 영구 삭제', () => {
|
|
// 새 폴더를 하나 더 지우고 → 직전 삭제 건이 14일+ 된 것처럼 mtime을 조작 → 다시 삭제
|
|
const stamps = fs.readdirSync(trashProj);
|
|
fs.utimesSync(path.join(trashProj, stamps[0]), new Date(Date.now() - RETENTION_MS - 60_000), new Date(Date.now() - RETENTION_MS - 60_000));
|
|
fs.mkdirSync(path.join(projDir, '사진'), { recursive: true });
|
|
fs.writeFileSync(path.join(projDir, '사진', 'a.jpg'), 'y');
|
|
assert.ok(moveToWorkshopTrash(ws, pid, path.join(projDir, '사진')));
|
|
const after = fs.readdirSync(trashProj);
|
|
assert.ok(!after.includes(stamps[0]), '14일 지난 항목이 정리되지 않음');
|
|
assert.equal(after.length, 2, '남은 항목 수가 다름(직전 건 + 방금 건)');
|
|
});
|
|
|
|
test('같은 이름을 여러 번 지워도 덮어쓰지 않는다(타임스탬프 분리)', () => {
|
|
for (const name of ['a.stl', 'a.stl', 'a.stl']) {
|
|
fs.writeFileSync(path.join(projDir, name), name);
|
|
assert.ok(moveToWorkshopTrash(ws, pid, path.join(projDir, name)));
|
|
}
|
|
const copies = fs.readdirSync(trashProj).flatMap(s => fs.readdirSync(path.join(trashProj, s)));
|
|
assert.equal(copies.filter(c => c === 'a.stl').length, 3);
|
|
});
|
|
|
|
test('대상이 없으면 false(호출부가 rm 폴백하지 않도록)', () => {
|
|
assert.equal(moveToWorkshopTrash(ws, pid, path.join(projDir, '없는파일.stl')), false);
|
|
});
|
|
|
|
test.after(() => { try { fs.rmSync(ws, { recursive: true, force: true }); } catch { /* noop */ } });
|
|
}); |