mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-04 11:51:53 +00:00
test: add unit tests for cmdProposal
Refactor `src/cmd-proposal.mjs` to use dependency injection for testability. Add `test/cmd-proposal.test.mjs` to verify all subcommands (`create`, `list`, `apply`, `reject`, `show`) and error handling. Fix indentation in `src/cmd-proposal.mjs`. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
79e261d7b2
commit
940bc9ca66
4 files changed
+7845
-37
No files matched your search
+54
-37
@@ -1,99 +1,116 @@
|
||||
import { createProposal, listProposals, applyProposal, rejectProposal, getProposal } from './services/governance/index.js';
|
||||
import {
|
||||
createProposal as _createProposal,
|
||||
listProposals as _listProposals,
|
||||
applyProposal as _applyProposal,
|
||||
rejectProposal as _rejectProposal,
|
||||
getProposal as _getProposal
|
||||
} from './services/governance/index.js';
|
||||
import { ExitError } from './errors.mjs';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
export async function cmdProposal(subcommand, args = {}) {
|
||||
export async function cmdProposal(subcommand, args = {}, deps = {}) {
|
||||
const {
|
||||
createProposal = _createProposal,
|
||||
listProposals = _listProposals,
|
||||
applyProposal = _applyProposal,
|
||||
rejectProposal = _rejectProposal,
|
||||
getProposal = _getProposal,
|
||||
readFile = fs.readFile,
|
||||
log = console.log,
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
switch (subcommand) {
|
||||
case 'create':
|
||||
return await handleCreate(args);
|
||||
return await handleCreate(args, { createProposal, readFile, log, error });
|
||||
case 'list':
|
||||
return await handleList(args);
|
||||
return await handleList(args, { listProposals, log, error });
|
||||
case 'apply':
|
||||
return await handleApply(args);
|
||||
return await handleApply(args, { applyProposal, log, error });
|
||||
case 'reject':
|
||||
return await handleReject(args);
|
||||
return await handleReject(args, { rejectProposal, log, error });
|
||||
case 'show':
|
||||
return await handleShow(args);
|
||||
return await handleShow(args, { getProposal, log, error });
|
||||
default:
|
||||
console.error(`Unknown proposal subcommand: ${subcommand}`);
|
||||
error(`Unknown proposal subcommand: ${subcommand}`);
|
||||
throw new ExitError(1, 'Unknown subcommand');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate({ agent, target, contentFile, reason }) {
|
||||
async function handleCreate({ agent, target, contentFile, reason }, { createProposal, readFile, log, error }) {
|
||||
if (!agent || !target || !contentFile || !reason) {
|
||||
console.error('Usage: torch-lock proposal create --agent <name> --target <path> --content <file> --reason <text>');
|
||||
error('Usage: torch-lock proposal create --agent <name> --target <path> --content <file> --reason <text>');
|
||||
throw new ExitError(1, 'Missing arguments');
|
||||
}
|
||||
|
||||
let newContent;
|
||||
try {
|
||||
newContent = await fs.readFile(contentFile, 'utf8');
|
||||
newContent = await readFile(contentFile, 'utf8');
|
||||
} catch (_e) {
|
||||
console.error(`Failed to read content file: ${contentFile}`);
|
||||
error(`Failed to read content file: ${contentFile}`);
|
||||
throw new ExitError(1, 'File read error');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createProposal({ agent, target, newContent, reason });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(`Failed to create proposal: ${e.message}`);
|
||||
error(`Failed to create proposal: ${e.message}`);
|
||||
throw new ExitError(1, 'Proposal creation failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleList({ status }) {
|
||||
async function handleList({ status }, { listProposals, log, error }) {
|
||||
try {
|
||||
const proposals = await listProposals();
|
||||
const filtered = status ? proposals.filter(p => p.status === status) : proposals;
|
||||
console.log(JSON.stringify(filtered, null, 2));
|
||||
log(JSON.stringify(filtered, null, 2));
|
||||
} catch (e) {
|
||||
console.error(`Failed to list proposals: ${e.message}`);
|
||||
error(`Failed to list proposals: ${e.message}`);
|
||||
throw new ExitError(1, 'List failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApply({ id }) {
|
||||
async function handleApply({ id }, { applyProposal, log, error }) {
|
||||
if (!id) {
|
||||
console.error('Usage: torch-lock proposal apply --id <proposal-id>');
|
||||
error('Usage: torch-lock proposal apply --id <proposal-id>');
|
||||
throw new ExitError(1, 'Missing id');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await applyProposal(id);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(`Failed to apply proposal: ${e.message}`);
|
||||
error(`Failed to apply proposal: ${e.message}`);
|
||||
throw new ExitError(1, 'Apply failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject({ id, reason }) {
|
||||
async function handleReject({ id, reason }, { rejectProposal, log, error }) {
|
||||
if (!id || !reason) {
|
||||
console.error('Usage: torch-lock proposal reject --id <proposal-id> --reason <text>');
|
||||
error('Usage: torch-lock proposal reject --id <proposal-id> --reason <text>');
|
||||
throw new ExitError(1, 'Missing arguments');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await rejectProposal(id, reason);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(`Failed to reject proposal: ${e.message}`);
|
||||
error(`Failed to reject proposal: ${e.message}`);
|
||||
throw new ExitError(1, 'Reject failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleShow({ id }) {
|
||||
if (!id) {
|
||||
console.error('Usage: torch-lock proposal show --id <proposal-id>');
|
||||
throw new ExitError(1, 'Missing id');
|
||||
}
|
||||
try {
|
||||
const proposal = await getProposal(id);
|
||||
console.log(JSON.stringify(proposal, null, 2));
|
||||
} catch (e) {
|
||||
console.error(`Failed to show proposal: ${e.message}`);
|
||||
throw new ExitError(1, 'Show failed');
|
||||
}
|
||||
async function handleShow({ id }, { getProposal, log, error }) {
|
||||
if (!id) {
|
||||
error('Usage: torch-lock proposal show --id <proposal-id>');
|
||||
throw new ExitError(1, 'Missing id');
|
||||
}
|
||||
try {
|
||||
const proposal = await getProposal(id);
|
||||
log(JSON.stringify(proposal, null, 2));
|
||||
} catch (e) {
|
||||
error(`Failed to show proposal: ${e.message}`);
|
||||
throw new ExitError(1, 'Show failed');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { cmdProposal } from '../src/cmd-proposal.mjs';
|
||||
import { ExitError } from '../src/errors.mjs';
|
||||
|
||||
describe('cmdProposal', () => {
|
||||
const noop = () => {};
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a proposal successfully', async () => {
|
||||
const logs = [];
|
||||
const errors = [];
|
||||
const mockCreate = async (args) => {
|
||||
return { id: 'prop-123', status: 'open', ...args };
|
||||
};
|
||||
const mockReadFile = async () => 'file-content';
|
||||
|
||||
const deps = {
|
||||
createProposal: mockCreate,
|
||||
readFile: mockReadFile,
|
||||
log: (msg) => logs.push(msg),
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
const args = {
|
||||
agent: 'test-agent',
|
||||
target: 'src/foo.js',
|
||||
contentFile: 'new-content.txt',
|
||||
reason: 'fix bug',
|
||||
};
|
||||
|
||||
await cmdProposal('create', args, deps);
|
||||
|
||||
assert.strictEqual(logs.length, 1);
|
||||
assert.strictEqual(errors.length, 0);
|
||||
const output = JSON.parse(logs[0]);
|
||||
assert.strictEqual(output.id, 'prop-123');
|
||||
assert.strictEqual(output.newContent, 'file-content');
|
||||
assert.strictEqual(output.agent, 'test-agent');
|
||||
});
|
||||
|
||||
it('should throw if arguments are missing', async () => {
|
||||
const errors = [];
|
||||
const deps = {
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('create', {}, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing arguments'
|
||||
);
|
||||
assert.ok(errors[0].includes('Usage:'));
|
||||
});
|
||||
|
||||
it('should throw if file read fails', async () => {
|
||||
const errors = [];
|
||||
const mockReadFile = async () => { throw new Error('File not found'); };
|
||||
const deps = {
|
||||
readFile: mockReadFile,
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
const args = {
|
||||
agent: 'test-agent',
|
||||
target: 'src/foo.js',
|
||||
contentFile: 'missing.txt',
|
||||
reason: 'fix bug',
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('create', args, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'File read error'
|
||||
);
|
||||
assert.ok(errors[0].includes('Failed to read content file'));
|
||||
});
|
||||
|
||||
it('should throw if createProposal fails', async () => {
|
||||
const errors = [];
|
||||
const mockCreate = async () => { throw new Error('DB error'); };
|
||||
const mockReadFile = async () => 'content';
|
||||
const deps = {
|
||||
createProposal: mockCreate,
|
||||
readFile: mockReadFile,
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
const args = {
|
||||
agent: 'test-agent',
|
||||
target: 'src/foo.js',
|
||||
contentFile: 'content.txt',
|
||||
reason: 'fix bug',
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('create', args, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Proposal creation failed'
|
||||
);
|
||||
assert.ok(errors[0].includes('Failed to create proposal'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should list all proposals', async () => {
|
||||
const logs = [];
|
||||
const mockList = async () => [{ id: '1', status: 'open' }, { id: '2', status: 'closed' }];
|
||||
const deps = {
|
||||
listProposals: mockList,
|
||||
log: (msg) => logs.push(msg),
|
||||
error: noop,
|
||||
};
|
||||
|
||||
await cmdProposal('list', {}, deps);
|
||||
|
||||
assert.strictEqual(logs.length, 1);
|
||||
const output = JSON.parse(logs[0]);
|
||||
assert.strictEqual(output.length, 2);
|
||||
});
|
||||
|
||||
it('should filter proposals by status', async () => {
|
||||
const logs = [];
|
||||
const mockList = async () => [{ id: '1', status: 'open' }, { id: '2', status: 'closed' }];
|
||||
const deps = {
|
||||
listProposals: mockList,
|
||||
log: (msg) => logs.push(msg),
|
||||
error: noop,
|
||||
};
|
||||
|
||||
await cmdProposal('list', { status: 'open' }, deps);
|
||||
|
||||
assert.strictEqual(logs.length, 1);
|
||||
const output = JSON.parse(logs[0]);
|
||||
assert.strictEqual(output.length, 1);
|
||||
assert.strictEqual(output[0].id, '1');
|
||||
});
|
||||
|
||||
it('should handle list failure', async () => {
|
||||
const errors = [];
|
||||
const mockList = async () => { throw new Error('List error'); };
|
||||
const deps = {
|
||||
listProposals: mockList,
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('list', {}, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'List failed'
|
||||
);
|
||||
assert.ok(errors[0].includes('Failed to list proposals'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('apply', () => {
|
||||
it('should apply a proposal', async () => {
|
||||
const logs = [];
|
||||
const mockApply = async (id) => ({ id, status: 'applied' });
|
||||
const deps = {
|
||||
applyProposal: mockApply,
|
||||
log: (msg) => logs.push(msg),
|
||||
error: noop,
|
||||
};
|
||||
|
||||
await cmdProposal('apply', { id: 'prop-1' }, deps);
|
||||
|
||||
assert.strictEqual(logs.length, 1);
|
||||
const output = JSON.parse(logs[0]);
|
||||
assert.strictEqual(output.status, 'applied');
|
||||
assert.strictEqual(output.id, 'prop-1');
|
||||
});
|
||||
|
||||
it('should throw if id is missing', async () => {
|
||||
const errors = [];
|
||||
const deps = {
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('apply', {}, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing id'
|
||||
);
|
||||
assert.ok(errors[0].includes('Usage:'));
|
||||
});
|
||||
|
||||
it('should handle apply failure', async () => {
|
||||
const errors = [];
|
||||
const mockApply = async () => { throw new Error('Apply error'); };
|
||||
const deps = {
|
||||
applyProposal: mockApply,
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('apply', { id: 'prop-1' }, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Apply failed'
|
||||
);
|
||||
assert.ok(errors[0].includes('Failed to apply proposal'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('reject', () => {
|
||||
it('should reject a proposal', async () => {
|
||||
const logs = [];
|
||||
const mockReject = async (id, reason) => ({ id, status: 'rejected', reason });
|
||||
const deps = {
|
||||
rejectProposal: mockReject,
|
||||
log: (msg) => logs.push(msg),
|
||||
error: noop,
|
||||
};
|
||||
|
||||
await cmdProposal('reject', { id: 'prop-1', reason: 'bad code' }, deps);
|
||||
|
||||
assert.strictEqual(logs.length, 1);
|
||||
const output = JSON.parse(logs[0]);
|
||||
assert.strictEqual(output.status, 'rejected');
|
||||
assert.strictEqual(output.reason, 'bad code');
|
||||
});
|
||||
|
||||
it('should throw if arguments are missing', async () => {
|
||||
const errors = [];
|
||||
const deps = {
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('reject', { id: 'prop-1' }, deps), // missing reason
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing arguments'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle reject failure', async () => {
|
||||
const errors = [];
|
||||
const mockReject = async () => { throw new Error('Reject error'); };
|
||||
const deps = {
|
||||
rejectProposal: mockReject,
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('reject', { id: 'prop-1', reason: 'bad' }, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Reject failed'
|
||||
);
|
||||
assert.ok(errors[0].includes('Failed to reject proposal'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('show', () => {
|
||||
it('should show a proposal', async () => {
|
||||
const logs = [];
|
||||
const mockGet = async (id) => ({ id, content: 'stuff' });
|
||||
const deps = {
|
||||
getProposal: mockGet,
|
||||
log: (msg) => logs.push(msg),
|
||||
error: noop,
|
||||
};
|
||||
|
||||
await cmdProposal('show', { id: 'prop-1' }, deps);
|
||||
|
||||
assert.strictEqual(logs.length, 1);
|
||||
const output = JSON.parse(logs[0]);
|
||||
assert.strictEqual(output.id, 'prop-1');
|
||||
assert.strictEqual(output.content, 'stuff');
|
||||
});
|
||||
|
||||
it('should throw if id is missing', async () => {
|
||||
const errors = [];
|
||||
const deps = {
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('show', {}, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing id'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle show failure', async () => {
|
||||
const errors = [];
|
||||
const mockGet = async () => { throw new Error('Show error'); };
|
||||
const deps = {
|
||||
getProposal: mockGet,
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('show', { id: 'prop-1' }, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Show failed'
|
||||
);
|
||||
assert.ok(errors[0].includes('Failed to show proposal'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown subcommand', () => {
|
||||
it('should throw ExitError for unknown subcommand', async () => {
|
||||
const errors = [];
|
||||
const deps = {
|
||||
log: noop,
|
||||
error: (msg) => errors.push(msg),
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => cmdProposal('foo', {}, deps),
|
||||
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Unknown subcommand'
|
||||
);
|
||||
assert.ok(errors[0].includes('Unknown proposal subcommand'));
|
||||
});
|
||||
});
|
||||
});
|
||||
+3738
File diff suppressed because it is too large.
Load diff
+3737
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user