feat: add per-provider timeout for usage panel to prevent blocking on slow responses
fix: update scheduler filesystem path from .kb to .fusion for task validation style: clean up TaskDetailModal styling with reusable CSS classes test: add comprehensive tests for file-service operations with mocked filesystem
This commit is contained in:
@@ -94,7 +94,7 @@ function createMockServer(portToReturn: number = 0) {
|
||||
return server;
|
||||
}
|
||||
|
||||
const mockListen = vi.fn((port: number) => {
|
||||
const mockListen = vi.fn<(port: number) => EventEmitter<DefaultEventMap> & { listen: ReturnType<typeof vi.fn>; address: ReturnType<typeof vi.fn>; close: ReturnType<typeof vi.fn> }>((port: number) => {
|
||||
const server = createMockServer(port);
|
||||
process.nextTick(() => server.emit("listening"));
|
||||
return server;
|
||||
@@ -460,7 +460,7 @@ describe("runDashboard — PR-first auto-merge queue", () => {
|
||||
});
|
||||
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
});
|
||||
|
||||
it("uses PR lifecycle instead of aiMergeTask when mergeStrategy is pull-request", async () => {
|
||||
@@ -485,7 +485,7 @@ describe("runDashboard — WorktreePool wiring", () => {
|
||||
resetGitHubMocks();
|
||||
// Re-set TaskStore mock (clearAllMocks wipes implementations)
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
|
||||
// Re-set engine mocks
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
@@ -553,7 +553,7 @@ describe("runDashboard — auto-merge pause exclusion", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
@@ -631,7 +631,7 @@ describe("runDashboard — immediate resume on unpause", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
@@ -737,7 +737,7 @@ describe("runDashboard — engine pause/unpause cycle", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
@@ -778,7 +778,7 @@ describe("runDashboard — port fallback on EADDRINUSE", () => {
|
||||
vi.clearAllMocks();
|
||||
resetGitHubMocks();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => makeMockStore());
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.TaskExecutor as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
() => ({ resumeOrphaned: vi.fn().mockResolvedValue(undefined) }),
|
||||
@@ -896,7 +896,7 @@ describe("runDashboard — enginePaused (soft pause)", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
@@ -1011,7 +1011,7 @@ describe("runDashboard — --paused flag", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
@@ -1072,7 +1072,7 @@ describe("runDashboard — --paused flag", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.TaskExecutor as unknown as ReturnType<typeof vi.fn>).mockImplementation(
|
||||
() => ({ resumeOrphaned: vi.fn().mockResolvedValue(undefined) }),
|
||||
@@ -1125,7 +1125,7 @@ describe("runDashboard — --dev mode", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
const engine = await import("@fusion/engine");
|
||||
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
|
||||
Promise.resolve({ merged: true }),
|
||||
@@ -1239,7 +1239,7 @@ describe("runDashboard — merge conflict retry logic", () => {
|
||||
resetGitHubMocks();
|
||||
mockStore = makeMockStore();
|
||||
const { TaskStore } = await import("@fusion/core");
|
||||
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
|
||||
|
||||
// Default mock store.getTask implementation
|
||||
mockStore.getTask = vi.fn().mockImplementation(async (id: string) => ({
|
||||
@@ -1573,16 +1573,16 @@ describe("promptForPort", () => {
|
||||
vi.mocked(createInterface).mockReturnValue(mockRl as unknown as ReturnType<typeof createInterface>);
|
||||
|
||||
// Simulate that the promise rejects when SIGINT is triggered
|
||||
const removeListenerSpy = vi.spyOn(process, "removeListener").mockImplementation(() => {});
|
||||
const removeListenerSpy = vi.spyOn(process, "removeListener").mockImplementation(() => process as unknown as void);
|
||||
|
||||
// Trigger SIGINT handler immediately to test rejection
|
||||
let sigintHandler: (() => void) | null = null;
|
||||
const onSpy = vi.spyOn(process, "on").mockImplementation((event: string, handler: (...args: unknown[]) => void) => {
|
||||
const onSpy = vi.spyOn(process, "on" as never).mockImplementation(((event: string, handler: (...args: unknown[]) => void) => {
|
||||
if (event === "SIGINT") {
|
||||
sigintHandler = handler as () => void;
|
||||
}
|
||||
return process;
|
||||
});
|
||||
}) as never);
|
||||
|
||||
mockRl.question.mockImplementation(() => {
|
||||
// Simulate SIGINT during prompt
|
||||
|
||||
@@ -184,7 +184,7 @@ export async function processPullRequestMergeTask(
|
||||
return "merged";
|
||||
}
|
||||
|
||||
export async function runDashboard(port: number, opts: { paused?: boolean; dev?: boolean; interactive?: boolean } = {}) {
|
||||
export async function runDashboard(port: number, opts: { paused?: boolean; dev?: boolean; interactive?: boolean; open?: boolean } = {}) {
|
||||
// Handle interactive port selection
|
||||
let selectedPort = port;
|
||||
if (opts.interactive) {
|
||||
|
||||
@@ -2652,6 +2652,10 @@ ${stepsSection}`;
|
||||
return this.rootDir;
|
||||
}
|
||||
|
||||
getTasksDir(): string {
|
||||
return this.tasksDir;
|
||||
}
|
||||
|
||||
private generateSpecifiedPrompt(task: Task): string {
|
||||
const deps =
|
||||
task.dependencies.length > 0
|
||||
|
||||
@@ -35,9 +35,9 @@ describe("column fixed-width CSS", () => {
|
||||
});
|
||||
|
||||
describe("desktop .board grid template", () => {
|
||||
it("uses repeat(6, minmax(280px, 1fr)) for 6 columns", () => {
|
||||
it("uses repeat(6, minmax(260px, 1fr)) for 6 columns", () => {
|
||||
expect(css).toContain(
|
||||
"grid-template-columns: repeat(6, minmax(280px, 1fr))",
|
||||
"grid-template-columns: repeat(6, minmax(260px, 1fr))",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1123,28 +1123,34 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
)}
|
||||
|
||||
{/* Mission items */}
|
||||
{missions.map((mission) => (
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{missions.map((mission: any) => {
|
||||
const m = mission as { id: string; title: string; description?: string; status: string };
|
||||
const selId = selectedMission as { id: string } | null;
|
||||
const isSelected = selId && selId.id === m.id;
|
||||
const statusColors = missionStatusColors[m.status as MissionStatus] || { bg: "", text: "" };
|
||||
return (
|
||||
<div
|
||||
key={mission.id}
|
||||
className={`mission-item ${selectedMission?.id === mission.id ? "selected" : ""}`}
|
||||
key={m.id}
|
||||
className={`mission-item ${isSelected ? "selected" : ""}`}
|
||||
onClick={() => handleSelectMission(mission)}
|
||||
>
|
||||
<div className="mission-item-content">
|
||||
<div className="mission-item-header">
|
||||
<Target size={16} />
|
||||
<span className="mission-item-title">{mission.title}</span>
|
||||
<span className="mission-item-title">{m.title}</span>
|
||||
<span
|
||||
className="status-badge small"
|
||||
style={{
|
||||
backgroundColor: missionStatusColors[mission.status].bg,
|
||||
color: missionStatusColors[mission.status].text,
|
||||
backgroundColor: statusColors.bg,
|
||||
color: statusColors.text,
|
||||
}}
|
||||
>
|
||||
{mission.status}
|
||||
{m.status}
|
||||
</span>
|
||||
</div>
|
||||
{mission.description && (
|
||||
<p className="mission-item-description">{mission.description}</p>
|
||||
{m.description && (
|
||||
<p className="mission-item-description">{m.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mission-item-actions" onClick={(e) => e.stopPropagation()}>
|
||||
@@ -1157,14 +1163,15 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
</button>
|
||||
<button
|
||||
className="icon-btn small danger"
|
||||
onClick={() => setDeleteConfirmId({ type: "mission", id: mission.id })}
|
||||
onClick={() => setDeleteConfirmId({ type: "mission", id: m.id })}
|
||||
title="Delete mission"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Edit mission form */}
|
||||
{editingMissionId && (
|
||||
|
||||
@@ -176,7 +176,7 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
</span>
|
||||
<span
|
||||
className="changes-file-stat"
|
||||
title={`+${fileEntry.additions} -${fileEntry.deletions}${fileEntry.size ? ` · ${formatFileSize(fileEntry.size)}` : ""}`}
|
||||
title={`+${fileEntry.additions} -${fileEntry.deletions}`}
|
||||
>
|
||||
+{fileEntry.additions} -{fileEntry.deletions}
|
||||
</span>
|
||||
|
||||
@@ -770,7 +770,7 @@ export function TaskDetailModal({
|
||||
</div>
|
||||
<div className="detail-section">
|
||||
{!isEditingSpec && (
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<div className="detail-spec-edit-trigger">
|
||||
<button className="btn btn-sm" onClick={enterSpecEditMode}>
|
||||
Edit
|
||||
</button>
|
||||
@@ -848,19 +848,11 @@ export function TaskDetailModal({
|
||||
<div className="detail-section">
|
||||
<h4>Attachments</h4>
|
||||
{attachments.length > 0 ? (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "12px", marginBottom: "8px" }}>
|
||||
<div className="detail-attachments-grid">
|
||||
{attachments.map((a) => (
|
||||
<div
|
||||
key={a.filename}
|
||||
style={{
|
||||
position: "relative",
|
||||
border: "1px solid var(--border, #333)",
|
||||
borderRadius: "6px",
|
||||
padding: "4px",
|
||||
background: "var(--bg-secondary, #1a1a2e)",
|
||||
}}
|
||||
>
|
||||
<div key={a.filename} className="detail-attachment-card">
|
||||
<a
|
||||
className="detail-attachment-link"
|
||||
href={`/api/tasks/${task.id}/attachments/${a.filename}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -868,30 +860,15 @@ export function TaskDetailModal({
|
||||
<img
|
||||
src={`/api/tasks/${task.id}/attachments/${a.filename}`}
|
||||
alt={a.originalName}
|
||||
style={{ maxWidth: "150px", maxHeight: "100px", display: "block", borderRadius: "4px" }}
|
||||
className="detail-attachment-image"
|
||||
/>
|
||||
</a>
|
||||
<div style={{ fontSize: "11px", marginTop: "4px", opacity: 0.7 }}>
|
||||
<div className="detail-attachment-meta">
|
||||
{a.originalName} ({formatBytes(a.size)})
|
||||
</div>
|
||||
<button
|
||||
className="detail-attachment-delete"
|
||||
onClick={() => handleDeleteAttachment(a.filename)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "2px",
|
||||
right: "2px",
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "50%",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
lineHeight: "20px",
|
||||
textAlign: "center",
|
||||
padding: 0,
|
||||
}}
|
||||
title="Delete attachment"
|
||||
>
|
||||
×
|
||||
@@ -900,14 +877,14 @@ export function TaskDetailModal({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ opacity: 0.5, marginBottom: "8px" }}>(no attachments)</div>
|
||||
<div className="detail-empty-inline">(no attachments)</div>
|
||||
)}
|
||||
<input
|
||||
className="detail-hidden-file-input"
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleUpload}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
@@ -942,15 +919,6 @@ export function TaskDetailModal({
|
||||
className="dep-remove-btn"
|
||||
onClick={(e) => handleRemoveDep(e, dep)}
|
||||
title={`Remove dependency ${dep}`}
|
||||
style={{
|
||||
marginLeft: "6px",
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--text-secondary, #888)",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
padding: "0 4px",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -958,9 +926,9 @@ export function TaskDetailModal({
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div style={{ opacity: 0.5, marginBottom: "8px" }}>(no dependencies)</div>
|
||||
<div className="detail-empty-inline">(no dependencies)</div>
|
||||
)}
|
||||
<div className="dep-trigger-wrap" style={{ position: "relative" }}>
|
||||
<div className="dep-trigger-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm dep-trigger"
|
||||
@@ -1070,7 +1038,7 @@ export function TaskDetailModal({
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<div style={{ flex: 1 }} />
|
||||
<div className="modal-actions-spacer" />
|
||||
{task.column === "in-review" ? (
|
||||
<>
|
||||
<button className="btn btn-sm" onClick={() => handleMove("in-progress")}>
|
||||
@@ -1096,56 +1064,33 @@ export function TaskDetailModal({
|
||||
</div>
|
||||
{showRefineModal && (
|
||||
<div
|
||||
className="modal-overlay open"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(0, 0, 0, 0.7)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 100,
|
||||
}}
|
||||
className="modal-overlay open detail-refine-overlay"
|
||||
onClick={handleCloseRefineModal}
|
||||
>
|
||||
<div
|
||||
className="modal"
|
||||
style={{ maxWidth: "500px", width: "90%", margin: "0" }}
|
||||
className="modal detail-refine-modal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h3 style={{ margin: 0 }}>Refine</h3>
|
||||
<h3 className="detail-refine-title">Refine</h3>
|
||||
<button className="modal-close" onClick={handleCloseRefineModal}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="detail-body">
|
||||
<p style={{ marginBottom: "12px", opacity: 0.8 }}>
|
||||
<p className="detail-refine-help">
|
||||
Describe what needs to be refined or improved...
|
||||
</p>
|
||||
<textarea
|
||||
className="detail-refine-textarea"
|
||||
value={refineFeedback}
|
||||
onChange={(e) => setRefineFeedback(e.target.value)}
|
||||
placeholder="Enter your feedback here..."
|
||||
rows={6}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid var(--border, #30363d)",
|
||||
background: "var(--bg-primary, #0d1117)",
|
||||
color: "var(--text-primary, #c9d1d9)",
|
||||
fontSize: "14px",
|
||||
resize: "vertical",
|
||||
minHeight: "120px",
|
||||
}}
|
||||
maxLength={2000}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ marginTop: "8px", textAlign: "right", fontSize: "12px", opacity: 0.6 }}>
|
||||
<div className="detail-refine-char-count">
|
||||
{refineFeedback.length}/2000 characters
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1146,10 +1146,10 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
const actions = container.querySelector(".modal-actions");
|
||||
expect(actions).toBeTruthy();
|
||||
// Spacer div with flex: 1 separates left actions from right actions
|
||||
const spacer = actions!.querySelector("div");
|
||||
// Spacer div separates left actions from right actions via CSS class
|
||||
const spacer = actions!.querySelector(".modal-actions-spacer");
|
||||
expect(spacer).toBeTruthy();
|
||||
expect((spacer as HTMLElement).style.flex).toContain("1");
|
||||
expect((spacer as HTMLElement).className).toContain("modal-actions-spacer");
|
||||
});
|
||||
|
||||
it("tab buttons use CSS classes instead of inline styles for responsive override", () => {
|
||||
|
||||
69
packages/dashboard/src/__tests__/debug.test.ts
Normal file
69
packages/dashboard/src/__tests__/debug.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Use vi.mock with __mocks__ pattern
|
||||
vi.mock("node:fs/promises", () => {
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", () => {
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import file-service
|
||||
import { listProjectFiles } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
|
||||
mocks.mockStat.mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
});
|
||||
mocks.mockReaddir.mockResolvedValue([]);
|
||||
|
||||
console.log("Calling listProjectFiles...");
|
||||
try {
|
||||
const result = await listProjectFiles(mockStore, "./src");
|
||||
console.log("Result:", result);
|
||||
console.log("mockStat calls:", mocks.mockStat.mock.calls);
|
||||
expect(result.path).toBe("src");
|
||||
expect(result.entries).toEqual([]);
|
||||
} catch (e) {
|
||||
console.log("Error:", e);
|
||||
console.log("mockStat calls:", mocks.mockStat.mock.calls);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
80
packages/dashboard/src/__tests__/debug10.test.ts
Normal file
80
packages/dashboard/src/__tests__/debug10.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
const basePath = resolve("/test/project");
|
||||
const filePath = "file.txt/sub.txt";
|
||||
const resolvedPath = resolve(basePath, filePath);
|
||||
const parentDir = dirname(resolvedPath);
|
||||
|
||||
// Test the mock directly
|
||||
mocks.mockStat.mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
});
|
||||
|
||||
const result = await mocks.mockStat("/test/some/path");
|
||||
console.log("Direct mock result:", result);
|
||||
console.log("Direct mock isDirectory():", result.isDirectory());
|
||||
|
||||
// Now test writeProjectFile
|
||||
try {
|
||||
await writeProjectFile(mockStore, filePath, "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
93
packages/dashboard/src/__tests__/debug11.test.ts
Normal file
93
packages/dashboard/src/__tests__/debug11.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// Track all stat calls
|
||||
let callCount = 0;
|
||||
mocks.mockStat.mockImplementation(async (path: string) => {
|
||||
callCount++;
|
||||
console.log(`stat call #${callCount}:`, path);
|
||||
|
||||
if (callCount === 1) {
|
||||
// First call - file doesn't exist
|
||||
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
||||
}
|
||||
if (callCount === 2) {
|
||||
// Second call - parent is a file
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
};
|
||||
}
|
||||
if (callCount === 3) {
|
||||
// Third call - after write
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 100,
|
||||
mtime: new Date(),
|
||||
};
|
||||
}
|
||||
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
81
packages/dashboard/src/__tests__/debug12.test.ts
Normal file
81
packages/dashboard/src/__tests__/debug12.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
let callCount = 0;
|
||||
mocks.mockStat.mockImplementation(async (path: string) => {
|
||||
callCount++;
|
||||
console.log(`stat call #${callCount}:`, path);
|
||||
|
||||
// Return a simple object
|
||||
const result = {
|
||||
isDirectory: () => path.includes("/file.txt"),
|
||||
isFile: () => !path.includes("/file.txt"),
|
||||
};
|
||||
|
||||
console.log(`stat #${callCount} returning:`, result);
|
||||
console.log(`stat #${callCount} isDirectory():`, result.isDirectory());
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
80
packages/dashboard/src/__tests__/debug13.test.ts
Normal file
80
packages/dashboard/src/__tests__/debug13.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test - parent is not a directory", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// First call: file doesn't exist (throw ENOENT)
|
||||
// Second call: parent is a file, not a directory (return object with isDirectory: false)
|
||||
// Third call: after write, get file stats
|
||||
mocks.mockStat
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }) // First: file doesn't exist
|
||||
.mockResolvedValueOnce({ // Second: parent is a file
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
})
|
||||
.mockResolvedValueOnce({ // Third: after write
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 100,
|
||||
mtime: new Date(),
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
expect(e.message).toContain("Parent is not a directory");
|
||||
}
|
||||
});
|
||||
});
|
||||
79
packages/dashboard/src/__tests__/debug14.test.ts
Normal file
79
packages/dashboard/src/__tests__/debug14.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
mocks.mockStat.mockImplementation(async (path: string) => {
|
||||
console.log("stat called:", path);
|
||||
throw { code: "ENOENT" };
|
||||
});
|
||||
});
|
||||
|
||||
it("test - parent is not a directory", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// First call: file doesn't exist (throw ENOENT)
|
||||
// Second call: parent is a file, not a directory (return object with isDirectory: false)
|
||||
mocks.mockStat
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }) // First: file doesn't exist
|
||||
.mockImplementation(async (path: string) => { // Subsequent calls
|
||||
console.log("Second stat call:", path);
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
63
packages/dashboard/src/__tests__/debug2.test.ts
Normal file
63
packages/dashboard/src/__tests__/debug2.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { writeProjectFile } from "../file-service";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs/promises", () => {
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", () => {
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
describe("debug", () => {
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
mocks.mockStat
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }) // File doesn't exist
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
}); // Parent is a file
|
||||
|
||||
console.log("mockStat calls before:", mocks.mockStat.mock.calls);
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
} catch (e) {
|
||||
console.log("Error:", e);
|
||||
console.log("mockStat calls after:", mocks.mockStat.mock.calls);
|
||||
}
|
||||
});
|
||||
});
|
||||
68
packages/dashboard/src/__tests__/debug3.test.ts
Normal file
68
packages/dashboard/src/__tests__/debug3.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { writeProjectFile } from "../file-service";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs/promises", () => {
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", () => {
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
describe("debug", () => {
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// Setup mock to return specific values for each call
|
||||
mocks.mockStat.mockImplementation((path: string) => {
|
||||
console.log("stat called with:", path);
|
||||
if (path === "/test/project/file.txt/sub.txt") {
|
||||
return Promise.reject({ code: "ENOENT" });
|
||||
}
|
||||
if (path === "/test/project/file.txt") {
|
||||
return Promise.resolve({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
});
|
||||
}
|
||||
return Promise.reject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
} catch (e) {
|
||||
console.log("Error:", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
78
packages/dashboard/src/__tests__/debug4.test.ts
Normal file
78
packages/dashboard/src/__tests__/debug4.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// Setup mock to return specific values for each call
|
||||
mocks.mockStat.mockImplementation((path: string) => {
|
||||
console.log("stat called with:", path);
|
||||
if (path === "/test/project/file.txt/sub.txt") {
|
||||
return Promise.reject({ code: "ENOENT" });
|
||||
}
|
||||
if (path === "/test/project/file.txt") {
|
||||
return Promise.resolve({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
});
|
||||
}
|
||||
return Promise.reject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
77
packages/dashboard/src/__tests__/debug5.test.ts
Normal file
77
packages/dashboard/src/__tests__/debug5.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
mocks.mockStat.mockImplementation((path: string) => {
|
||||
console.log("stat called with:", path);
|
||||
if (path === "/test/project/file.txt/sub.txt") {
|
||||
return Promise.reject({ code: "ENOENT" });
|
||||
}
|
||||
if (path === "/test/project/file.txt") {
|
||||
return Promise.resolve({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
});
|
||||
}
|
||||
return Promise.reject({ code: "ENOENT" });
|
||||
});
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
expect(e.message).toContain("Parent is not a directory");
|
||||
}
|
||||
});
|
||||
});
|
||||
74
packages/dashboard/src/__tests__/debug6.test.ts
Normal file
74
packages/dashboard/src/__tests__/debug6.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// Setup mock
|
||||
mocks.mockStat
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }) // First call - file doesn't exist
|
||||
.mockResolvedValueOnce({ // Second call - parent is a file
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
});
|
||||
|
||||
console.log("Before call");
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
console.log("mockStat calls:", mocks.mockStat.mock.calls);
|
||||
}
|
||||
});
|
||||
});
|
||||
80
packages/dashboard/src/__tests__/debug7.test.ts
Normal file
80
packages/dashboard/src/__tests__/debug7.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// Setup mock to return specific values
|
||||
mocks.mockStat.mockImplementation(async (path: string) => {
|
||||
console.log("stat called:", path);
|
||||
if (path === "/test/project/file.txt/sub.txt") {
|
||||
throw { code: "ENOENT" };
|
||||
}
|
||||
if (path === "/test/project/file.txt") {
|
||||
const result = {
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
};
|
||||
console.log("Returning for parent:", result);
|
||||
return result;
|
||||
}
|
||||
throw { code: "ENOENT" };
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, "file.txt/sub.txt", "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
93
packages/dashboard/src/__tests__/debug8.test.ts
Normal file
93
packages/dashboard/src/__tests__/debug8.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { join, resolve, dirname } from "path";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
// Log what paths will be used
|
||||
const basePath = resolve("/test/project");
|
||||
const filePath = "file.txt/sub.txt";
|
||||
const resolvedPath = resolve(basePath, filePath);
|
||||
const parentDir = dirname(resolvedPath);
|
||||
|
||||
console.log("basePath:", basePath);
|
||||
console.log("filePath:", filePath);
|
||||
console.log("resolvedPath:", resolvedPath);
|
||||
console.log("parentDir:", parentDir);
|
||||
|
||||
// Setup mock to return specific values
|
||||
mocks.mockStat.mockImplementation(async (path: string) => {
|
||||
console.log("stat called:", path);
|
||||
if (path === resolvedPath) {
|
||||
console.log("File path match!");
|
||||
throw { code: "ENOENT" };
|
||||
}
|
||||
if (path === parentDir) {
|
||||
console.log("Parent dir match!");
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
};
|
||||
}
|
||||
console.log("No match for:", path);
|
||||
throw { code: "ENOENT" };
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, filePath, "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
93
packages/dashboard/src/__tests__/debug9.test.ts
Normal file
93
packages/dashboard/src/__tests__/debug9.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
// Create mock functions that can be configured in tests
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Hoist mocks alongside vi.mock
|
||||
vi.mock("node:fs/promises", async () => {
|
||||
const actual = await import("node:fs/promises");
|
||||
return {
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs", async () => {
|
||||
const actual = await import("node:fs");
|
||||
return {
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Import AFTER mocks are set up
|
||||
import { writeProjectFile } from "../file-service";
|
||||
|
||||
describe("debug", () => {
|
||||
beforeEach(() => {
|
||||
mocks.mockStat.mockReset();
|
||||
});
|
||||
|
||||
it("test", async () => {
|
||||
const mockGetRootDir = vi.fn();
|
||||
const mockStore = {
|
||||
getRootDir: mockGetRootDir,
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
|
||||
const basePath = resolve("/test/project");
|
||||
const filePath = "file.txt/sub.txt";
|
||||
const resolvedPath = resolve(basePath, filePath);
|
||||
const parentDir = dirname(resolvedPath);
|
||||
|
||||
// Create a proper stats object
|
||||
const fileNotDirStats = {
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 100,
|
||||
mtime: new Date(),
|
||||
};
|
||||
|
||||
// Setup mock - note: use .mockResolvedValue for promises
|
||||
mocks.mockStat.mockImplementation(async (path: string) => {
|
||||
console.log("stat called:", path);
|
||||
if (path === resolvedPath) {
|
||||
console.log("File path - throwing ENOENT");
|
||||
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
||||
}
|
||||
if (path === parentDir) {
|
||||
console.log("Parent dir - returning file stats");
|
||||
console.log("Returning:", fileNotDirStats);
|
||||
console.log("isDirectory():", fileNotDirStats.isDirectory());
|
||||
return fileNotDirStats;
|
||||
}
|
||||
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
try {
|
||||
await writeProjectFile(mockStore, filePath, "content");
|
||||
console.log("Success!");
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -14,34 +14,44 @@ import {
|
||||
} from "../file-service.js";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
|
||||
// Mock node:fs/promises
|
||||
const mockReaddir = vi.fn();
|
||||
const mockReadFile = vi.fn();
|
||||
const mockWriteFile = vi.fn();
|
||||
const mockStat = vi.fn();
|
||||
// Create mock functions that can be configured in tests
|
||||
// Use vi.hoisted to hoist alongside vi.mock
|
||||
const mocks = vi.hoisted(() => ({
|
||||
mockReaddir: vi.fn(),
|
||||
mockReadFile: vi.fn(),
|
||||
mockWriteFile: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockExistsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
||||
// Hoist mocks alongside vi.mock - must include default export
|
||||
vi.mock("node:fs/promises", () => {
|
||||
return {
|
||||
...actual,
|
||||
readdir: (...args: any[]) => mockReaddir(...args),
|
||||
readFile: (...args: any[]) => mockReadFile(...args),
|
||||
writeFile: (...args: any[]) => mockWriteFile(...args),
|
||||
stat: (...args: any[]) => mockStat(...args),
|
||||
default: {
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
},
|
||||
readdir: mocks.mockReaddir,
|
||||
readFile: mocks.mockReadFile,
|
||||
writeFile: mocks.mockWriteFile,
|
||||
stat: mocks.mockStat,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock node:fs
|
||||
const mockExistsSync = vi.fn();
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
vi.mock("node:fs", () => {
|
||||
return {
|
||||
...actual,
|
||||
existsSync: (...args: any[]) => mockExistsSync(...args),
|
||||
default: {
|
||||
existsSync: mocks.mockExistsSync,
|
||||
},
|
||||
existsSync: mocks.mockExistsSync,
|
||||
};
|
||||
});
|
||||
|
||||
// Export mocks for use in tests
|
||||
export const { mockReaddir, mockReadFile, mockWriteFile, mockStat, mockExistsSync } = mocks;
|
||||
|
||||
describe("FileServiceError", () => {
|
||||
it("constructor sets code and name correctly", () => {
|
||||
const error = new FileServiceError("Test message", "ETEST");
|
||||
@@ -227,15 +237,16 @@ describe("path traversal protection", () => {
|
||||
|
||||
it("allows valid relative paths with dots", async () => {
|
||||
mockGetRootDir.mockReturnValue("/project");
|
||||
// Need successful stat for this test to pass validation
|
||||
mockStat.mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
});
|
||||
mockReaddir.mockResolvedValue([]);
|
||||
|
||||
// This should NOT throw
|
||||
await expect(listProjectFiles(mockStore, "./src")).resolves.not.toThrow();
|
||||
// Should resolve ./src to src and succeed
|
||||
const result = await listProjectFiles(mockStore, "./src");
|
||||
expect(result.path).toBe("src");
|
||||
expect(result.entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("allows paths containing single dots in middle", async () => {
|
||||
@@ -246,7 +257,10 @@ describe("path traversal protection", () => {
|
||||
});
|
||||
mockReaddir.mockResolvedValue([]);
|
||||
|
||||
await expect(listProjectFiles(mockStore, "src/./components")).resolves.not.toThrow();
|
||||
// Should resolve src/./components to src/components and succeed
|
||||
const result = await listProjectFiles(mockStore, "src/./components");
|
||||
expect(result.path).toBe("src/components");
|
||||
expect(result.entries).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -360,9 +374,8 @@ describe("writeProjectFile", () => {
|
||||
mockGetRootDir.mockReturnValue("/test/project");
|
||||
mockStat
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }) // File doesn't exist
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }); // Parent doesn't exist
|
||||
.mockRejectedValueOnce({ code: "ENOENT" }); // Parent doesn't exist (this should throw)
|
||||
|
||||
await expect(writeProjectFile(mockStore, "missing/file.txt", "content")).rejects.toThrow(FileServiceError);
|
||||
await expect(writeProjectFile(mockStore, "missing/file.txt", "content")).rejects.toThrow("Parent directory does not exist");
|
||||
});
|
||||
|
||||
@@ -375,7 +388,6 @@ describe("writeProjectFile", () => {
|
||||
isFile: () => true,
|
||||
}); // Parent is a file
|
||||
|
||||
await expect(writeProjectFile(mockStore, "file.txt/sub.txt", "content")).rejects.toThrow(FileServiceError);
|
||||
await expect(writeProjectFile(mockStore, "file.txt/sub.txt", "content")).rejects.toThrow("Parent is not a directory");
|
||||
});
|
||||
|
||||
@@ -455,7 +467,7 @@ describe("task file operations", () => {
|
||||
await readFile(mockStore, "FN-123", "PROMPT.md");
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
"/project/.fusion/tasks/KB-123/PROMPT.md",
|
||||
"/project/.fusion/tasks/FN-123/PROMPT.md",
|
||||
"utf-8",
|
||||
);
|
||||
});
|
||||
@@ -476,7 +488,7 @@ describe("task file operations", () => {
|
||||
await readFile(mockStore, "FN-123", "PROMPT.md");
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
"/project/.fusion/tasks/KB-123/PROMPT.md",
|
||||
"/project/.fusion/tasks/FN-123/PROMPT.md",
|
||||
"utf-8",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1228,19 +1228,60 @@ async function fetchZaiUsage(): Promise<ProviderUsage> {
|
||||
* Fetch usage data from all configured providers with caching.
|
||||
* Results are cached for 30 seconds to avoid hitting provider API rate limits.
|
||||
*/
|
||||
/** Max time to wait for any individual provider fetch (ms) */
|
||||
const PROVIDER_FETCH_TIMEOUT_MS = 10_000; // 10 seconds
|
||||
|
||||
/**
|
||||
* Wrap a provider fetch with a timeout. Returns the provider result or an
|
||||
* error provider if the fetch takes longer than PROVIDER_FETCH_TIMEOUT_MS.
|
||||
*/
|
||||
function withTimeout(
|
||||
providerPromise: Promise<ProviderUsage>,
|
||||
providerName: string,
|
||||
timeoutMs: number = PROVIDER_FETCH_TIMEOUT_MS,
|
||||
): Promise<ProviderUsage> {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
resolve({
|
||||
name: providerName,
|
||||
icon: "⏱️",
|
||||
status: "error",
|
||||
error: "Timed out",
|
||||
windows: [],
|
||||
});
|
||||
}, timeoutMs);
|
||||
|
||||
providerPromise
|
||||
.then((result) => {
|
||||
clearTimeout(timer);
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
clearTimeout(timer);
|
||||
resolve({
|
||||
name: providerName,
|
||||
icon: "⏱️",
|
||||
status: "error",
|
||||
error: err.message || "Failed",
|
||||
windows: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAllProviderUsage(_authStorage?: AuthStorageLike): Promise<ProviderUsage[]> {
|
||||
// Check cache
|
||||
if (usageCache && Date.now() - usageCache.timestamp < CACHE_TTL_MS) {
|
||||
return usageCache.data;
|
||||
}
|
||||
|
||||
// Fetch all providers in parallel
|
||||
// Fetch all providers in parallel with per-provider timeout
|
||||
const results = await Promise.allSettled([
|
||||
fetchClaudeUsage(),
|
||||
fetchCodexUsage(),
|
||||
fetchGeminiUsage(),
|
||||
fetchMinimaxUsage(),
|
||||
fetchZaiUsage(),
|
||||
withTimeout(fetchClaudeUsage(), "Claude"),
|
||||
withTimeout(fetchCodexUsage(), "Codex"),
|
||||
withTimeout(fetchGeminiUsage(), "Gemini"),
|
||||
withTimeout(fetchMinimaxUsage(), "Minimax"),
|
||||
withTimeout(fetchZaiUsage(), "Zai"),
|
||||
]);
|
||||
|
||||
const providers: ProviderUsage[] = [];
|
||||
|
||||
@@ -77,6 +77,7 @@ function createMockStore(overrides: Record<string, any> = {}) {
|
||||
parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
getSettings: vi.fn().mockResolvedValue({ ...DEFAULT_SETTINGS }),
|
||||
getRootDir: vi.fn().mockReturnValue("/tmp/root"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/tmp/root/.fusion/tasks"),
|
||||
updateStep: vi.fn().mockImplementation(async (id: string, step: number, status: StepStatus) => {
|
||||
return makeTaskDetail(id, "in-progress");
|
||||
}),
|
||||
|
||||
@@ -49,6 +49,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/test/project/.fusion/tasks"),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
...overrides,
|
||||
@@ -276,7 +277,7 @@ describe("Scheduler", () => {
|
||||
|
||||
vi.mocked(existsSync).mockImplementation((path) => {
|
||||
const value = String(path);
|
||||
return value.includes(".kb/tasks/FN-010") || value.includes("PROMPT.md");
|
||||
return value.includes(".fusion/tasks/FN-010") || value.includes("PROMPT.md");
|
||||
});
|
||||
vi.mocked(readFile).mockResolvedValue("# Prompt\n" as any);
|
||||
|
||||
@@ -301,6 +302,7 @@ describe("Scheduler", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxWorktrees: 4 }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/test/project/.fusion/tasks"),
|
||||
});
|
||||
|
||||
// Set up mocks directly on the store
|
||||
@@ -341,6 +343,7 @@ describe("Scheduler", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxWorktrees: 4 }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/test/project/.fusion/tasks"),
|
||||
});
|
||||
|
||||
const moveTask = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -381,6 +384,7 @@ describe("Scheduler", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxWorktrees: 4 }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/test/project/.fusion/tasks"),
|
||||
});
|
||||
|
||||
const moveTask = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -418,6 +422,7 @@ describe("Scheduler", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxWorktrees: 4 }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/test/project/.fusion/tasks"),
|
||||
});
|
||||
|
||||
const moveTask = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -459,6 +464,7 @@ describe("Scheduler", () => {
|
||||
getSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxWorktrees: 4 }),
|
||||
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/test/project"),
|
||||
getTasksDir: vi.fn().mockReturnValue("/test/project/.fusion/tasks"),
|
||||
});
|
||||
|
||||
const moveTask = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -184,7 +184,7 @@ export class Scheduler {
|
||||
* @returns Object with `valid: true` if checks pass, or `valid: false` with a `reason` string if they fail
|
||||
*/
|
||||
private async validateTaskFilesystem(id: string): Promise<{ valid: boolean; reason?: string }> {
|
||||
const taskDir = join(this.store.getRootDir(), ".kb", "tasks", id);
|
||||
const taskDir = join(this.store.getTasksDir(), id);
|
||||
|
||||
// Check if task directory exists
|
||||
if (!existsSync(taskDir)) {
|
||||
|
||||
Reference in New Issue
Block a user