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:
gsxdsm
2026-04-01 22:38:18 -07:00
parent 3ecc64a62d
commit 04b194982d
30 changed files with 1281 additions and 142 deletions

View 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;
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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");
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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");
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View 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);
}
});
});

View File

@@ -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",
);
});

View File

@@ -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[] = [];