Tests. Autocomplete. Few Fixes. Mocks for Electrum Service. Template-to-Json parser. Fix global paths. Use IO Dependency injection for logging from cli. Additional commands in CLI.

This commit is contained in:
2026-04-20 10:30:38 +00:00
parent df4f438f6d
commit ff2fe126c6
44 changed files with 8220 additions and 1503 deletions

View File

@@ -0,0 +1,279 @@
import { expect, test, describe, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { addFakeResource, createMockAppService, createMockEngine, DEFAULT_SEED, reserveResource } from "../mocks/engine";
import { type Engine } from "@xo-cash/engine";
import { p2pkhTemplate } from "../mocks/template-p2pkh";
import { AppService } from "../../../src/services/app";
import { handleResourceCommand } from "../../../src/cli/commands/resource";
import { CommandError } from "../../../src/cli/commands/types";
import { createCommandDeps, createMockIO, expectLogs, type LogExpectation } from "../mocks/command";
type TestCase = {
name: string;
inputs: string[];
options?: Record<string, string>;
shouldThrow: boolean;
expectedEvent?: string;
expectedData?: Record<string, unknown>;
logs?: LogExpectation[];
};
const testCases: TestCase[] = [
// List commands (no resources in empty wallet)
{
name: "list returns empty count when no resources",
inputs: ["list"],
shouldThrow: false,
expectedData: {
count: 0,
},
logs: [{ out: "No resources found" }],
},
{
name: "list reserved returns empty count when no reserved resources",
inputs: ["list", "reserved"],
shouldThrow: false,
expectedData: {
count: 0,
},
logs: [{ out: "No resources found" }],
},
{
name: "list all returns empty count when no resources",
inputs: ["list", "all"],
shouldThrow: false,
expectedData: {
count: 0,
},
logs: [{ out: "No resources found" }],
},
// Unreserve-all with no resources
{
name: "unreserve-all returns zero count when no reserved resources",
inputs: ["unreserve-all"],
shouldThrow: false,
expectedData: {
count: 0,
},
logs: [{ out: "No reserved resources" }],
},
// Error cases
{
name: "throws when no subcommand provided",
inputs: [],
shouldThrow: true,
expectedEvent: "resource.subcommand.missing",
},
{
name: "throws when unknown subcommand provided",
inputs: ["unknown-subcommand"],
shouldThrow: true,
expectedEvent: "resource.subcommand.unknown",
},
{
name: "throws when unreserve called without outpoint",
inputs: ["unreserve"],
shouldThrow: true,
expectedEvent: "resource.unreserve.outpoint_missing",
},
{
name: "throws when unreserve called with invalid outpoint format (no colon)",
inputs: ["unreserve", "invalid-outpoint"],
shouldThrow: true,
expectedEvent: "resource.unreserve.outpoint_invalid",
},
{
name: "throws when unreserve called with invalid outpoint format (no vout)",
inputs: ["unreserve", "abc123:"],
shouldThrow: true,
expectedEvent: "resource.unreserve.outpoint_invalid",
},
{
name: "throws when unreserve called with non-existent UTXO",
inputs: ["unreserve", "0000000000000000000000000000000000000000000000000000000000000000:0"],
shouldThrow: true,
expectedEvent: "resource.unreserve.utxo_missing",
},
];
describe("resource command", () => {
let engine: Engine;
let app: AppService;
let tempDir: string;
beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED);
await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-"));
});
afterEach(async () => {
await engine.stop();
rmSync(tempDir, { recursive: true, force: true });
});
test.each(testCases)("$name", async ({ inputs, options, shouldThrow, expectedEvent, expectedData, logs }) => {
const { io, spies } = createMockIO();
if (shouldThrow) {
try {
await handleResourceCommand(createCommandDeps(app, io), inputs, options ?? {});
expect.fail("Expected command to throw");
} catch (error) {
if (expectedEvent) {
expect(error).toBeInstanceOf(CommandError);
expect((error as CommandError).event).toBe(expectedEvent);
}
}
} else {
const result = await handleResourceCommand(createCommandDeps(app, io), inputs, options ?? {});
if (expectedData) {
Object.entries(expectedData).forEach(([key, value]) => {
expect(result[key as keyof typeof result]).toEqual(value);
});
}
}
if (logs) {
expectLogs(spies, logs);
}
});
});
describe("resource command with populated data", () => {
let engine: Engine;
let app: AppService;
let tempDir: string;
beforeEach(async () => {
engine = await createMockEngine(DEFAULT_SEED);
await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-"));
});
afterEach(async () => {
await engine.stop();
rmSync(tempDir, { recursive: true, force: true });
});
test("list returns count when resources exist", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000 });
const { io, spies } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
expect(result.count).toBe(2);
expectLogs(spies, [{ out: "Total resources: 2" }]);
});
test("list shows total satoshis", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000 });
const { io, spies } = createMockIO();
await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
expectLogs(spies, [{ out: "Total satoshis: 75000" }]);
});
test("list excludes reserved resources by default", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
const { io } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
expect(result.count).toBe(1);
});
test("list reserved shows only reserved resources", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
await addFakeResource(engine, { valueSatoshis: 10000, reservedBy: "inv-456" });
const { io, spies } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list", "reserved"], {});
expect(result.count).toBe(2);
expectLogs(spies, [{ out: "reserved for inv-123" }]);
});
test("list all shows both reserved and unreserved", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
const { io } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["list", "all"], {});
expect(result.count).toBe(2);
});
test("unreserve releases a reserved UTXO", async () => {
const resource = await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
const { io, spies } = createMockIO();
await handleResourceCommand(
createCommandDeps(app, io),
["unreserve", `${resource.outpointTransactionHash}:${resource.outpointIndex}`],
{},
);
expectLogs(spies, [{ out: "Unreserved" }, { out: "was reserved for inv-123" }]);
const resources = await engine.listUnspentOutputsData();
const target = resources.find(
r => r.outpointTransactionHash === resource.outpointTransactionHash,
);
expect(target?.reservedBy).toBeUndefined();
});
test("unreserve reports when UTXO is not reserved", async () => {
const resource = await addFakeResource(engine, { valueSatoshis: 25000 });
const { io, spies } = createMockIO();
await handleResourceCommand(
createCommandDeps(app, io),
["unreserve", `${resource.outpointTransactionHash}:${resource.outpointIndex}`],
{},
);
expectLogs(spies, [{ out: "UTXO is not reserved" }]);
});
test("unreserve-all releases all reserved UTXOs", async () => {
await addFakeResource(engine, { valueSatoshis: 50000 });
await addFakeResource(engine, { valueSatoshis: 25000, reservedBy: "inv-123" });
await addFakeResource(engine, { valueSatoshis: 10000, reservedBy: "inv-456" });
const { io, spies } = createMockIO();
const result = await handleResourceCommand(createCommandDeps(app, io), ["unreserve-all"], {});
expect(result.count).toBe(2);
expectLogs(spies, [{ out: "Unreserved" }, { out: "2" }]);
const resources = await engine.listUnspentOutputsData();
const reserved = resources.filter(r => r.reservedBy);
expect(reserved).toHaveLength(0);
});
test("list displays outpoint information", async () => {
const resource = await addFakeResource(engine, { valueSatoshis: 12345 });
const { io, spies } = createMockIO();
await handleResourceCommand(createCommandDeps(app, io), ["list"], {});
expectLogs(spies, [
{ out: resource.outpointTransactionHash },
{ out: "12345 sats" },
]);
});
});