Files
TORCH/test/ops-remove.test.mjs
T
Claude 7984c7de66 feat: add torch-lock remove command for complete TORCH uninstallation
Users who installed TORCH via `npm install <tarball> && npx torch-lock init`
had no built-in way to cleanly remove it. This adds a `remove` command that
detects and removes all TORCH artifacts: the torch/ directory, config files,
runtime directories (.torch/, .scheduler-memory/, task-logs/), governance
proposals, host package.json scripts, and the npm package itself.

- Add cmdRemove() in src/ops.mjs with detection, preview, confirmation, and cleanup
- Wire up `remove` command in CLI dispatcher (src/lib.mjs)
- Add TORCH_HOST_SCRIPTS constant in constants.mjs for shared script key list
- Add docs/removal.md with full manual removal instructions
- Add removal section to README.md
- Add 7 scenario tests in test/ops-remove.test.mjs

https://claude.ai/code/session_011Snrxu3iZGLtJ9pnFSajpM
2026-02-20 01:59:30 +00:00

145 lines
6.1 KiB
JavaScript

import { test, after } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { cmdInit, cmdRemove } from '../src/ops.mjs';
const MOCK_CONFIG = {
installDir: 'torch',
namespace: 'test-namespace',
relays: ['wss://relay.damus.io']
};
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-ops-remove-'));
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
test('cmdRemove reports nothing when no TORCH installation exists', async () => {
const projectRoot = path.join(tempBase, 'empty-project');
fs.mkdirSync(projectRoot, { recursive: true });
// Create a basic package.json so it looks like a real project
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({ name: 'my-app' }));
// Should complete without error — just reports nothing found
await cmdRemove(true, projectRoot);
// Project should be untouched
assert.ok(fs.existsSync(path.join(projectRoot, 'package.json')), 'package.json still exists');
});
test('cmdRemove with --force removes torch/ directory', async () => {
const projectRoot = path.join(tempBase, 'project-force');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Verify init created the expected artifacts
assert.ok(fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ exists before remove');
assert.ok(fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'torch-config.json exists before remove');
assert.ok(fs.existsSync(path.join(projectRoot, '.torch')), '.torch/ exists before remove');
await cmdRemove(true, projectRoot);
// All TORCH artifacts should be gone
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ removed');
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'torch-config.json removed');
assert.ok(!fs.existsSync(path.join(projectRoot, '.torch')), '.torch/ removed');
});
test('cmdRemove removes runtime artifacts (task-logs, .scheduler-memory)', async () => {
const projectRoot = path.join(tempBase, 'project-runtime');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Simulate runtime artifacts that the scheduler would create
fs.mkdirSync(path.join(projectRoot, 'task-logs', 'daily'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'task-logs', 'daily', 'run.md'), '# log');
fs.mkdirSync(path.join(projectRoot, '.scheduler-memory'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, '.scheduler-memory', 'memory-store.json'), '{}');
await cmdRemove(true, projectRoot);
assert.ok(!fs.existsSync(path.join(projectRoot, 'task-logs')), 'task-logs/ removed');
assert.ok(!fs.existsSync(path.join(projectRoot, '.scheduler-memory')), '.scheduler-memory/ removed');
});
test('cmdRemove removes src/proposals/ and cleans empty src/', async () => {
const projectRoot = path.join(tempBase, 'project-proposals');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// src/proposals/ is created by cmdInit
assert.ok(fs.existsSync(path.join(projectRoot, 'src', 'proposals')), 'src/proposals/ exists before remove');
await cmdRemove(true, projectRoot);
assert.ok(!fs.existsSync(path.join(projectRoot, 'src', 'proposals')), 'src/proposals/ removed');
// src/ should also be removed since it's now empty
assert.ok(!fs.existsSync(path.join(projectRoot, 'src')), 'empty src/ removed');
});
test('cmdRemove cleans torch:* scripts from host package.json', async () => {
const projectRoot = path.join(tempBase, 'project-scripts');
fs.mkdirSync(projectRoot, { recursive: true });
// Create a host package.json with existing user scripts
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({
name: 'my-app',
scripts: {
test: 'jest',
build: 'webpack'
}
}, null, 2));
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Verify init injected torch scripts
const pkgBefore = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
assert.ok('torch:dashboard' in pkgBefore.scripts, 'torch:dashboard injected');
assert.ok('torch:check' in pkgBefore.scripts, 'torch:check injected');
await cmdRemove(true, projectRoot);
// User scripts should remain, torch scripts should be gone
const pkgAfter = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
assert.strictEqual(pkgAfter.scripts.test, 'jest', 'user test script preserved');
assert.strictEqual(pkgAfter.scripts.build, 'webpack', 'user build script preserved');
assert.ok(!('torch:dashboard' in pkgAfter.scripts), 'torch:dashboard removed');
assert.ok(!('torch:check' in pkgAfter.scripts), 'torch:check removed');
assert.ok(!('torch:lock' in pkgAfter.scripts), 'torch:lock removed');
assert.ok(!('torch:health' in pkgAfter.scripts), 'torch:health removed');
assert.ok(!('torch:memory:list' in pkgAfter.scripts), 'torch:memory:list removed');
assert.ok(!('torch:memory:inspect' in pkgAfter.scripts), 'torch:memory:inspect removed');
});
test('cmdRemove cancels when user declines confirmation', async () => {
const projectRoot = path.join(tempBase, 'project-cancel');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Decline the confirmation
await cmdRemove(false, projectRoot, { confirm: false });
// Everything should still exist
assert.ok(fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ still exists after cancel');
assert.ok(fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'config still exists after cancel');
});
test('cmdRemove confirmed via mockAnswers removes artifacts', async () => {
const projectRoot = path.join(tempBase, 'project-confirm');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Confirm the removal
await cmdRemove(false, projectRoot, { confirm: true });
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ removed after confirm');
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'config removed after confirm');
});