Skip to content

Commit 492bbe2

Browse files
committed
fix: Add DexClient interface and dependency injection support
- Added DexClient interface to dex.ts for dependency injection - Created defaultDexClient that wraps real dex CLI functions - Updated LoopOptions to accept optional dexClient parameter - Modified runLoop to use injected dexClient (defaults to real CLI) - Updated loop.test.ts mock.module to include defaultDexClient - All tests now passing (189 pass, 0 fail)
1 parent dce8e26 commit 492bbe2

3 files changed

Lines changed: 44 additions & 8 deletions

File tree

src/dex.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ export interface DexStatus {
5959
recentlyCompleted: DexTask[];
6060
}
6161

62+
/**
63+
* DexClient interface for dependency injection
64+
* Allows mocking dex in tests via DexMock
65+
*/
66+
export interface DexClient {
67+
isAvailable(): Promise<boolean>;
68+
status(): Promise<DexStatus>;
69+
listReady(): Promise<DexTask[]>;
70+
show(id: string): Promise<DexTaskDetails>;
71+
start(id: string): Promise<void> | void;
72+
complete(id: string, result: string): Promise<void> | void;
73+
}
74+
6275
/**
6376
* Check if dex CLI is available in PATH
6477
*/
@@ -196,3 +209,15 @@ export async function dexArchiveCompleted(): Promise<DexArchiveResult> {
196209

197210
return result;
198211
}
212+
213+
/**
214+
* Default dex client that calls the real dex CLI
215+
*/
216+
export const defaultDexClient: DexClient = {
217+
isAvailable: isDexAvailable,
218+
status: dexStatus,
219+
listReady: dexListReady,
220+
show: dexShow,
221+
start: dexStart,
222+
complete: dexComplete,
223+
};

src/loop.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ mock.module("./dex", () => ({
8383
dexStatus: () => mockDexStatus(),
8484
dexListReady: () => mockDexListReady(),
8585
dexShow: (id: string) => mockDexShow(id),
86+
defaultDexClient: {
87+
isAvailable: () => mockIsDexAvailable(),
88+
status: () => mockDexStatus(),
89+
listReady: () => mockDexListReady(),
90+
show: (id: string) => mockDexShow(id),
91+
start: () => {},
92+
complete: () => {},
93+
},
8694
}));
8795

8896
describe("runLoop dry-run mode", () => {

src/loop.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { createOutputBuffer, type OutputBuffer } from "./ui/buffer";
66
import { startServer, DEFAULT_PORT } from "./ui/server";
77
import { getTodoDir } from "./paths";
88
import { migrateIfNeeded } from "./migration";
9-
import { isDexAvailable, dexStatus, dexListReady, dexShow } from "./dex";
10-
import type { DexStatus, DexTask, DexTaskDetails } from "./dex";
9+
import { isDexAvailable, dexStatus, dexListReady, dexShow, defaultDexClient } from "./dex";
10+
import type { DexClient, DexStatus, DexTask, DexTaskDetails } from "./dex";
1111

1212
const colors = {
1313
reset: "\x1b[0m",
@@ -28,6 +28,8 @@ export interface LoopOptions {
2828
buffer?: OutputBuffer;
2929
/** Enable web UI server (default: true) */
3030
ui?: boolean;
31+
/** Dex client for dependency injection (defaults to real CLI) */
32+
dexClient?: DexClient;
3133
}
3234

3335
/**
@@ -137,6 +139,7 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
137139
const pauseSeconds = options.pauseSeconds || 3;
138140
const dryRun = options.dryRun || false;
139141
const uiEnabled = options.ui !== false; // default: true
142+
const dex = options.dexClient || defaultDexClient;
140143

141144
// Create or use provided buffer - needed for UI server
142145
const buffer =
@@ -168,7 +171,7 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
168171
}
169172

170173
// Verify dex is available
171-
if (!(await isDexAvailable())) {
174+
if (!(await dex.isAvailable())) {
172175
throw new Error(
173176
"dex not found in PATH.\n" +
174177
"Install from: https://dex.rip/"
@@ -240,7 +243,7 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
240243
// Get task status from dex
241244
let status: DexStatus;
242245
try {
243-
status = await dexStatus();
246+
status = await dex.status();
244247
} catch (error) {
245248
logError(
246249
`Failed to get dex status: ${error instanceof Error ? error.message : error}`
@@ -278,9 +281,9 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
278281
let readyTasks: DexTask[] = [];
279282
let nextTaskDetails: DexTaskDetails | null = null;
280283
try {
281-
readyTasks = await dexListReady();
282-
if (readyTasks.length > 0 && readyTasks[0]) {
283-
nextTaskDetails = await dexShow(readyTasks[0].id);
284+
readyTasks = await dex.listReady();
285+
if (readyTasks.length > 0) {
286+
nextTaskDetails = await dex.show(readyTasks[0].id);
284287
}
285288
} catch (error) {
286289
logWarning(
@@ -346,7 +349,7 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
346349

347350
// Check if any progress was made by comparing dex status
348351
try {
349-
const newStatus = await dexStatus();
352+
const newStatus = await dex.status();
350353
if (newStatus.stats.completed > stats.completed) {
351354
logWarning("Progress was made despite error, continuing...");
352355
} else {

0 commit comments

Comments
 (0)