style: fix formatting with Prettier

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:31:57 -05:00
parent 584f5a3426
commit 8d578558ff
295 changed files with 9088 additions and 10546 deletions

View File

@@ -4,11 +4,11 @@
// 1x1 transparent PNG base64 data
export const pngBase64Fixture =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
export const imageDataFixture = {
base64: pngBase64Fixture,
mimeType: "image/png",
filename: "test.png",
originalPath: "/path/to/test.png",
mimeType: 'image/png',
filename: 'test.png',
originalPath: '/path/to/test.png',
};

View File

@@ -1,11 +1,11 @@
/**
* Helper for creating test git repositories for integration tests
*/
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
const execAsync = promisify(exec);
@@ -18,36 +18,36 @@ export interface TestRepo {
* Create a temporary git repository for testing
*/
export async function createTestGitRepo(): Promise<TestRepo> {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "automaker-test-"));
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'automaker-test-'));
// Initialize git repo
await execAsync("git init", { cwd: tmpDir });
await execAsync('git init', { cwd: tmpDir });
await execAsync('git config user.email "test@example.com"', { cwd: tmpDir });
await execAsync('git config user.name "Test User"', { cwd: tmpDir });
// Create initial commit
await fs.writeFile(path.join(tmpDir, "README.md"), "# Test Project\n");
await execAsync("git add .", { cwd: tmpDir });
await fs.writeFile(path.join(tmpDir, 'README.md'), '# Test Project\n');
await execAsync('git add .', { cwd: tmpDir });
await execAsync('git commit -m "Initial commit"', { cwd: tmpDir });
// Create main branch explicitly
await execAsync("git branch -M main", { cwd: tmpDir });
await execAsync('git branch -M main', { cwd: tmpDir });
return {
path: tmpDir,
cleanup: async () => {
try {
// Remove all worktrees first
const { stdout } = await execAsync("git worktree list --porcelain", {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: tmpDir,
}).catch(() => ({ stdout: "" }));
}).catch(() => ({ stdout: '' }));
const worktrees = stdout
.split("\n\n")
.split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
return pathLine ? pathLine.replace("worktree ", "") : null;
const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
return pathLine ? pathLine.replace('worktree ', '') : null;
})
.filter(Boolean);
@@ -64,7 +64,7 @@ export async function createTestGitRepo(): Promise<TestRepo> {
// Remove the repository
await fs.rm(tmpDir, { recursive: true, force: true });
} catch (error) {
console.error("Failed to cleanup test repo:", error);
console.error('Failed to cleanup test repo:', error);
}
},
};
@@ -78,24 +78,21 @@ export async function createTestFeature(
featureId: string,
featureData: any
): Promise<void> {
const featuresDir = path.join(repoPath, ".automaker", "features");
const featuresDir = path.join(repoPath, '.automaker', 'features');
const featureDir = path.join(featuresDir, featureId);
await fs.mkdir(featureDir, { recursive: true });
await fs.writeFile(
path.join(featureDir, "feature.json"),
JSON.stringify(featureData, null, 2)
);
await fs.writeFile(path.join(featureDir, 'feature.json'), JSON.stringify(featureData, null, 2));
}
/**
* Get list of git branches
*/
export async function listBranches(repoPath: string): Promise<string[]> {
const { stdout } = await execAsync("git branch --list", { cwd: repoPath });
const { stdout } = await execAsync('git branch --list', { cwd: repoPath });
return stdout
.split("\n")
.map((line) => line.trim().replace(/^[*+]\s*/, ""))
.split('\n')
.map((line) => line.trim().replace(/^[*+]\s*/, ''))
.filter(Boolean);
}
@@ -104,16 +101,16 @@ export async function listBranches(repoPath: string): Promise<string[]> {
*/
export async function listWorktrees(repoPath: string): Promise<string[]> {
try {
const { stdout } = await execAsync("git worktree list --porcelain", {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: repoPath,
});
return stdout
.split("\n\n")
.split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
return pathLine ? pathLine.replace("worktree ", "") : null;
const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
return pathLine ? pathLine.replace('worktree ', '') : null;
})
.filter(Boolean) as string[];
} catch {
@@ -124,10 +121,7 @@ export async function listWorktrees(repoPath: string): Promise<string[]> {
/**
* Check if a branch exists
*/
export async function branchExists(
repoPath: string,
branchName: string
): Promise<boolean> {
export async function branchExists(repoPath: string, branchName: string): Promise<boolean> {
const branches = await listBranches(repoPath);
return branches.includes(branchName);
}
@@ -135,10 +129,7 @@ export async function branchExists(
/**
* Check if a worktree exists
*/
export async function worktreeExists(
repoPath: string,
worktreePath: string
): Promise<boolean> {
export async function worktreeExists(repoPath: string, worktreePath: string): Promise<boolean> {
const worktrees = await listWorktrees(repoPath);
return worktrees.some((wt) => wt === worktreePath);
}

View File

@@ -1,22 +1,20 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { createCreateHandler } from "@/routes/worktree/routes/create.js";
import { AUTOMAKER_INITIAL_COMMIT_MESSAGE } from "@/routes/worktree/common.js";
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs/promises";
import * as os from "os";
import * as path from "path";
import { describe, it, expect, vi, afterEach } from 'vitest';
import { createCreateHandler } from '@/routes/worktree/routes/create.js';
import { AUTOMAKER_INITIAL_COMMIT_MESSAGE } from '@/routes/worktree/common.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
const execAsync = promisify(exec);
describe("worktree create route - repositories without commits", () => {
describe('worktree create route - repositories without commits', () => {
let repoPath: string | null = null;
async function initRepoWithoutCommit() {
repoPath = await fs.mkdtemp(
path.join(os.tmpdir(), "automaker-no-commit-")
);
await execAsync("git init", { cwd: repoPath });
repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'automaker-no-commit-'));
await execAsync('git init', { cwd: repoPath });
await execAsync('git config user.email "test@example.com"', {
cwd: repoPath,
});
@@ -32,14 +30,14 @@ describe("worktree create route - repositories without commits", () => {
repoPath = null;
});
it("creates an initial commit before adding a worktree when HEAD is missing", async () => {
it('creates an initial commit before adding a worktree when HEAD is missing', async () => {
await initRepoWithoutCommit();
const handler = createCreateHandler();
const json = vi.fn();
const status = vi.fn().mockReturnThis();
const req = {
body: { projectPath: repoPath, branchName: "feature/no-head" },
body: { projectPath: repoPath, branchName: 'feature/no-head' },
} as any;
const res = {
json,
@@ -53,17 +51,12 @@ describe("worktree create route - repositories without commits", () => {
const payload = json.mock.calls[0][0];
expect(payload.success).toBe(true);
const { stdout: commitCount } = await execAsync(
"git rev-list --count HEAD",
{ cwd: repoPath! }
);
const { stdout: commitCount } = await execAsync('git rev-list --count HEAD', {
cwd: repoPath!,
});
expect(Number(commitCount.trim())).toBeGreaterThan(0);
const { stdout: latestMessage } = await execAsync(
"git log -1 --pretty=%B",
{ cwd: repoPath! }
);
const { stdout: latestMessage } = await execAsync('git log -1 --pretty=%B', { cwd: repoPath! });
expect(latestMessage.trim()).toBe(AUTOMAKER_INITIAL_COMMIT_MESSAGE);
});
});

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { AutoModeService } from "@/services/auto-mode-service.js";
import { ProviderFactory } from "@/providers/provider-factory.js";
import { FeatureLoader } from "@/services/feature-loader.js";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AutoModeService } from '@/services/auto-mode-service.js';
import { ProviderFactory } from '@/providers/provider-factory.js';
import { FeatureLoader } from '@/services/feature-loader.js';
import {
createTestGitRepo,
createTestFeature,
@@ -10,17 +10,17 @@ import {
branchExists,
worktreeExists,
type TestRepo,
} from "../helpers/git-test-repo.js";
import * as fs from "fs/promises";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";
} from '../helpers/git-test-repo.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
vi.mock("@/providers/provider-factory.js");
vi.mock('@/providers/provider-factory.js');
describe("auto-mode-service.ts (integration)", () => {
describe('auto-mode-service.ts (integration)', () => {
let service: AutoModeService;
let testRepo: TestRepo;
let featureLoader: FeatureLoader;
@@ -46,22 +46,22 @@ describe("auto-mode-service.ts (integration)", () => {
}
});
describe("worktree operations", () => {
it("should use existing git worktree for feature", async () => {
const branchName = "feature/test-feature-1";
describe('worktree operations', () => {
it('should use existing git worktree for feature', async () => {
const branchName = 'feature/test-feature-1';
// Create a test feature with branchName set
await createTestFeature(testRepo.path, "test-feature-1", {
id: "test-feature-1",
category: "test",
description: "Test feature",
status: "pending",
await createTestFeature(testRepo.path, 'test-feature-1', {
id: 'test-feature-1',
category: 'test',
description: 'Test feature',
status: 'pending',
branchName: branchName,
});
// Create worktree before executing (worktrees are now created when features are added/edited)
const worktreesDir = path.join(testRepo.path, ".worktrees");
const worktreePath = path.join(worktreesDir, "test-feature-1");
const worktreesDir = path.join(testRepo.path, '.worktrees');
const worktreePath = path.join(worktreesDir, 'test-feature-1');
await fs.mkdir(worktreesDir, { recursive: true });
await execAsync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, {
cwd: testRepo.path,
@@ -69,30 +69,28 @@ describe("auto-mode-service.ts (integration)", () => {
// Mock provider to complete quickly
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "assistant",
type: 'assistant',
message: {
role: "assistant",
content: [{ type: "text", text: "Feature implemented" }],
role: 'assistant',
content: [{ type: 'text', text: 'Feature implemented' }],
},
};
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Execute feature with worktrees enabled
await service.executeFeature(
testRepo.path,
"test-feature-1",
'test-feature-1',
true, // useWorktrees
false // isAutoMode
);
@@ -107,8 +105,8 @@ describe("auto-mode-service.ts (integration)", () => {
const worktrees = await listWorktrees(testRepo.path);
expect(worktrees.length).toBeGreaterThan(0);
// Verify that at least one worktree path contains our feature ID
const worktreePathsMatch = worktrees.some(wt =>
wt.includes("test-feature-1") || wt.includes(".worktrees")
const worktreePathsMatch = worktrees.some(
(wt) => wt.includes('test-feature-1') || wt.includes('.worktrees')
);
expect(worktreePathsMatch).toBe(true);
@@ -116,243 +114,200 @@ describe("auto-mode-service.ts (integration)", () => {
// This is expected behavior - manual cleanup is required
}, 30000);
it("should handle error gracefully", async () => {
await createTestFeature(testRepo.path, "test-feature-error", {
id: "test-feature-error",
category: "test",
description: "Test feature that errors",
status: "pending",
it('should handle error gracefully', async () => {
await createTestFeature(testRepo.path, 'test-feature-error', {
id: 'test-feature-error',
category: 'test',
description: 'Test feature that errors',
status: 'pending',
});
// Mock provider that throws error
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
throw new Error("Provider error");
throw new Error('Provider error');
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Execute feature (should handle error)
await service.executeFeature(
testRepo.path,
"test-feature-error",
true,
false
);
await service.executeFeature(testRepo.path, 'test-feature-error', true, false);
// Verify feature status was updated to backlog (error status)
const feature = await featureLoader.get(
testRepo.path,
"test-feature-error"
);
expect(feature?.status).toBe("backlog");
const feature = await featureLoader.get(testRepo.path, 'test-feature-error');
expect(feature?.status).toBe('backlog');
}, 30000);
it("should work without worktrees", async () => {
await createTestFeature(testRepo.path, "test-no-worktree", {
id: "test-no-worktree",
category: "test",
description: "Test without worktree",
status: "pending",
it('should work without worktrees', async () => {
await createTestFeature(testRepo.path, 'test-no-worktree', {
id: 'test-no-worktree',
category: 'test',
description: 'Test without worktree',
status: 'pending',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Execute without worktrees
await service.executeFeature(
testRepo.path,
"test-no-worktree",
'test-no-worktree',
false, // useWorktrees = false
false
);
// Feature should be updated successfully
const feature = await featureLoader.get(
testRepo.path,
"test-no-worktree"
);
expect(feature?.status).toBe("waiting_approval");
const feature = await featureLoader.get(testRepo.path, 'test-no-worktree');
expect(feature?.status).toBe('waiting_approval');
}, 30000);
});
describe("feature execution", () => {
it("should execute feature and update status", async () => {
await createTestFeature(testRepo.path, "feature-exec-1", {
id: "feature-exec-1",
category: "ui",
description: "Execute this feature",
status: "pending",
describe('feature execution', () => {
it('should execute feature and update status', async () => {
await createTestFeature(testRepo.path, 'feature-exec-1', {
id: 'feature-exec-1',
category: 'ui',
description: 'Execute this feature',
status: 'pending',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "assistant",
type: 'assistant',
message: {
role: "assistant",
content: [{ type: "text", text: "Implemented the feature" }],
role: 'assistant',
content: [{ type: 'text', text: 'Implemented the feature' }],
},
};
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
"feature-exec-1",
'feature-exec-1',
false, // Don't use worktrees so agent output is saved to main project
false
);
// Check feature status was updated
const feature = await featureLoader.get(testRepo.path, "feature-exec-1");
expect(feature?.status).toBe("waiting_approval");
const feature = await featureLoader.get(testRepo.path, 'feature-exec-1');
expect(feature?.status).toBe('waiting_approval');
// Check agent output was saved
const agentOutput = await featureLoader.getAgentOutput(
testRepo.path,
"feature-exec-1"
);
const agentOutput = await featureLoader.getAgentOutput(testRepo.path, 'feature-exec-1');
expect(agentOutput).toBeTruthy();
expect(agentOutput).toContain("Implemented the feature");
expect(agentOutput).toContain('Implemented the feature');
}, 30000);
it("should handle feature not found", async () => {
it('should handle feature not found', async () => {
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Try to execute non-existent feature
await service.executeFeature(
testRepo.path,
"nonexistent-feature",
true,
false
);
await service.executeFeature(testRepo.path, 'nonexistent-feature', true, false);
// Should emit error event
expect(mockEvents.emit).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
featureId: "nonexistent-feature",
error: expect.stringContaining("not found"),
featureId: 'nonexistent-feature',
error: expect.stringContaining('not found'),
})
);
}, 30000);
it("should prevent duplicate feature execution", async () => {
await createTestFeature(testRepo.path, "feature-dup", {
id: "feature-dup",
category: "test",
description: "Duplicate test",
status: "pending",
it('should prevent duplicate feature execution', async () => {
await createTestFeature(testRepo.path, 'feature-dup', {
id: 'feature-dup',
category: 'test',
description: 'Duplicate test',
status: 'pending',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
// Simulate slow execution
await new Promise((resolve) => setTimeout(resolve, 500));
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Start first execution
const promise1 = service.executeFeature(
testRepo.path,
"feature-dup",
false,
false
);
const promise1 = service.executeFeature(testRepo.path, 'feature-dup', false, false);
// Try to start second execution (should throw)
await expect(
service.executeFeature(testRepo.path, "feature-dup", false, false)
).rejects.toThrow("already running");
service.executeFeature(testRepo.path, 'feature-dup', false, false)
).rejects.toThrow('already running');
await promise1;
}, 30000);
it("should use feature-specific model", async () => {
await createTestFeature(testRepo.path, "feature-model", {
id: "feature-model",
category: "test",
description: "Model test",
status: "pending",
model: "claude-sonnet-4-20250514",
it('should use feature-specific model', async () => {
await createTestFeature(testRepo.path, 'feature-model', {
id: 'feature-model',
category: 'test',
description: 'Model test',
status: 'pending',
model: 'claude-sonnet-4-20250514',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
"feature-model",
false,
false
);
await service.executeFeature(testRepo.path, 'feature-model', false, false);
// Should have used claude-sonnet-4-20250514
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith(
"claude-sonnet-4-20250514"
);
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith('claude-sonnet-4-20250514');
}, 30000);
});
describe("auto loop", () => {
it("should start and stop auto loop", async () => {
describe('auto loop', () => {
it('should start and stop auto loop', async () => {
const startPromise = service.startAutoLoop(testRepo.path, 2);
// Give it time to start
@@ -365,35 +320,33 @@ describe("auto-mode-service.ts (integration)", () => {
await startPromise.catch(() => {}); // Cleanup
}, 10000);
it("should process pending features in auto loop", async () => {
it('should process pending features in auto loop', async () => {
// Create multiple pending features
await createTestFeature(testRepo.path, "auto-1", {
id: "auto-1",
category: "test",
description: "Auto feature 1",
status: "pending",
await createTestFeature(testRepo.path, 'auto-1', {
id: 'auto-1',
category: 'test',
description: 'Auto feature 1',
status: 'pending',
});
await createTestFeature(testRepo.path, "auto-2", {
id: "auto-2",
category: "test",
description: "Auto feature 2",
status: "pending",
await createTestFeature(testRepo.path, 'auto-2', {
id: 'auto-2',
category: 'test',
description: 'Auto feature 2',
status: 'pending',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Start auto loop
const startPromise = service.startAutoLoop(testRepo.path, 2);
@@ -406,25 +359,25 @@ describe("auto-mode-service.ts (integration)", () => {
await startPromise.catch(() => {});
// Check that features were updated
const feature1 = await featureLoader.get(testRepo.path, "auto-1");
const feature2 = await featureLoader.get(testRepo.path, "auto-2");
const feature1 = await featureLoader.get(testRepo.path, 'auto-1');
const feature2 = await featureLoader.get(testRepo.path, 'auto-2');
// At least one should have been processed
const processedCount = [feature1, feature2].filter(
(f) => f?.status === "waiting_approval" || f?.status === "in_progress"
(f) => f?.status === 'waiting_approval' || f?.status === 'in_progress'
).length;
expect(processedCount).toBeGreaterThan(0);
}, 15000);
it("should respect max concurrency", async () => {
it('should respect max concurrency', async () => {
// Create 5 features
for (let i = 1; i <= 5; i++) {
await createTestFeature(testRepo.path, `concurrent-${i}`, {
id: `concurrent-${i}`,
category: "test",
category: 'test',
description: `Concurrent feature ${i}`,
status: "pending",
status: 'pending',
});
}
@@ -432,7 +385,7 @@ describe("auto-mode-service.ts (integration)", () => {
let maxConcurrent = 0;
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
concurrentCount++;
maxConcurrent = Math.max(maxConcurrent, concurrentCount);
@@ -443,15 +396,13 @@ describe("auto-mode-service.ts (integration)", () => {
concurrentCount--;
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Start with max concurrency of 2
const startPromise = service.startAutoLoop(testRepo.path, 2);
@@ -466,7 +417,7 @@ describe("auto-mode-service.ts (integration)", () => {
expect(maxConcurrent).toBeLessThanOrEqual(2);
}, 15000);
it("should emit auto mode events", async () => {
it('should emit auto mode events', async () => {
const startPromise = service.startAutoLoop(testRepo.path, 1);
// Wait for start event
@@ -474,7 +425,7 @@ describe("auto-mode-service.ts (integration)", () => {
// Check start event was emitted
const startEvent = mockEvents.emit.mock.calls.find((call) =>
call[1]?.message?.includes("Auto mode started")
call[1]?.message?.includes('Auto mode started')
);
expect(startEvent).toBeTruthy();
@@ -484,74 +435,69 @@ describe("auto-mode-service.ts (integration)", () => {
// Check stop event was emitted (emitted immediately by stopAutoLoop)
const stopEvent = mockEvents.emit.mock.calls.find(
(call) =>
call[1]?.type === "auto_mode_stopped" ||
call[1]?.message?.includes("Auto mode stopped")
call[1]?.type === 'auto_mode_stopped' || call[1]?.message?.includes('Auto mode stopped')
);
expect(stopEvent).toBeTruthy();
}, 10000);
});
describe("error handling", () => {
it("should handle provider errors gracefully", async () => {
await createTestFeature(testRepo.path, "error-feature", {
id: "error-feature",
category: "test",
description: "Error test",
status: "pending",
describe('error handling', () => {
it('should handle provider errors gracefully', async () => {
await createTestFeature(testRepo.path, 'error-feature', {
id: 'error-feature',
category: 'test',
description: 'Error test',
status: 'pending',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
throw new Error("Provider execution failed");
throw new Error('Provider execution failed');
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
// Should not throw
await service.executeFeature(testRepo.path, "error-feature", true, false);
await service.executeFeature(testRepo.path, 'error-feature', true, false);
// Feature should be marked as backlog (error status)
const feature = await featureLoader.get(testRepo.path, "error-feature");
expect(feature?.status).toBe("backlog");
const feature = await featureLoader.get(testRepo.path, 'error-feature');
expect(feature?.status).toBe('backlog');
}, 30000);
it("should continue auto loop after feature error", async () => {
await createTestFeature(testRepo.path, "fail-1", {
id: "fail-1",
category: "test",
description: "Will fail",
status: "pending",
it('should continue auto loop after feature error', async () => {
await createTestFeature(testRepo.path, 'fail-1', {
id: 'fail-1',
category: 'test',
description: 'Will fail',
status: 'pending',
});
await createTestFeature(testRepo.path, "success-1", {
id: "success-1",
category: "test",
description: "Will succeed",
status: "pending",
await createTestFeature(testRepo.path, 'success-1', {
id: 'success-1',
category: 'test',
description: 'Will succeed',
status: 'pending',
});
let callCount = 0;
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
callCount++;
if (callCount === 1) {
throw new Error("First feature fails");
throw new Error('First feature fails');
}
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
const startPromise = service.startAutoLoop(testRepo.path, 1);
@@ -566,200 +512,177 @@ describe("auto-mode-service.ts (integration)", () => {
}, 15000);
});
describe("planning mode", () => {
it("should execute feature with skip planning mode", async () => {
await createTestFeature(testRepo.path, "skip-plan-feature", {
id: "skip-plan-feature",
category: "test",
description: "Feature with skip planning",
status: "pending",
planningMode: "skip",
describe('planning mode', () => {
it('should execute feature with skip planning mode', async () => {
await createTestFeature(testRepo.path, 'skip-plan-feature', {
id: 'skip-plan-feature',
category: 'test',
description: 'Feature with skip planning',
status: 'pending',
planningMode: 'skip',
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "assistant",
type: 'assistant',
message: {
role: "assistant",
content: [{ type: "text", text: "Feature implemented" }],
role: 'assistant',
content: [{ type: 'text', text: 'Feature implemented' }],
},
};
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
"skip-plan-feature",
false,
false
);
await service.executeFeature(testRepo.path, 'skip-plan-feature', false, false);
const feature = await featureLoader.get(testRepo.path, "skip-plan-feature");
expect(feature?.status).toBe("waiting_approval");
const feature = await featureLoader.get(testRepo.path, 'skip-plan-feature');
expect(feature?.status).toBe('waiting_approval');
}, 30000);
it("should execute feature with lite planning mode without approval", async () => {
await createTestFeature(testRepo.path, "lite-plan-feature", {
id: "lite-plan-feature",
category: "test",
description: "Feature with lite planning",
status: "pending",
planningMode: "lite",
it('should execute feature with lite planning mode without approval', async () => {
await createTestFeature(testRepo.path, 'lite-plan-feature', {
id: 'lite-plan-feature',
category: 'test',
description: 'Feature with lite planning',
status: 'pending',
planningMode: 'lite',
requirePlanApproval: false,
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "assistant",
type: 'assistant',
message: {
role: "assistant",
content: [{ type: "text", text: "[PLAN_GENERATED] Planning outline complete.\n\nFeature implemented" }],
role: 'assistant',
content: [
{
type: 'text',
text: '[PLAN_GENERATED] Planning outline complete.\n\nFeature implemented',
},
],
},
};
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
"lite-plan-feature",
false,
false
);
await service.executeFeature(testRepo.path, 'lite-plan-feature', false, false);
const feature = await featureLoader.get(testRepo.path, "lite-plan-feature");
expect(feature?.status).toBe("waiting_approval");
const feature = await featureLoader.get(testRepo.path, 'lite-plan-feature');
expect(feature?.status).toBe('waiting_approval');
}, 30000);
it("should emit planning_started event for spec mode", async () => {
await createTestFeature(testRepo.path, "spec-plan-feature", {
id: "spec-plan-feature",
category: "test",
description: "Feature with spec planning",
status: "pending",
planningMode: "spec",
it('should emit planning_started event for spec mode', async () => {
await createTestFeature(testRepo.path, 'spec-plan-feature', {
id: 'spec-plan-feature',
category: 'test',
description: 'Feature with spec planning',
status: 'pending',
planningMode: 'spec',
requirePlanApproval: false,
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "assistant",
type: 'assistant',
message: {
role: "assistant",
content: [{ type: "text", text: "Spec generated\n\n[SPEC_GENERATED] Review the spec." }],
role: 'assistant',
content: [
{ type: 'text', text: 'Spec generated\n\n[SPEC_GENERATED] Review the spec.' },
],
},
};
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
"spec-plan-feature",
false,
false
);
await service.executeFeature(testRepo.path, 'spec-plan-feature', false, false);
// Check planning_started event was emitted
const planningEvent = mockEvents.emit.mock.calls.find(
(call) => call[1]?.mode === "spec"
);
const planningEvent = mockEvents.emit.mock.calls.find((call) => call[1]?.mode === 'spec');
expect(planningEvent).toBeTruthy();
}, 30000);
it("should handle feature with full planning mode", async () => {
await createTestFeature(testRepo.path, "full-plan-feature", {
id: "full-plan-feature",
category: "test",
description: "Feature with full planning",
status: "pending",
planningMode: "full",
it('should handle feature with full planning mode', async () => {
await createTestFeature(testRepo.path, 'full-plan-feature', {
id: 'full-plan-feature',
category: 'test',
description: 'Feature with full planning',
status: 'pending',
planningMode: 'full',
requirePlanApproval: false,
});
const mockProvider = {
getName: () => "claude",
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: "assistant",
type: 'assistant',
message: {
role: "assistant",
content: [{ type: "text", text: "Full spec with phases\n\n[SPEC_GENERATED] Review." }],
role: 'assistant',
content: [
{ type: 'text', text: 'Full spec with phases\n\n[SPEC_GENERATED] Review.' },
],
},
};
yield {
type: "result",
subtype: "success",
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(
mockProvider as any
);
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
await service.executeFeature(
testRepo.path,
"full-plan-feature",
false,
false
);
await service.executeFeature(testRepo.path, 'full-plan-feature', false, false);
// Check planning_started event was emitted with full mode
const planningEvent = mockEvents.emit.mock.calls.find(
(call) => call[1]?.mode === "full"
);
const planningEvent = mockEvents.emit.mock.calls.find((call) => call[1]?.mode === 'full');
expect(planningEvent).toBeTruthy();
}, 30000);
it("should track pending approval correctly", async () => {
it('should track pending approval correctly', async () => {
// Initially no pending approvals
expect(service.hasPendingApproval("non-existent")).toBe(false);
expect(service.hasPendingApproval('non-existent')).toBe(false);
});
it("should cancel pending approval gracefully", () => {
it('should cancel pending approval gracefully', () => {
// Should not throw when cancelling non-existent approval
expect(() => service.cancelPlanApproval("non-existent")).not.toThrow();
expect(() => service.cancelPlanApproval('non-existent')).not.toThrow();
});
it("should resolve approval with error for non-existent feature", async () => {
it('should resolve approval with error for non-existent feature', async () => {
const result = await service.resolvePlanApproval(
"non-existent",
'non-existent',
true,
undefined,
undefined,
undefined
);
expect(result.success).toBe(false);
expect(result.error).toContain("No pending approval");
expect(result.error).toContain('No pending approval');
});
});
});

View File

@@ -1,143 +1,137 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect } from 'vitest';
import {
specToXml,
getStructuredSpecPromptInstruction,
getAppSpecFormatInstruction,
APP_SPEC_XML_FORMAT,
type SpecOutput,
} from "@/lib/app-spec-format.js";
} from '@/lib/app-spec-format.js';
describe("app-spec-format.ts", () => {
describe("specToXml", () => {
it("should convert minimal spec to XML", () => {
describe('app-spec-format.ts', () => {
describe('specToXml', () => {
it('should convert minimal spec to XML', () => {
const spec: SpecOutput = {
project_name: "Test Project",
overview: "A test project",
technology_stack: ["TypeScript", "Node.js"],
core_capabilities: ["Testing", "Development"],
implemented_features: [
{ name: "Feature 1", description: "First feature" },
],
project_name: 'Test Project',
overview: 'A test project',
technology_stack: ['TypeScript', 'Node.js'],
core_capabilities: ['Testing', 'Development'],
implemented_features: [{ name: 'Feature 1', description: 'First feature' }],
};
const xml = specToXml(spec);
expect(xml).toContain('<?xml version="1.0" encoding="UTF-8"?>');
expect(xml).toContain("<project_specification>");
expect(xml).toContain("</project_specification>");
expect(xml).toContain("<project_name>Test Project</project_name>");
expect(xml).toContain("<technology>TypeScript</technology>");
expect(xml).toContain("<capability>Testing</capability>");
expect(xml).toContain('<project_specification>');
expect(xml).toContain('</project_specification>');
expect(xml).toContain('<project_name>Test Project</project_name>');
expect(xml).toContain('<technology>TypeScript</technology>');
expect(xml).toContain('<capability>Testing</capability>');
});
it("should escape XML special characters", () => {
it('should escape XML special characters', () => {
const spec: SpecOutput = {
project_name: "Test & Project",
overview: "Description with <tags>",
technology_stack: ["TypeScript"],
core_capabilities: ["Cap"],
project_name: 'Test & Project',
overview: 'Description with <tags>',
technology_stack: ['TypeScript'],
core_capabilities: ['Cap'],
implemented_features: [],
};
const xml = specToXml(spec);
expect(xml).toContain("Test &amp; Project");
expect(xml).toContain("&lt;tags&gt;");
expect(xml).toContain('Test &amp; Project');
expect(xml).toContain('&lt;tags&gt;');
});
it("should include file_locations when provided", () => {
it('should include file_locations when provided', () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
project_name: 'Test',
overview: 'Test',
technology_stack: ['TS'],
core_capabilities: ['Cap'],
implemented_features: [
{
name: "Feature",
description: "Desc",
file_locations: ["src/index.ts"],
name: 'Feature',
description: 'Desc',
file_locations: ['src/index.ts'],
},
],
};
const xml = specToXml(spec);
expect(xml).toContain("<file_locations>");
expect(xml).toContain("<location>src/index.ts</location>");
expect(xml).toContain('<file_locations>');
expect(xml).toContain('<location>src/index.ts</location>');
});
it("should not include file_locations when empty", () => {
it('should not include file_locations when empty', () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
implemented_features: [
{ name: "Feature", description: "Desc", file_locations: [] },
],
project_name: 'Test',
overview: 'Test',
technology_stack: ['TS'],
core_capabilities: ['Cap'],
implemented_features: [{ name: 'Feature', description: 'Desc', file_locations: [] }],
};
const xml = specToXml(spec);
expect(xml).not.toContain("<file_locations>");
expect(xml).not.toContain('<file_locations>');
});
it("should include additional_requirements when provided", () => {
it('should include additional_requirements when provided', () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
project_name: 'Test',
overview: 'Test',
technology_stack: ['TS'],
core_capabilities: ['Cap'],
implemented_features: [],
additional_requirements: ["Node.js 18+"],
additional_requirements: ['Node.js 18+'],
};
const xml = specToXml(spec);
expect(xml).toContain("<additional_requirements>");
expect(xml).toContain("<requirement>Node.js 18+</requirement>");
expect(xml).toContain('<additional_requirements>');
expect(xml).toContain('<requirement>Node.js 18+</requirement>');
});
it("should include development_guidelines when provided", () => {
it('should include development_guidelines when provided', () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
project_name: 'Test',
overview: 'Test',
technology_stack: ['TS'],
core_capabilities: ['Cap'],
implemented_features: [],
development_guidelines: ["Use ESLint"],
development_guidelines: ['Use ESLint'],
};
const xml = specToXml(spec);
expect(xml).toContain("<development_guidelines>");
expect(xml).toContain("<guideline>Use ESLint</guideline>");
expect(xml).toContain('<development_guidelines>');
expect(xml).toContain('<guideline>Use ESLint</guideline>');
});
it("should include implementation_roadmap when provided", () => {
it('should include implementation_roadmap when provided', () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
project_name: 'Test',
overview: 'Test',
technology_stack: ['TS'],
core_capabilities: ['Cap'],
implemented_features: [],
implementation_roadmap: [
{ phase: "Phase 1", status: "completed", description: "Setup" },
],
implementation_roadmap: [{ phase: 'Phase 1', status: 'completed', description: 'Setup' }],
};
const xml = specToXml(spec);
expect(xml).toContain("<implementation_roadmap>");
expect(xml).toContain("<status>completed</status>");
expect(xml).toContain('<implementation_roadmap>');
expect(xml).toContain('<status>completed</status>');
});
it("should not include optional sections when empty", () => {
it('should not include optional sections when empty', () => {
const spec: SpecOutput = {
project_name: "Test",
overview: "Test",
technology_stack: ["TS"],
core_capabilities: ["Cap"],
project_name: 'Test',
overview: 'Test',
technology_stack: ['TS'],
core_capabilities: ['Cap'],
implemented_features: [],
additional_requirements: [],
development_guidelines: [],
@@ -146,44 +140,44 @@ describe("app-spec-format.ts", () => {
const xml = specToXml(spec);
expect(xml).not.toContain("<additional_requirements>");
expect(xml).not.toContain("<development_guidelines>");
expect(xml).not.toContain("<implementation_roadmap>");
expect(xml).not.toContain('<additional_requirements>');
expect(xml).not.toContain('<development_guidelines>');
expect(xml).not.toContain('<implementation_roadmap>');
});
});
describe("getStructuredSpecPromptInstruction", () => {
it("should return non-empty prompt instruction", () => {
describe('getStructuredSpecPromptInstruction', () => {
it('should return non-empty prompt instruction', () => {
const instruction = getStructuredSpecPromptInstruction();
expect(instruction).toBeTruthy();
expect(instruction.length).toBeGreaterThan(100);
});
it("should mention required fields", () => {
it('should mention required fields', () => {
const instruction = getStructuredSpecPromptInstruction();
expect(instruction).toContain("project_name");
expect(instruction).toContain("overview");
expect(instruction).toContain("technology_stack");
expect(instruction).toContain('project_name');
expect(instruction).toContain('overview');
expect(instruction).toContain('technology_stack');
});
});
describe("getAppSpecFormatInstruction", () => {
it("should return non-empty format instruction", () => {
describe('getAppSpecFormatInstruction', () => {
it('should return non-empty format instruction', () => {
const instruction = getAppSpecFormatInstruction();
expect(instruction).toBeTruthy();
expect(instruction.length).toBeGreaterThan(100);
});
it("should include critical formatting requirements", () => {
it('should include critical formatting requirements', () => {
const instruction = getAppSpecFormatInstruction();
expect(instruction).toContain("CRITICAL FORMATTING REQUIREMENTS");
expect(instruction).toContain('CRITICAL FORMATTING REQUIREMENTS');
});
});
describe("APP_SPEC_XML_FORMAT", () => {
it("should contain valid XML template structure", () => {
expect(APP_SPEC_XML_FORMAT).toContain("<project_specification>");
expect(APP_SPEC_XML_FORMAT).toContain("</project_specification>");
describe('APP_SPEC_XML_FORMAT', () => {
it('should contain valid XML template structure', () => {
expect(APP_SPEC_XML_FORMAT).toContain('<project_specification>');
expect(APP_SPEC_XML_FORMAT).toContain('</project_specification>');
});
});
});

View File

@@ -1,20 +1,20 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { createMockExpressContext } from "../../utils/mocks.js";
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createMockExpressContext } from '../../utils/mocks.js';
/**
* Note: auth.ts reads AUTOMAKER_API_KEY at module load time.
* We need to reset modules and reimport for each test to get fresh state.
*/
describe("auth.ts", () => {
describe('auth.ts', () => {
beforeEach(() => {
vi.resetModules();
});
describe("authMiddleware - no API key", () => {
it("should call next() when no API key is set", async () => {
describe('authMiddleware - no API key', () => {
it('should call next() when no API key is set', async () => {
delete process.env.AUTOMAKER_API_KEY;
const { authMiddleware } = await import("@/lib/auth.js");
const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
authMiddleware(req, res, next);
@@ -24,11 +24,11 @@ describe("auth.ts", () => {
});
});
describe("authMiddleware - with API key", () => {
it("should reject request without API key header", async () => {
process.env.AUTOMAKER_API_KEY = "test-secret-key";
describe('authMiddleware - with API key', () => {
it('should reject request without API key header', async () => {
process.env.AUTOMAKER_API_KEY = 'test-secret-key';
const { authMiddleware } = await import("@/lib/auth.js");
const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
authMiddleware(req, res, next);
@@ -36,34 +36,34 @@ describe("auth.ts", () => {
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: "Authentication required. Provide X-API-Key header.",
error: 'Authentication required. Provide X-API-Key header.',
});
expect(next).not.toHaveBeenCalled();
});
it("should reject request with invalid API key", async () => {
process.env.AUTOMAKER_API_KEY = "test-secret-key";
it('should reject request with invalid API key', async () => {
process.env.AUTOMAKER_API_KEY = 'test-secret-key';
const { authMiddleware } = await import("@/lib/auth.js");
const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
req.headers["x-api-key"] = "wrong-key";
req.headers['x-api-key'] = 'wrong-key';
authMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
success: false,
error: "Invalid API key.",
error: 'Invalid API key.',
});
expect(next).not.toHaveBeenCalled();
});
it("should call next() with valid API key", async () => {
process.env.AUTOMAKER_API_KEY = "test-secret-key";
it('should call next() with valid API key', async () => {
process.env.AUTOMAKER_API_KEY = 'test-secret-key';
const { authMiddleware } = await import("@/lib/auth.js");
const { req, res, next} = createMockExpressContext();
req.headers["x-api-key"] = "test-secret-key";
const { authMiddleware } = await import('@/lib/auth.js');
const { req, res, next } = createMockExpressContext();
req.headers['x-api-key'] = 'test-secret-key';
authMiddleware(req, res, next);
@@ -72,44 +72,44 @@ describe("auth.ts", () => {
});
});
describe("isAuthEnabled", () => {
it("should return false when no API key is set", async () => {
describe('isAuthEnabled', () => {
it('should return false when no API key is set', async () => {
delete process.env.AUTOMAKER_API_KEY;
const { isAuthEnabled } = await import("@/lib/auth.js");
const { isAuthEnabled } = await import('@/lib/auth.js');
expect(isAuthEnabled()).toBe(false);
});
it("should return true when API key is set", async () => {
process.env.AUTOMAKER_API_KEY = "test-key";
it('should return true when API key is set', async () => {
process.env.AUTOMAKER_API_KEY = 'test-key';
const { isAuthEnabled } = await import("@/lib/auth.js");
const { isAuthEnabled } = await import('@/lib/auth.js');
expect(isAuthEnabled()).toBe(true);
});
});
describe("getAuthStatus", () => {
it("should return disabled status when no API key", async () => {
describe('getAuthStatus', () => {
it('should return disabled status when no API key', async () => {
delete process.env.AUTOMAKER_API_KEY;
const { getAuthStatus } = await import("@/lib/auth.js");
const { getAuthStatus } = await import('@/lib/auth.js');
const status = getAuthStatus();
expect(status).toEqual({
enabled: false,
method: "none",
method: 'none',
});
});
it("should return enabled status when API key is set", async () => {
process.env.AUTOMAKER_API_KEY = "test-key";
it('should return enabled status when API key is set', async () => {
process.env.AUTOMAKER_API_KEY = 'test-key';
const { getAuthStatus } = await import("@/lib/auth.js");
const { getAuthStatus } = await import('@/lib/auth.js');
const status = getAuthStatus();
expect(status).toEqual({
enabled: true,
method: "api_key",
method: 'api_key',
});
});
});

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect } from 'vitest';
import {
getEnhancementPrompt,
getSystemPrompt,
@@ -15,38 +15,38 @@ import {
SIMPLIFY_EXAMPLES,
ACCEPTANCE_EXAMPLES,
type EnhancementMode,
} from "@/lib/enhancement-prompts.js";
} from '@/lib/enhancement-prompts.js';
describe("enhancement-prompts.ts", () => {
describe("System Prompt Constants", () => {
it("should have non-empty improve system prompt", () => {
describe('enhancement-prompts.ts', () => {
describe('System Prompt Constants', () => {
it('should have non-empty improve system prompt', () => {
expect(IMPROVE_SYSTEM_PROMPT).toBeDefined();
expect(IMPROVE_SYSTEM_PROMPT.length).toBeGreaterThan(100);
expect(IMPROVE_SYSTEM_PROMPT).toContain("ANALYZE");
expect(IMPROVE_SYSTEM_PROMPT).toContain("CLARIFY");
expect(IMPROVE_SYSTEM_PROMPT).toContain('ANALYZE');
expect(IMPROVE_SYSTEM_PROMPT).toContain('CLARIFY');
});
it("should have non-empty technical system prompt", () => {
it('should have non-empty technical system prompt', () => {
expect(TECHNICAL_SYSTEM_PROMPT).toBeDefined();
expect(TECHNICAL_SYSTEM_PROMPT.length).toBeGreaterThan(100);
expect(TECHNICAL_SYSTEM_PROMPT).toContain("technical");
expect(TECHNICAL_SYSTEM_PROMPT).toContain('technical');
});
it("should have non-empty simplify system prompt", () => {
it('should have non-empty simplify system prompt', () => {
expect(SIMPLIFY_SYSTEM_PROMPT).toBeDefined();
expect(SIMPLIFY_SYSTEM_PROMPT.length).toBeGreaterThan(100);
expect(SIMPLIFY_SYSTEM_PROMPT).toContain("simplify");
expect(SIMPLIFY_SYSTEM_PROMPT).toContain('simplify');
});
it("should have non-empty acceptance system prompt", () => {
it('should have non-empty acceptance system prompt', () => {
expect(ACCEPTANCE_SYSTEM_PROMPT).toBeDefined();
expect(ACCEPTANCE_SYSTEM_PROMPT.length).toBeGreaterThan(100);
expect(ACCEPTANCE_SYSTEM_PROMPT).toContain("acceptance criteria");
expect(ACCEPTANCE_SYSTEM_PROMPT).toContain('acceptance criteria');
});
});
describe("Example Constants", () => {
it("should have improve examples with input and output", () => {
describe('Example Constants', () => {
it('should have improve examples with input and output', () => {
expect(IMPROVE_EXAMPLES).toBeDefined();
expect(IMPROVE_EXAMPLES.length).toBeGreaterThan(0);
IMPROVE_EXAMPLES.forEach((example) => {
@@ -57,7 +57,7 @@ describe("enhancement-prompts.ts", () => {
});
});
it("should have technical examples with input and output", () => {
it('should have technical examples with input and output', () => {
expect(TECHNICAL_EXAMPLES).toBeDefined();
expect(TECHNICAL_EXAMPLES.length).toBeGreaterThan(0);
TECHNICAL_EXAMPLES.forEach((example) => {
@@ -66,7 +66,7 @@ describe("enhancement-prompts.ts", () => {
});
});
it("should have simplify examples with input and output", () => {
it('should have simplify examples with input and output', () => {
expect(SIMPLIFY_EXAMPLES).toBeDefined();
expect(SIMPLIFY_EXAMPLES.length).toBeGreaterThan(0);
SIMPLIFY_EXAMPLES.forEach((example) => {
@@ -75,7 +75,7 @@ describe("enhancement-prompts.ts", () => {
});
});
it("should have acceptance examples with input and output", () => {
it('should have acceptance examples with input and output', () => {
expect(ACCEPTANCE_EXAMPLES).toBeDefined();
expect(ACCEPTANCE_EXAMPLES.length).toBeGreaterThan(0);
ACCEPTANCE_EXAMPLES.forEach((example) => {
@@ -85,66 +85,66 @@ describe("enhancement-prompts.ts", () => {
});
});
describe("getEnhancementPrompt", () => {
it("should return config for improve mode", () => {
const config = getEnhancementPrompt("improve");
describe('getEnhancementPrompt', () => {
it('should return config for improve mode', () => {
const config = getEnhancementPrompt('improve');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
expect(config.description).toContain("clear");
expect(config.description).toContain('clear');
});
it("should return config for technical mode", () => {
const config = getEnhancementPrompt("technical");
it('should return config for technical mode', () => {
const config = getEnhancementPrompt('technical');
expect(config.systemPrompt).toBe(TECHNICAL_SYSTEM_PROMPT);
expect(config.description).toContain("technical");
expect(config.description).toContain('technical');
});
it("should return config for simplify mode", () => {
const config = getEnhancementPrompt("simplify");
it('should return config for simplify mode', () => {
const config = getEnhancementPrompt('simplify');
expect(config.systemPrompt).toBe(SIMPLIFY_SYSTEM_PROMPT);
expect(config.description).toContain("concise");
expect(config.description).toContain('concise');
});
it("should return config for acceptance mode", () => {
const config = getEnhancementPrompt("acceptance");
it('should return config for acceptance mode', () => {
const config = getEnhancementPrompt('acceptance');
expect(config.systemPrompt).toBe(ACCEPTANCE_SYSTEM_PROMPT);
expect(config.description).toContain("acceptance");
expect(config.description).toContain('acceptance');
});
it("should handle case-insensitive mode", () => {
const config = getEnhancementPrompt("IMPROVE");
it('should handle case-insensitive mode', () => {
const config = getEnhancementPrompt('IMPROVE');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
});
it("should fall back to improve for invalid mode", () => {
const config = getEnhancementPrompt("invalid-mode");
it('should fall back to improve for invalid mode', () => {
const config = getEnhancementPrompt('invalid-mode');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
});
it("should fall back to improve for empty string", () => {
const config = getEnhancementPrompt("");
it('should fall back to improve for empty string', () => {
const config = getEnhancementPrompt('');
expect(config.systemPrompt).toBe(IMPROVE_SYSTEM_PROMPT);
});
});
describe("getSystemPrompt", () => {
it("should return correct system prompt for each mode", () => {
expect(getSystemPrompt("improve")).toBe(IMPROVE_SYSTEM_PROMPT);
expect(getSystemPrompt("technical")).toBe(TECHNICAL_SYSTEM_PROMPT);
expect(getSystemPrompt("simplify")).toBe(SIMPLIFY_SYSTEM_PROMPT);
expect(getSystemPrompt("acceptance")).toBe(ACCEPTANCE_SYSTEM_PROMPT);
describe('getSystemPrompt', () => {
it('should return correct system prompt for each mode', () => {
expect(getSystemPrompt('improve')).toBe(IMPROVE_SYSTEM_PROMPT);
expect(getSystemPrompt('technical')).toBe(TECHNICAL_SYSTEM_PROMPT);
expect(getSystemPrompt('simplify')).toBe(SIMPLIFY_SYSTEM_PROMPT);
expect(getSystemPrompt('acceptance')).toBe(ACCEPTANCE_SYSTEM_PROMPT);
});
});
describe("getExamples", () => {
it("should return correct examples for each mode", () => {
expect(getExamples("improve")).toBe(IMPROVE_EXAMPLES);
expect(getExamples("technical")).toBe(TECHNICAL_EXAMPLES);
expect(getExamples("simplify")).toBe(SIMPLIFY_EXAMPLES);
expect(getExamples("acceptance")).toBe(ACCEPTANCE_EXAMPLES);
describe('getExamples', () => {
it('should return correct examples for each mode', () => {
expect(getExamples('improve')).toBe(IMPROVE_EXAMPLES);
expect(getExamples('technical')).toBe(TECHNICAL_EXAMPLES);
expect(getExamples('simplify')).toBe(SIMPLIFY_EXAMPLES);
expect(getExamples('acceptance')).toBe(ACCEPTANCE_EXAMPLES);
});
it("should return arrays with example objects", () => {
const modes: EnhancementMode[] = ["improve", "technical", "simplify", "acceptance"];
it('should return arrays with example objects', () => {
const modes: EnhancementMode[] = ['improve', 'technical', 'simplify', 'acceptance'];
modes.forEach((mode) => {
const examples = getExamples(mode);
expect(Array.isArray(examples)).toBe(true);
@@ -153,38 +153,38 @@ describe("enhancement-prompts.ts", () => {
});
});
describe("buildUserPrompt", () => {
const testText = "Add a logout button";
describe('buildUserPrompt', () => {
const testText = 'Add a logout button';
it("should build prompt with examples by default", () => {
const prompt = buildUserPrompt("improve", testText);
expect(prompt).toContain("Example 1:");
it('should build prompt with examples by default', () => {
const prompt = buildUserPrompt('improve', testText);
expect(prompt).toContain('Example 1:');
expect(prompt).toContain(testText);
expect(prompt).toContain("Now, please enhance the following task description:");
expect(prompt).toContain('Now, please enhance the following task description:');
});
it("should build prompt without examples when includeExamples is false", () => {
const prompt = buildUserPrompt("improve", testText, false);
expect(prompt).not.toContain("Example 1:");
it('should build prompt without examples when includeExamples is false', () => {
const prompt = buildUserPrompt('improve', testText, false);
expect(prompt).not.toContain('Example 1:');
expect(prompt).toContain(testText);
expect(prompt).toContain("Please enhance the following task description:");
expect(prompt).toContain('Please enhance the following task description:');
});
it("should include all examples for improve mode", () => {
const prompt = buildUserPrompt("improve", testText);
it('should include all examples for improve mode', () => {
const prompt = buildUserPrompt('improve', testText);
IMPROVE_EXAMPLES.forEach((example, index) => {
expect(prompt).toContain(`Example ${index + 1}:`);
expect(prompt).toContain(example.input);
});
});
it("should include separator between examples", () => {
const prompt = buildUserPrompt("improve", testText);
expect(prompt).toContain("---");
it('should include separator between examples', () => {
const prompt = buildUserPrompt('improve', testText);
expect(prompt).toContain('---');
});
it("should work with all enhancement modes", () => {
const modes: EnhancementMode[] = ["improve", "technical", "simplify", "acceptance"];
it('should work with all enhancement modes', () => {
const modes: EnhancementMode[] = ['improve', 'technical', 'simplify', 'acceptance'];
modes.forEach((mode) => {
const prompt = buildUserPrompt(mode, testText);
expect(prompt).toContain(testText);
@@ -192,40 +192,40 @@ describe("enhancement-prompts.ts", () => {
});
});
it("should preserve the original text exactly", () => {
const specialText = "Add feature with special chars: <>&\"'";
const prompt = buildUserPrompt("improve", specialText);
it('should preserve the original text exactly', () => {
const specialText = 'Add feature with special chars: <>&"\'';
const prompt = buildUserPrompt('improve', specialText);
expect(prompt).toContain(specialText);
});
});
describe("isValidEnhancementMode", () => {
it("should return true for valid modes", () => {
expect(isValidEnhancementMode("improve")).toBe(true);
expect(isValidEnhancementMode("technical")).toBe(true);
expect(isValidEnhancementMode("simplify")).toBe(true);
expect(isValidEnhancementMode("acceptance")).toBe(true);
describe('isValidEnhancementMode', () => {
it('should return true for valid modes', () => {
expect(isValidEnhancementMode('improve')).toBe(true);
expect(isValidEnhancementMode('technical')).toBe(true);
expect(isValidEnhancementMode('simplify')).toBe(true);
expect(isValidEnhancementMode('acceptance')).toBe(true);
});
it("should return false for invalid modes", () => {
expect(isValidEnhancementMode("invalid")).toBe(false);
expect(isValidEnhancementMode("IMPROVE")).toBe(false); // case-sensitive
expect(isValidEnhancementMode("")).toBe(false);
expect(isValidEnhancementMode("random")).toBe(false);
it('should return false for invalid modes', () => {
expect(isValidEnhancementMode('invalid')).toBe(false);
expect(isValidEnhancementMode('IMPROVE')).toBe(false); // case-sensitive
expect(isValidEnhancementMode('')).toBe(false);
expect(isValidEnhancementMode('random')).toBe(false);
});
});
describe("getAvailableEnhancementModes", () => {
it("should return all four enhancement modes", () => {
describe('getAvailableEnhancementModes', () => {
it('should return all four enhancement modes', () => {
const modes = getAvailableEnhancementModes();
expect(modes).toHaveLength(4);
expect(modes).toContain("improve");
expect(modes).toContain("technical");
expect(modes).toContain("simplify");
expect(modes).toContain("acceptance");
expect(modes).toContain('improve');
expect(modes).toContain('technical');
expect(modes).toContain('simplify');
expect(modes).toContain('acceptance');
});
it("should return an array", () => {
it('should return an array', () => {
const modes = getAvailableEnhancementModes();
expect(Array.isArray(modes)).toBe(true);
});

View File

@@ -1,20 +1,20 @@
import { describe, it, expect, vi } from "vitest";
import { createEventEmitter, type EventType } from "@/lib/events.js";
import { describe, it, expect, vi } from 'vitest';
import { createEventEmitter, type EventType } from '@/lib/events.js';
describe("events.ts", () => {
describe("createEventEmitter", () => {
it("should emit events to single subscriber", () => {
describe('events.ts', () => {
describe('createEventEmitter', () => {
it('should emit events to single subscriber', () => {
const emitter = createEventEmitter();
const callback = vi.fn();
emitter.subscribe(callback);
emitter.emit("agent:stream", { message: "test" });
emitter.emit('agent:stream', { message: 'test' });
expect(callback).toHaveBeenCalledOnce();
expect(callback).toHaveBeenCalledWith("agent:stream", { message: "test" });
expect(callback).toHaveBeenCalledWith('agent:stream', { message: 'test' });
});
it("should emit events to multiple subscribers", () => {
it('should emit events to multiple subscribers', () => {
const emitter = createEventEmitter();
const callback1 = vi.fn();
const callback2 = vi.fn();
@@ -23,42 +23,42 @@ describe("events.ts", () => {
emitter.subscribe(callback1);
emitter.subscribe(callback2);
emitter.subscribe(callback3);
emitter.emit("feature:started", { id: "123" });
emitter.emit('feature:started', { id: '123' });
expect(callback1).toHaveBeenCalledOnce();
expect(callback2).toHaveBeenCalledOnce();
expect(callback3).toHaveBeenCalledOnce();
expect(callback1).toHaveBeenCalledWith("feature:started", { id: "123" });
expect(callback1).toHaveBeenCalledWith('feature:started', { id: '123' });
});
it("should support unsubscribe functionality", () => {
it('should support unsubscribe functionality', () => {
const emitter = createEventEmitter();
const callback = vi.fn();
const unsubscribe = emitter.subscribe(callback);
emitter.emit("agent:stream", { test: 1 });
emitter.emit('agent:stream', { test: 1 });
expect(callback).toHaveBeenCalledOnce();
unsubscribe();
emitter.emit("agent:stream", { test: 2 });
emitter.emit('agent:stream', { test: 2 });
expect(callback).toHaveBeenCalledOnce(); // Still called only once
});
it("should handle errors in subscribers without crashing", () => {
it('should handle errors in subscribers without crashing', () => {
const emitter = createEventEmitter();
const errorCallback = vi.fn(() => {
throw new Error("Subscriber error");
throw new Error('Subscriber error');
});
const normalCallback = vi.fn();
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
emitter.subscribe(errorCallback);
emitter.subscribe(normalCallback);
expect(() => {
emitter.emit("feature:error", { error: "test" });
emitter.emit('feature:error', { error: 'test' });
}).not.toThrow();
expect(errorCallback).toHaveBeenCalledOnce();
@@ -68,17 +68,17 @@ describe("events.ts", () => {
consoleSpy.mockRestore();
});
it("should emit different event types", () => {
it('should emit different event types', () => {
const emitter = createEventEmitter();
const callback = vi.fn();
emitter.subscribe(callback);
const eventTypes: EventType[] = [
"agent:stream",
"auto-mode:started",
"feature:completed",
"project:analysis-progress",
'agent:stream',
'auto-mode:started',
'feature:completed',
'project:analysis-progress',
];
eventTypes.forEach((type) => {
@@ -88,15 +88,15 @@ describe("events.ts", () => {
expect(callback).toHaveBeenCalledTimes(4);
});
it("should handle emitting without subscribers", () => {
it('should handle emitting without subscribers', () => {
const emitter = createEventEmitter();
expect(() => {
emitter.emit("agent:stream", { test: true });
emitter.emit('agent:stream', { test: true });
}).not.toThrow();
});
it("should allow multiple subscriptions and unsubscriptions", () => {
it('should allow multiple subscriptions and unsubscriptions', () => {
const emitter = createEventEmitter();
const callback1 = vi.fn();
const callback2 = vi.fn();
@@ -106,14 +106,14 @@ describe("events.ts", () => {
const unsub2 = emitter.subscribe(callback2);
const unsub3 = emitter.subscribe(callback3);
emitter.emit("feature:started", { test: 1 });
emitter.emit('feature:started', { test: 1 });
expect(callback1).toHaveBeenCalledOnce();
expect(callback2).toHaveBeenCalledOnce();
expect(callback3).toHaveBeenCalledOnce();
unsub2();
emitter.emit("feature:started", { test: 2 });
emitter.emit('feature:started', { test: 2 });
expect(callback1).toHaveBeenCalledTimes(2);
expect(callback2).toHaveBeenCalledOnce(); // Still just once
expect(callback3).toHaveBeenCalledTimes(2);
@@ -121,7 +121,7 @@ describe("events.ts", () => {
unsub1();
unsub3();
emitter.emit("feature:started", { test: 3 });
emitter.emit('feature:started', { test: 3 });
expect(callback1).toHaveBeenCalledTimes(2);
expect(callback2).toHaveBeenCalledOnce();
expect(callback3).toHaveBeenCalledTimes(2);

View File

@@ -1,13 +1,13 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { ProviderFactory } from "@/providers/provider-factory.js";
import { ClaudeProvider } from "@/providers/claude-provider.js";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ProviderFactory } from '@/providers/provider-factory.js';
import { ClaudeProvider } from '@/providers/claude-provider.js';
describe("provider-factory.ts", () => {
describe('provider-factory.ts', () => {
let consoleSpy: any;
beforeEach(() => {
consoleSpy = {
warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
};
});
@@ -15,55 +15,49 @@ describe("provider-factory.ts", () => {
consoleSpy.warn.mockRestore();
});
describe("getProviderForModel", () => {
describe("Claude models (claude-* prefix)", () => {
it("should return ClaudeProvider for claude-opus-4-5-20251101", () => {
const provider = ProviderFactory.getProviderForModel(
"claude-opus-4-5-20251101"
);
describe('getProviderForModel', () => {
describe('Claude models (claude-* prefix)', () => {
it('should return ClaudeProvider for claude-opus-4-5-20251101', () => {
const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for claude-sonnet-4-20250514", () => {
const provider = ProviderFactory.getProviderForModel(
"claude-sonnet-4-20250514"
);
it('should return ClaudeProvider for claude-sonnet-4-20250514', () => {
const provider = ProviderFactory.getProviderForModel('claude-sonnet-4-20250514');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for claude-haiku-4-5", () => {
const provider = ProviderFactory.getProviderForModel("claude-haiku-4-5");
it('should return ClaudeProvider for claude-haiku-4-5', () => {
const provider = ProviderFactory.getProviderForModel('claude-haiku-4-5');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should be case-insensitive for claude models", () => {
const provider = ProviderFactory.getProviderForModel(
"CLAUDE-OPUS-4-5-20251101"
);
it('should be case-insensitive for claude models', () => {
const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-5-20251101');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
});
describe("Claude aliases", () => {
describe('Claude aliases', () => {
it("should return ClaudeProvider for 'haiku'", () => {
const provider = ProviderFactory.getProviderForModel("haiku");
const provider = ProviderFactory.getProviderForModel('haiku');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for 'sonnet'", () => {
const provider = ProviderFactory.getProviderForModel("sonnet");
const provider = ProviderFactory.getProviderForModel('sonnet');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for 'opus'", () => {
const provider = ProviderFactory.getProviderForModel("opus");
const provider = ProviderFactory.getProviderForModel('opus');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should be case-insensitive for aliases", () => {
const provider1 = ProviderFactory.getProviderForModel("HAIKU");
const provider2 = ProviderFactory.getProviderForModel("Sonnet");
const provider3 = ProviderFactory.getProviderForModel("Opus");
it('should be case-insensitive for aliases', () => {
const provider1 = ProviderFactory.getProviderForModel('HAIKU');
const provider2 = ProviderFactory.getProviderForModel('Sonnet');
const provider3 = ProviderFactory.getProviderForModel('Opus');
expect(provider1).toBeInstanceOf(ClaudeProvider);
expect(provider2).toBeInstanceOf(ClaudeProvider);
@@ -71,65 +65,61 @@ describe("provider-factory.ts", () => {
});
});
describe("Unknown models", () => {
it("should default to ClaudeProvider for unknown model", () => {
const provider = ProviderFactory.getProviderForModel("unknown-model-123");
describe('Unknown models', () => {
it('should default to ClaudeProvider for unknown model', () => {
const provider = ProviderFactory.getProviderForModel('unknown-model-123');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should warn when defaulting to Claude", () => {
ProviderFactory.getProviderForModel("random-model");
it('should warn when defaulting to Claude', () => {
ProviderFactory.getProviderForModel('random-model');
expect(consoleSpy.warn).toHaveBeenCalledWith(
expect.stringContaining("Unknown model prefix")
expect.stringContaining('Unknown model prefix')
);
expect(consoleSpy.warn).toHaveBeenCalledWith(expect.stringContaining('random-model'));
expect(consoleSpy.warn).toHaveBeenCalledWith(
expect.stringContaining("random-model")
);
expect(consoleSpy.warn).toHaveBeenCalledWith(
expect.stringContaining("defaulting to Claude")
expect.stringContaining('defaulting to Claude')
);
});
it("should handle empty string", () => {
const provider = ProviderFactory.getProviderForModel("");
it('should handle empty string', () => {
const provider = ProviderFactory.getProviderForModel('');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(consoleSpy.warn).toHaveBeenCalled();
});
it("should default to ClaudeProvider for gpt models (not supported)", () => {
const provider = ProviderFactory.getProviderForModel("gpt-5.2");
it('should default to ClaudeProvider for gpt models (not supported)', () => {
const provider = ProviderFactory.getProviderForModel('gpt-5.2');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(consoleSpy.warn).toHaveBeenCalled();
});
it("should default to ClaudeProvider for o-series models (not supported)", () => {
const provider = ProviderFactory.getProviderForModel("o1");
it('should default to ClaudeProvider for o-series models (not supported)', () => {
const provider = ProviderFactory.getProviderForModel('o1');
expect(provider).toBeInstanceOf(ClaudeProvider);
expect(consoleSpy.warn).toHaveBeenCalled();
});
});
});
describe("getAllProviders", () => {
it("should return array of all providers", () => {
describe('getAllProviders', () => {
it('should return array of all providers', () => {
const providers = ProviderFactory.getAllProviders();
expect(Array.isArray(providers)).toBe(true);
});
it("should include ClaudeProvider", () => {
it('should include ClaudeProvider', () => {
const providers = ProviderFactory.getAllProviders();
const hasClaudeProvider = providers.some(
(p) => p instanceof ClaudeProvider
);
const hasClaudeProvider = providers.some((p) => p instanceof ClaudeProvider);
expect(hasClaudeProvider).toBe(true);
});
it("should return exactly 1 provider", () => {
it('should return exactly 1 provider', () => {
const providers = ProviderFactory.getAllProviders();
expect(providers).toHaveLength(1);
});
it("should create new instances each time", () => {
it('should create new instances each time', () => {
const providers1 = ProviderFactory.getAllProviders();
const providers2 = ProviderFactory.getAllProviders();
@@ -137,60 +127,60 @@ describe("provider-factory.ts", () => {
});
});
describe("checkAllProviders", () => {
it("should return installation status for all providers", async () => {
describe('checkAllProviders', () => {
it('should return installation status for all providers', async () => {
const statuses = await ProviderFactory.checkAllProviders();
expect(statuses).toHaveProperty("claude");
expect(statuses).toHaveProperty('claude');
});
it("should call detectInstallation on each provider", async () => {
it('should call detectInstallation on each provider', async () => {
const statuses = await ProviderFactory.checkAllProviders();
expect(statuses.claude).toHaveProperty("installed");
expect(statuses.claude).toHaveProperty('installed');
});
it("should return correct provider names as keys", async () => {
it('should return correct provider names as keys', async () => {
const statuses = await ProviderFactory.checkAllProviders();
const keys = Object.keys(statuses);
expect(keys).toContain("claude");
expect(keys).toContain('claude');
expect(keys).toHaveLength(1);
});
});
describe("getProviderByName", () => {
describe('getProviderByName', () => {
it("should return ClaudeProvider for 'claude'", () => {
const provider = ProviderFactory.getProviderByName("claude");
const provider = ProviderFactory.getProviderByName('claude');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should return ClaudeProvider for 'anthropic'", () => {
const provider = ProviderFactory.getProviderByName("anthropic");
const provider = ProviderFactory.getProviderByName('anthropic');
expect(provider).toBeInstanceOf(ClaudeProvider);
});
it("should be case-insensitive", () => {
const provider1 = ProviderFactory.getProviderByName("CLAUDE");
const provider2 = ProviderFactory.getProviderByName("ANTHROPIC");
it('should be case-insensitive', () => {
const provider1 = ProviderFactory.getProviderByName('CLAUDE');
const provider2 = ProviderFactory.getProviderByName('ANTHROPIC');
expect(provider1).toBeInstanceOf(ClaudeProvider);
expect(provider2).toBeInstanceOf(ClaudeProvider);
});
it("should return null for unknown provider", () => {
const provider = ProviderFactory.getProviderByName("unknown");
it('should return null for unknown provider', () => {
const provider = ProviderFactory.getProviderByName('unknown');
expect(provider).toBeNull();
});
it("should return null for empty string", () => {
const provider = ProviderFactory.getProviderByName("");
it('should return null for empty string', () => {
const provider = ProviderFactory.getProviderByName('');
expect(provider).toBeNull();
});
it("should create new instance each time", () => {
const provider1 = ProviderFactory.getProviderByName("claude");
const provider2 = ProviderFactory.getProviderByName("claude");
it('should create new instance each time', () => {
const provider1 = ProviderFactory.getProviderByName('claude');
const provider2 = ProviderFactory.getProviderByName('claude');
expect(provider1).not.toBe(provider2);
expect(provider1).toBeInstanceOf(ClaudeProvider);
@@ -198,35 +188,33 @@ describe("provider-factory.ts", () => {
});
});
describe("getAllAvailableModels", () => {
it("should return array of models", () => {
describe('getAllAvailableModels', () => {
it('should return array of models', () => {
const models = ProviderFactory.getAllAvailableModels();
expect(Array.isArray(models)).toBe(true);
});
it("should include models from all providers", () => {
it('should include models from all providers', () => {
const models = ProviderFactory.getAllAvailableModels();
expect(models.length).toBeGreaterThan(0);
});
it("should return models with required fields", () => {
it('should return models with required fields', () => {
const models = ProviderFactory.getAllAvailableModels();
models.forEach((model) => {
expect(model).toHaveProperty("id");
expect(model).toHaveProperty("name");
expect(typeof model.id).toBe("string");
expect(typeof model.name).toBe("string");
expect(model).toHaveProperty('id');
expect(model).toHaveProperty('name');
expect(typeof model.id).toBe('string');
expect(typeof model.name).toBe('string');
});
});
it("should include Claude models", () => {
it('should include Claude models', () => {
const models = ProviderFactory.getAllAvailableModels();
// Claude models should include claude-* in their IDs
const hasClaudeModels = models.some((m) =>
m.id.toLowerCase().includes("claude")
);
const hasClaudeModels = models.some((m) => m.id.toLowerCase().includes('claude'));
expect(hasClaudeModels).toBe(true);
});

View File

@@ -1,65 +1,59 @@
import { describe, it, expect, beforeEach } from "vitest";
import { describe, it, expect, beforeEach } from 'vitest';
import {
setRunningState,
getErrorMessage,
getSpecRegenerationStatus,
} from "@/routes/app-spec/common.js";
} from '@/routes/app-spec/common.js';
describe("app-spec/common.ts", () => {
describe('app-spec/common.ts', () => {
beforeEach(() => {
// Reset state before each test
setRunningState(false, null);
});
describe("setRunningState", () => {
it("should set isRunning to true when running is true", () => {
describe('setRunningState', () => {
it('should set isRunning to true when running is true', () => {
setRunningState(true);
expect(getSpecRegenerationStatus().isRunning).toBe(true);
});
it("should set isRunning to false when running is false", () => {
it('should set isRunning to false when running is false', () => {
setRunningState(true);
setRunningState(false);
expect(getSpecRegenerationStatus().isRunning).toBe(false);
});
it("should set currentAbortController when provided", () => {
it('should set currentAbortController when provided', () => {
const controller = new AbortController();
setRunningState(true, controller);
expect(getSpecRegenerationStatus().currentAbortController).toBe(
controller
);
expect(getSpecRegenerationStatus().currentAbortController).toBe(controller);
});
it("should set currentAbortController to null when not provided", () => {
it('should set currentAbortController to null when not provided', () => {
const controller = new AbortController();
setRunningState(true, controller);
setRunningState(false);
expect(getSpecRegenerationStatus().currentAbortController).toBe(null);
});
it("should set currentAbortController to null when explicitly passed null", () => {
it('should set currentAbortController to null when explicitly passed null', () => {
const controller = new AbortController();
setRunningState(true, controller);
setRunningState(true, null);
expect(getSpecRegenerationStatus().currentAbortController).toBe(null);
});
it("should update state multiple times correctly", () => {
it('should update state multiple times correctly', () => {
const controller1 = new AbortController();
const controller2 = new AbortController();
setRunningState(true, controller1);
expect(getSpecRegenerationStatus().isRunning).toBe(true);
expect(getSpecRegenerationStatus().currentAbortController).toBe(
controller1
);
expect(getSpecRegenerationStatus().currentAbortController).toBe(controller1);
setRunningState(true, controller2);
expect(getSpecRegenerationStatus().isRunning).toBe(true);
expect(getSpecRegenerationStatus().currentAbortController).toBe(
controller2
);
expect(getSpecRegenerationStatus().currentAbortController).toBe(controller2);
setRunningState(false, null);
expect(getSpecRegenerationStatus().isRunning).toBe(false);
@@ -67,42 +61,42 @@ describe("app-spec/common.ts", () => {
});
});
describe("getErrorMessage", () => {
it("should return message from Error instance", () => {
const error = new Error("Test error message");
expect(getErrorMessage(error)).toBe("Test error message");
describe('getErrorMessage', () => {
it('should return message from Error instance', () => {
const error = new Error('Test error message');
expect(getErrorMessage(error)).toBe('Test error message');
});
it("should return 'Unknown error' for non-Error objects", () => {
expect(getErrorMessage("string error")).toBe("Unknown error");
expect(getErrorMessage(123)).toBe("Unknown error");
expect(getErrorMessage(null)).toBe("Unknown error");
expect(getErrorMessage(undefined)).toBe("Unknown error");
expect(getErrorMessage({})).toBe("Unknown error");
expect(getErrorMessage([])).toBe("Unknown error");
expect(getErrorMessage('string error')).toBe('Unknown error');
expect(getErrorMessage(123)).toBe('Unknown error');
expect(getErrorMessage(null)).toBe('Unknown error');
expect(getErrorMessage(undefined)).toBe('Unknown error');
expect(getErrorMessage({})).toBe('Unknown error');
expect(getErrorMessage([])).toBe('Unknown error');
});
it("should return message from Error with empty message", () => {
const error = new Error("");
expect(getErrorMessage(error)).toBe("");
it('should return message from Error with empty message', () => {
const error = new Error('');
expect(getErrorMessage(error)).toBe('');
});
it("should handle Error objects with custom properties", () => {
const error = new Error("Base message");
(error as any).customProp = "custom value";
expect(getErrorMessage(error)).toBe("Base message");
it('should handle Error objects with custom properties', () => {
const error = new Error('Base message');
(error as any).customProp = 'custom value';
expect(getErrorMessage(error)).toBe('Base message');
});
it("should handle Error objects created with different constructors", () => {
it('should handle Error objects created with different constructors', () => {
class CustomError extends Error {
constructor(message: string) {
super(message);
this.name = "CustomError";
this.name = 'CustomError';
}
}
const customError = new CustomError("Custom error message");
expect(getErrorMessage(customError)).toBe("Custom error message");
const customError = new CustomError('Custom error message');
expect(getErrorMessage(customError)).toBe('Custom error message');
});
});
});

View File

@@ -1,11 +1,11 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect } from 'vitest';
describe("app-spec/parse-and-create-features.ts - JSON extraction", () => {
describe('app-spec/parse-and-create-features.ts - JSON extraction', () => {
// Test the JSON extraction regex pattern used in parseAndCreateFeatures
const jsonExtractionPattern = /\{[\s\S]*"features"[\s\S]*\}/;
describe("JSON extraction regex", () => {
it("should extract JSON with features array", () => {
describe('JSON extraction regex', () => {
it('should extract JSON with features array', () => {
const content = `Here is the response:
{
"features": [
@@ -26,7 +26,7 @@ describe("app-spec/parse-and-create-features.ts - JSON extraction", () => {
expect(match![0]).toContain('"id": "feature-1"');
});
it("should extract JSON with multiple features", () => {
it('should extract JSON with multiple features', () => {
const content = `Some text before
{
"features": [
@@ -49,7 +49,7 @@ Some text after`;
expect(match![0]).toContain('"feature-2"');
});
it("should extract JSON with nested objects and arrays", () => {
it('should extract JSON with nested objects and arrays', () => {
const content = `Response:
{
"features": [
@@ -69,7 +69,7 @@ Some text after`;
expect(match![0]).toContain('"dep-1"');
});
it("should handle JSON with whitespace and newlines", () => {
it('should handle JSON with whitespace and newlines', () => {
const content = `Text before
{
"features": [
@@ -87,7 +87,7 @@ Text after`;
expect(match![0]).toContain('"features"');
});
it("should extract JSON when features array is empty", () => {
it('should extract JSON when features array is empty', () => {
const content = `Response:
{
"features": []
@@ -96,10 +96,10 @@ Text after`;
const match = content.match(jsonExtractionPattern);
expect(match).not.toBeNull();
expect(match![0]).toContain('"features"');
expect(match![0]).toContain("[]");
expect(match![0]).toContain('[]');
});
it("should not match content without features key", () => {
it('should not match content without features key', () => {
const content = `{
"otherKey": "value"
}`;
@@ -108,13 +108,13 @@ Text after`;
expect(match).toBeNull();
});
it("should not match content without JSON structure", () => {
const content = "Just plain text with features mentioned";
it('should not match content without JSON structure', () => {
const content = 'Just plain text with features mentioned';
const match = content.match(jsonExtractionPattern);
expect(match).toBeNull();
});
it("should extract JSON when features key appears multiple times", () => {
it('should extract JSON when features key appears multiple times', () => {
const content = `Before:
{
"features": [
@@ -132,7 +132,7 @@ After: The word "features" appears again`;
expect(match![0]).toContain('"features"');
});
it("should handle JSON with escaped quotes", () => {
it('should handle JSON with escaped quotes', () => {
const content = `{
"features": [
{
@@ -147,7 +147,7 @@ After: The word "features" appears again`;
expect(match![0]).toContain('"features"');
});
it("should extract JSON with complex nested structure", () => {
it('should extract JSON with complex nested structure', () => {
const content = `Response:
{
"features": [
@@ -177,8 +177,8 @@ After: The word "features" appears again`;
});
});
describe("JSON parsing validation", () => {
it("should parse valid feature JSON structure", () => {
describe('JSON parsing validation', () => {
it('should parse valid feature JSON structure', () => {
const validJson = `{
"features": [
{
@@ -196,11 +196,11 @@ After: The word "features" appears again`;
expect(parsed.features).toBeDefined();
expect(Array.isArray(parsed.features)).toBe(true);
expect(parsed.features.length).toBe(1);
expect(parsed.features[0].id).toBe("feature-1");
expect(parsed.features[0].title).toBe("Test Feature");
expect(parsed.features[0].id).toBe('feature-1');
expect(parsed.features[0].title).toBe('Test Feature');
});
it("should handle features with optional fields", () => {
it('should handle features with optional fields', () => {
const jsonWithOptionalFields = `{
"features": [
{
@@ -213,14 +213,14 @@ After: The word "features" appears again`;
}`;
const parsed = JSON.parse(jsonWithOptionalFields);
expect(parsed.features[0].id).toBe("feature-1");
expect(parsed.features[0].id).toBe('feature-1');
expect(parsed.features[0].priority).toBe(2);
// description and dependencies are optional
expect(parsed.features[0].description).toBeUndefined();
expect(parsed.features[0].dependencies).toBeUndefined();
});
it("should handle features with dependencies", () => {
it('should handle features with dependencies', () => {
const jsonWithDeps = `{
"features": [
{
@@ -238,7 +238,7 @@ After: The word "features" appears again`;
const parsed = JSON.parse(jsonWithDeps);
expect(parsed.features[0].dependencies).toEqual([]);
expect(parsed.features[1].dependencies).toEqual(["feature-1"]);
expect(parsed.features[1].dependencies).toEqual(['feature-1']);
});
});
});

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { AutoModeService } from "@/services/auto-mode-service.js";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AutoModeService } from '@/services/auto-mode-service.js';
describe("auto-mode-service.ts - Planning Mode", () => {
describe('auto-mode-service.ts - Planning Mode', () => {
let service: AutoModeService;
const mockEvents = {
subscribe: vi.fn(),
@@ -18,98 +18,98 @@ describe("auto-mode-service.ts - Planning Mode", () => {
await service.stopAutoLoop().catch(() => {});
});
describe("getPlanningPromptPrefix", () => {
describe('getPlanningPromptPrefix', () => {
// Access private method through any cast for testing
const getPlanningPromptPrefix = (svc: any, feature: any) => {
return svc.getPlanningPromptPrefix(feature);
};
it("should return empty string for skip mode", () => {
const feature = { id: "test", planningMode: "skip" as const };
it('should return empty string for skip mode', () => {
const feature = { id: 'test', planningMode: 'skip' as const };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toBe("");
expect(result).toBe('');
});
it("should return empty string when planningMode is undefined", () => {
const feature = { id: "test" };
it('should return empty string when planningMode is undefined', () => {
const feature = { id: 'test' };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toBe("");
expect(result).toBe('');
});
it("should return lite prompt for lite mode without approval", () => {
it('should return lite prompt for lite mode without approval', () => {
const feature = {
id: "test",
planningMode: "lite" as const,
requirePlanApproval: false
id: 'test',
planningMode: 'lite' as const,
requirePlanApproval: false,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Planning Phase (Lite Mode)");
expect(result).toContain("[PLAN_GENERATED]");
expect(result).toContain("Feature Request");
expect(result).toContain('Planning Phase (Lite Mode)');
expect(result).toContain('[PLAN_GENERATED]');
expect(result).toContain('Feature Request');
});
it("should return lite_with_approval prompt for lite mode with approval", () => {
it('should return lite_with_approval prompt for lite mode with approval', () => {
const feature = {
id: "test",
planningMode: "lite" as const,
requirePlanApproval: true
id: 'test',
planningMode: 'lite' as const,
requirePlanApproval: true,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Planning Phase (Lite Mode)");
expect(result).toContain("[SPEC_GENERATED]");
expect(result).toContain("DO NOT proceed with implementation");
expect(result).toContain('Planning Phase (Lite Mode)');
expect(result).toContain('[SPEC_GENERATED]');
expect(result).toContain('DO NOT proceed with implementation');
});
it("should return spec prompt for spec mode", () => {
it('should return spec prompt for spec mode', () => {
const feature = {
id: "test",
planningMode: "spec" as const
id: 'test',
planningMode: 'spec' as const,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Specification Phase (Spec Mode)");
expect(result).toContain("```tasks");
expect(result).toContain("T001");
expect(result).toContain("[TASK_START]");
expect(result).toContain("[TASK_COMPLETE]");
expect(result).toContain('Specification Phase (Spec Mode)');
expect(result).toContain('```tasks');
expect(result).toContain('T001');
expect(result).toContain('[TASK_START]');
expect(result).toContain('[TASK_COMPLETE]');
});
it("should return full prompt for full mode", () => {
it('should return full prompt for full mode', () => {
const feature = {
id: "test",
planningMode: "full" as const
id: 'test',
planningMode: 'full' as const,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Full Specification Phase (Full SDD Mode)");
expect(result).toContain("Phase 1: Foundation");
expect(result).toContain("Phase 2: Core Implementation");
expect(result).toContain("Phase 3: Integration & Testing");
expect(result).toContain('Full Specification Phase (Full SDD Mode)');
expect(result).toContain('Phase 1: Foundation');
expect(result).toContain('Phase 2: Core Implementation');
expect(result).toContain('Phase 3: Integration & Testing');
});
it("should include the separator and Feature Request header", () => {
it('should include the separator and Feature Request header', () => {
const feature = {
id: "test",
planningMode: "spec" as const
id: 'test',
planningMode: 'spec' as const,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("---");
expect(result).toContain("## Feature Request");
expect(result).toContain('---');
expect(result).toContain('## Feature Request');
});
it("should instruct agent to NOT output exploration text", () => {
const modes = ["lite", "spec", "full"] as const;
it('should instruct agent to NOT output exploration text', () => {
const modes = ['lite', 'spec', 'full'] as const;
for (const mode of modes) {
const feature = { id: "test", planningMode: mode };
const feature = { id: 'test', planningMode: mode };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Do NOT output exploration text");
expect(result).toContain("Start DIRECTLY");
expect(result).toContain('Do NOT output exploration text');
expect(result).toContain('Start DIRECTLY');
}
});
});
describe("parseTasksFromSpec (via module)", () => {
describe('parseTasksFromSpec (via module)', () => {
// We need to test the module-level function
// Import it directly for testing
it("should parse tasks from a valid tasks block", async () => {
it('should parse tasks from a valid tasks block', async () => {
// This tests the internal logic through integration
// The function is module-level, so we verify behavior through the service
const specContent = `
@@ -123,12 +123,12 @@ describe("auto-mode-service.ts - Planning Mode", () => {
`;
// Since parseTasksFromSpec is a module-level function,
// we verify its behavior indirectly through plan parsing
expect(specContent).toContain("T001");
expect(specContent).toContain("T002");
expect(specContent).toContain("T003");
expect(specContent).toContain('T001');
expect(specContent).toContain('T002');
expect(specContent).toContain('T003');
});
it("should handle tasks block with phases", () => {
it('should handle tasks block with phases', () => {
const specContent = `
\`\`\`tasks
## Phase 1: Setup
@@ -139,190 +139,191 @@ describe("auto-mode-service.ts - Planning Mode", () => {
- [ ] T003: Create main module | File: src/index.ts
\`\`\`
`;
expect(specContent).toContain("Phase 1");
expect(specContent).toContain("Phase 2");
expect(specContent).toContain("T001");
expect(specContent).toContain("T003");
expect(specContent).toContain('Phase 1');
expect(specContent).toContain('Phase 2');
expect(specContent).toContain('T001');
expect(specContent).toContain('T003');
});
});
describe("plan approval flow", () => {
it("should track pending approvals correctly", () => {
expect(service.hasPendingApproval("test-feature")).toBe(false);
describe('plan approval flow', () => {
it('should track pending approvals correctly', () => {
expect(service.hasPendingApproval('test-feature')).toBe(false);
});
it("should allow cancelling non-existent approval without error", () => {
expect(() => service.cancelPlanApproval("non-existent")).not.toThrow();
it('should allow cancelling non-existent approval without error', () => {
expect(() => service.cancelPlanApproval('non-existent')).not.toThrow();
});
it("should return running features count after stop", async () => {
it('should return running features count after stop', async () => {
const count = await service.stopAutoLoop();
expect(count).toBe(0);
});
});
describe("resolvePlanApproval", () => {
it("should return error when no pending approval exists", async () => {
describe('resolvePlanApproval', () => {
it('should return error when no pending approval exists', async () => {
const result = await service.resolvePlanApproval(
"non-existent-feature",
'non-existent-feature',
true,
undefined,
undefined,
undefined
);
expect(result.success).toBe(false);
expect(result.error).toContain("No pending approval");
expect(result.error).toContain('No pending approval');
});
it("should handle approval with edited plan", async () => {
it('should handle approval with edited plan', async () => {
// Without a pending approval, this should fail gracefully
const result = await service.resolvePlanApproval(
"test-feature",
'test-feature',
true,
"Edited plan content",
'Edited plan content',
undefined,
undefined
);
expect(result.success).toBe(false);
});
it("should handle rejection with feedback", async () => {
it('should handle rejection with feedback', async () => {
const result = await service.resolvePlanApproval(
"test-feature",
'test-feature',
false,
undefined,
"Please add more details",
'Please add more details',
undefined
);
expect(result.success).toBe(false);
});
});
describe("buildFeaturePrompt", () => {
describe('buildFeaturePrompt', () => {
const buildFeaturePrompt = (svc: any, feature: any) => {
return svc.buildFeaturePrompt(feature);
};
it("should include feature ID and description", () => {
it('should include feature ID and description', () => {
const feature = {
id: "feat-123",
description: "Add user authentication",
id: 'feat-123',
description: 'Add user authentication',
};
const result = buildFeaturePrompt(service, feature);
expect(result).toContain("feat-123");
expect(result).toContain("Add user authentication");
expect(result).toContain('feat-123');
expect(result).toContain('Add user authentication');
});
it("should include specification when present", () => {
it('should include specification when present', () => {
const feature = {
id: "feat-123",
description: "Test feature",
spec: "Detailed specification here",
id: 'feat-123',
description: 'Test feature',
spec: 'Detailed specification here',
};
const result = buildFeaturePrompt(service, feature);
expect(result).toContain("Specification:");
expect(result).toContain("Detailed specification here");
expect(result).toContain('Specification:');
expect(result).toContain('Detailed specification here');
});
it("should include image paths when present", () => {
it('should include image paths when present', () => {
const feature = {
id: "feat-123",
description: "Test feature",
id: 'feat-123',
description: 'Test feature',
imagePaths: [
{ path: "/tmp/image1.png", filename: "image1.png", mimeType: "image/png" },
"/tmp/image2.jpg",
{ path: '/tmp/image1.png', filename: 'image1.png', mimeType: 'image/png' },
'/tmp/image2.jpg',
],
};
const result = buildFeaturePrompt(service, feature);
expect(result).toContain("Context Images Attached");
expect(result).toContain("image1.png");
expect(result).toContain("/tmp/image2.jpg");
expect(result).toContain('Context Images Attached');
expect(result).toContain('image1.png');
expect(result).toContain('/tmp/image2.jpg');
});
it("should include summary tags instruction", () => {
it('should include summary tags instruction', () => {
const feature = {
id: "feat-123",
description: "Test feature",
id: 'feat-123',
description: 'Test feature',
};
const result = buildFeaturePrompt(service, feature);
expect(result).toContain("<summary>");
expect(result).toContain("</summary>");
expect(result).toContain('<summary>');
expect(result).toContain('</summary>');
});
});
describe("extractTitleFromDescription", () => {
describe('extractTitleFromDescription', () => {
const extractTitle = (svc: any, description: string) => {
return svc.extractTitleFromDescription(description);
};
it("should return 'Untitled Feature' for empty description", () => {
expect(extractTitle(service, "")).toBe("Untitled Feature");
expect(extractTitle(service, " ")).toBe("Untitled Feature");
expect(extractTitle(service, '')).toBe('Untitled Feature');
expect(extractTitle(service, ' ')).toBe('Untitled Feature');
});
it("should return first line if under 60 characters", () => {
const description = "Add user login\nWith email validation";
expect(extractTitle(service, description)).toBe("Add user login");
it('should return first line if under 60 characters', () => {
const description = 'Add user login\nWith email validation';
expect(extractTitle(service, description)).toBe('Add user login');
});
it("should truncate long first lines to 60 characters", () => {
const description = "This is a very long feature description that exceeds the sixty character limit significantly";
it('should truncate long first lines to 60 characters', () => {
const description =
'This is a very long feature description that exceeds the sixty character limit significantly';
const result = extractTitle(service, description);
expect(result.length).toBe(60);
expect(result).toContain("...");
expect(result).toContain('...');
});
});
describe("PLANNING_PROMPTS structure", () => {
describe('PLANNING_PROMPTS structure', () => {
const getPlanningPromptPrefix = (svc: any, feature: any) => {
return svc.getPlanningPromptPrefix(feature);
};
it("should have all required planning modes", () => {
const modes = ["lite", "spec", "full"] as const;
it('should have all required planning modes', () => {
const modes = ['lite', 'spec', 'full'] as const;
for (const mode of modes) {
const feature = { id: "test", planningMode: mode };
const feature = { id: 'test', planningMode: mode };
const result = getPlanningPromptPrefix(service, feature);
expect(result.length).toBeGreaterThan(100);
}
});
it("lite prompt should include correct structure", () => {
const feature = { id: "test", planningMode: "lite" as const };
it('lite prompt should include correct structure', () => {
const feature = { id: 'test', planningMode: 'lite' as const };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Goal");
expect(result).toContain("Approach");
expect(result).toContain("Files to Touch");
expect(result).toContain("Tasks");
expect(result).toContain("Risks");
expect(result).toContain('Goal');
expect(result).toContain('Approach');
expect(result).toContain('Files to Touch');
expect(result).toContain('Tasks');
expect(result).toContain('Risks');
});
it("spec prompt should include task format instructions", () => {
const feature = { id: "test", planningMode: "spec" as const };
it('spec prompt should include task format instructions', () => {
const feature = { id: 'test', planningMode: 'spec' as const };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Problem");
expect(result).toContain("Solution");
expect(result).toContain("Acceptance Criteria");
expect(result).toContain("GIVEN-WHEN-THEN");
expect(result).toContain("Implementation Tasks");
expect(result).toContain("Verification");
expect(result).toContain('Problem');
expect(result).toContain('Solution');
expect(result).toContain('Acceptance Criteria');
expect(result).toContain('GIVEN-WHEN-THEN');
expect(result).toContain('Implementation Tasks');
expect(result).toContain('Verification');
});
it("full prompt should include phases", () => {
const feature = { id: "test", planningMode: "full" as const };
it('full prompt should include phases', () => {
const feature = { id: 'test', planningMode: 'full' as const };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain("Problem Statement");
expect(result).toContain("User Story");
expect(result).toContain("Technical Context");
expect(result).toContain("Non-Goals");
expect(result).toContain("Phase 1");
expect(result).toContain("Phase 2");
expect(result).toContain("Phase 3");
expect(result).toContain('Problem Statement');
expect(result).toContain('User Story');
expect(result).toContain('Technical Context');
expect(result).toContain('Non-Goals');
expect(result).toContain('Phase 1');
expect(result).toContain('Phase 2');
expect(result).toContain('Phase 3');
});
});
describe("status management", () => {
it("should report correct status", () => {
describe('status management', () => {
it('should report correct status', () => {
const status = service.getStatus();
expect(status.runningFeatures).toEqual([]);
expect(status.isRunning).toBe(false);

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { AutoModeService } from "@/services/auto-mode-service.js";
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AutoModeService } from '@/services/auto-mode-service.js';
describe("auto-mode-service.ts", () => {
describe('auto-mode-service.ts', () => {
let service: AutoModeService;
const mockEvents = {
subscribe: vi.fn(),
@@ -13,29 +13,27 @@ describe("auto-mode-service.ts", () => {
service = new AutoModeService(mockEvents as any);
});
describe("constructor", () => {
it("should initialize with event emitter", () => {
describe('constructor', () => {
it('should initialize with event emitter', () => {
expect(service).toBeDefined();
});
});
describe("startAutoLoop", () => {
it("should throw if auto mode is already running", async () => {
describe('startAutoLoop', () => {
it('should throw if auto mode is already running', async () => {
// Start first loop
const promise1 = service.startAutoLoop("/test/project", 3);
const promise1 = service.startAutoLoop('/test/project', 3);
// Try to start second loop
await expect(
service.startAutoLoop("/test/project", 3)
).rejects.toThrow("already running");
await expect(service.startAutoLoop('/test/project', 3)).rejects.toThrow('already running');
// Cleanup
await service.stopAutoLoop();
await promise1.catch(() => {});
});
it("should emit auto mode start event", async () => {
const promise = service.startAutoLoop("/test/project", 3);
it('should emit auto mode start event', async () => {
const promise = service.startAutoLoop('/test/project', 3);
// Give it time to emit the event
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -43,7 +41,7 @@ describe("auto-mode-service.ts", () => {
expect(mockEvents.emit).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
message: expect.stringContaining("Auto mode started"),
message: expect.stringContaining('Auto mode started'),
})
);
@@ -53,9 +51,9 @@ describe("auto-mode-service.ts", () => {
});
});
describe("stopAutoLoop", () => {
it("should stop the auto loop", async () => {
const promise = service.startAutoLoop("/test/project", 3);
describe('stopAutoLoop', () => {
it('should stop the auto loop', async () => {
const promise = service.startAutoLoop('/test/project', 3);
const runningCount = await service.stopAutoLoop();
@@ -63,7 +61,7 @@ describe("auto-mode-service.ts", () => {
await promise.catch(() => {});
});
it("should return 0 when not running", async () => {
it('should return 0 when not running', async () => {
const runningCount = await service.stopAutoLoop();
expect(runningCount).toBe(0);
});

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect } from 'vitest';
/**
* Test the task parsing logic by reimplementing the parsing functions
@@ -88,59 +88,59 @@ function parseTasksFromSpec(specContent: string): ParsedTask[] {
return tasks;
}
describe("Task Parsing", () => {
describe("parseTaskLine", () => {
it("should parse task with file path", () => {
const line = "- [ ] T001: Create user model | File: src/models/user.ts";
describe('Task Parsing', () => {
describe('parseTaskLine', () => {
it('should parse task with file path', () => {
const line = '- [ ] T001: Create user model | File: src/models/user.ts';
const result = parseTaskLine(line);
expect(result).toEqual({
id: "T001",
description: "Create user model",
filePath: "src/models/user.ts",
id: 'T001',
description: 'Create user model',
filePath: 'src/models/user.ts',
phase: undefined,
status: "pending",
status: 'pending',
});
});
it("should parse task without file path", () => {
const line = "- [ ] T002: Setup database connection";
it('should parse task without file path', () => {
const line = '- [ ] T002: Setup database connection';
const result = parseTaskLine(line);
expect(result).toEqual({
id: "T002",
description: "Setup database connection",
id: 'T002',
description: 'Setup database connection',
phase: undefined,
status: "pending",
status: 'pending',
});
});
it("should include phase when provided", () => {
const line = "- [ ] T003: Write tests | File: tests/user.test.ts";
const result = parseTaskLine(line, "Phase 1: Foundation");
expect(result?.phase).toBe("Phase 1: Foundation");
it('should include phase when provided', () => {
const line = '- [ ] T003: Write tests | File: tests/user.test.ts';
const result = parseTaskLine(line, 'Phase 1: Foundation');
expect(result?.phase).toBe('Phase 1: Foundation');
});
it("should return null for invalid line", () => {
expect(parseTaskLine("- [ ] Invalid format")).toBeNull();
expect(parseTaskLine("Not a task line")).toBeNull();
expect(parseTaskLine("")).toBeNull();
it('should return null for invalid line', () => {
expect(parseTaskLine('- [ ] Invalid format')).toBeNull();
expect(parseTaskLine('Not a task line')).toBeNull();
expect(parseTaskLine('')).toBeNull();
});
it("should handle multi-word descriptions", () => {
const line = "- [ ] T004: Implement user authentication with JWT tokens | File: src/auth.ts";
it('should handle multi-word descriptions', () => {
const line = '- [ ] T004: Implement user authentication with JWT tokens | File: src/auth.ts';
const result = parseTaskLine(line);
expect(result?.description).toBe("Implement user authentication with JWT tokens");
expect(result?.description).toBe('Implement user authentication with JWT tokens');
});
it("should trim whitespace from description and file path", () => {
const line = "- [ ] T005: Create API endpoint | File: src/routes/api.ts ";
it('should trim whitespace from description and file path', () => {
const line = '- [ ] T005: Create API endpoint | File: src/routes/api.ts ';
const result = parseTaskLine(line);
expect(result?.description).toBe("Create API endpoint");
expect(result?.filePath).toBe("src/routes/api.ts");
expect(result?.description).toBe('Create API endpoint');
expect(result?.filePath).toBe('src/routes/api.ts');
});
});
describe("parseTasksFromSpec", () => {
it("should parse tasks from a tasks code block", () => {
describe('parseTasksFromSpec', () => {
it('should parse tasks from a tasks code block', () => {
const specContent = `
## Specification
@@ -157,12 +157,12 @@ Some notes here.
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(3);
expect(tasks[0].id).toBe("T001");
expect(tasks[1].id).toBe("T002");
expect(tasks[2].id).toBe("T003");
expect(tasks[0].id).toBe('T001');
expect(tasks[1].id).toBe('T002');
expect(tasks[2].id).toBe('T003');
});
it("should parse tasks with phases", () => {
it('should parse tasks with phases', () => {
const specContent = `
\`\`\`tasks
## Phase 1: Foundation
@@ -179,20 +179,20 @@ Some notes here.
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(5);
expect(tasks[0].phase).toBe("Phase 1: Foundation");
expect(tasks[1].phase).toBe("Phase 1: Foundation");
expect(tasks[2].phase).toBe("Phase 2: Implementation");
expect(tasks[3].phase).toBe("Phase 2: Implementation");
expect(tasks[4].phase).toBe("Phase 3: Testing");
expect(tasks[0].phase).toBe('Phase 1: Foundation');
expect(tasks[1].phase).toBe('Phase 1: Foundation');
expect(tasks[2].phase).toBe('Phase 2: Implementation');
expect(tasks[3].phase).toBe('Phase 2: Implementation');
expect(tasks[4].phase).toBe('Phase 3: Testing');
});
it("should return empty array for content without tasks", () => {
const specContent = "Just some text without any tasks";
it('should return empty array for content without tasks', () => {
const specContent = 'Just some text without any tasks';
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toEqual([]);
});
it("should fallback to finding task lines outside code block", () => {
it('should fallback to finding task lines outside code block', () => {
const specContent = `
## Implementation Plan
@@ -201,11 +201,11 @@ Some notes here.
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(2);
expect(tasks[0].id).toBe("T001");
expect(tasks[1].id).toBe("T002");
expect(tasks[0].id).toBe('T001');
expect(tasks[1].id).toBe('T002');
});
it("should handle empty tasks block", () => {
it('should handle empty tasks block', () => {
const specContent = `
\`\`\`tasks
\`\`\`
@@ -214,7 +214,7 @@ Some notes here.
expect(tasks).toEqual([]);
});
it("should handle mixed valid and invalid lines", () => {
it('should handle mixed valid and invalid lines', () => {
const specContent = `
\`\`\`tasks
- [ ] T001: Valid task | File: src/valid.ts
@@ -227,7 +227,7 @@ Some other text
expect(tasks).toHaveLength(2);
});
it("should preserve task order", () => {
it('should preserve task order', () => {
const specContent = `
\`\`\`tasks
- [ ] T003: Third
@@ -236,12 +236,12 @@ Some other text
\`\`\`
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks[0].id).toBe("T003");
expect(tasks[1].id).toBe("T001");
expect(tasks[2].id).toBe("T002");
expect(tasks[0].id).toBe('T003');
expect(tasks[1].id).toBe('T001');
expect(tasks[2].id).toBe('T002');
});
it("should handle task IDs with different numbers", () => {
it('should handle task IDs with different numbers', () => {
const specContent = `
\`\`\`tasks
- [ ] T001: First
@@ -251,14 +251,14 @@ Some other text
`;
const tasks = parseTasksFromSpec(specContent);
expect(tasks).toHaveLength(3);
expect(tasks[0].id).toBe("T001");
expect(tasks[1].id).toBe("T010");
expect(tasks[2].id).toBe("T100");
expect(tasks[0].id).toBe('T001');
expect(tasks[1].id).toBe('T010');
expect(tasks[2].id).toBe('T100');
});
});
describe("spec content generation patterns", () => {
it("should match the expected lite mode output format", () => {
describe('spec content generation patterns', () => {
it('should match the expected lite mode output format', () => {
const liteModeOutput = `
1. **Goal**: Implement user registration
2. **Approach**: Create form component, add validation, connect to API
@@ -271,12 +271,12 @@ Some other text
[PLAN_GENERATED] Planning outline complete.
`;
expect(liteModeOutput).toContain("[PLAN_GENERATED]");
expect(liteModeOutput).toContain("Goal");
expect(liteModeOutput).toContain("Approach");
expect(liteModeOutput).toContain('[PLAN_GENERATED]');
expect(liteModeOutput).toContain('Goal');
expect(liteModeOutput).toContain('Approach');
});
it("should match the expected spec mode output format", () => {
it('should match the expected spec mode output format', () => {
const specModeOutput = `
1. **Problem**: Users cannot register for accounts
@@ -300,12 +300,12 @@ Some other text
[SPEC_GENERATED] Please review the specification above.
`;
expect(specModeOutput).toContain("[SPEC_GENERATED]");
expect(specModeOutput).toContain("```tasks");
expect(specModeOutput).toContain("T001");
expect(specModeOutput).toContain('[SPEC_GENERATED]');
expect(specModeOutput).toContain('```tasks');
expect(specModeOutput).toContain('T001');
});
it("should match the expected full mode output format", () => {
it('should match the expected full mode output format', () => {
const fullModeOutput = `
1. **Problem Statement**: Users need ability to create accounts
@@ -336,10 +336,10 @@ Some other text
[SPEC_GENERATED] Please review the comprehensive specification above.
`;
expect(fullModeOutput).toContain("Phase 1");
expect(fullModeOutput).toContain("Phase 2");
expect(fullModeOutput).toContain("Phase 3");
expect(fullModeOutput).toContain("[SPEC_GENERATED]");
expect(fullModeOutput).toContain('Phase 1');
expect(fullModeOutput).toContain('Phase 2');
expect(fullModeOutput).toContain('Phase 3');
expect(fullModeOutput).toContain('[SPEC_GENERATED]');
});
});
});

View File

@@ -1,14 +1,14 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { TerminalService, getTerminalService } from "@/services/terminal-service.js";
import * as pty from "node-pty";
import * as os from "os";
import * as fs from "fs";
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { TerminalService, getTerminalService } from '@/services/terminal-service.js';
import * as pty from 'node-pty';
import * as os from 'os';
import * as fs from 'fs';
vi.mock("node-pty");
vi.mock("fs");
vi.mock("os");
vi.mock('node-pty');
vi.mock('fs');
vi.mock('os');
describe("terminal-service.ts", () => {
describe('terminal-service.ts', () => {
let service: TerminalService;
let mockPtyProcess: any;
@@ -26,225 +26,225 @@ describe("terminal-service.ts", () => {
};
vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
vi.mocked(os.homedir).mockReturnValue("/home/user");
vi.mocked(os.platform).mockReturnValue("linux");
vi.mocked(os.arch).mockReturnValue("x64");
vi.mocked(os.homedir).mockReturnValue('/home/user');
vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.arch).mockReturnValue('x64');
});
afterEach(() => {
service.cleanup();
});
describe("detectShell", () => {
it("should detect PowerShell Core on Windows when available", () => {
vi.mocked(os.platform).mockReturnValue("win32");
describe('detectShell', () => {
it('should detect PowerShell Core on Windows when available', () => {
vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
return path === "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
return path === 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
});
const result = service.detectShell();
expect(result.shell).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe");
expect(result.shell).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe');
expect(result.args).toEqual([]);
});
it("should fall back to PowerShell on Windows if Core not available", () => {
vi.mocked(os.platform).mockReturnValue("win32");
it('should fall back to PowerShell on Windows if Core not available', () => {
vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
return path === "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
return path === 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
});
const result = service.detectShell();
expect(result.shell).toBe("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
expect(result.shell).toBe('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe');
expect(result.args).toEqual([]);
});
it("should fall back to cmd.exe on Windows if no PowerShell", () => {
vi.mocked(os.platform).mockReturnValue("win32");
it('should fall back to cmd.exe on Windows if no PowerShell', () => {
vi.mocked(os.platform).mockReturnValue('win32');
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = service.detectShell();
expect(result.shell).toBe("cmd.exe");
expect(result.shell).toBe('cmd.exe');
expect(result.args).toEqual([]);
});
it("should detect user shell on macOS", () => {
vi.mocked(os.platform).mockReturnValue("darwin");
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/zsh" });
it('should detect user shell on macOS', () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/zsh' });
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = service.detectShell();
expect(result.shell).toBe("/bin/zsh");
expect(result.args).toEqual(["--login"]);
expect(result.shell).toBe('/bin/zsh');
expect(result.args).toEqual(['--login']);
});
it("should fall back to zsh on macOS if user shell not available", () => {
vi.mocked(os.platform).mockReturnValue("darwin");
vi.spyOn(process, "env", "get").mockReturnValue({});
it('should fall back to zsh on macOS if user shell not available', () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
return path === "/bin/zsh";
return path === '/bin/zsh';
});
const result = service.detectShell();
expect(result.shell).toBe("/bin/zsh");
expect(result.args).toEqual(["--login"]);
expect(result.shell).toBe('/bin/zsh');
expect(result.args).toEqual(['--login']);
});
it("should fall back to bash on macOS if zsh not available", () => {
vi.mocked(os.platform).mockReturnValue("darwin");
vi.spyOn(process, "env", "get").mockReturnValue({});
it('should fall back to bash on macOS if zsh not available', () => {
vi.mocked(os.platform).mockReturnValue('darwin');
vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = service.detectShell();
expect(result.shell).toBe("/bin/bash");
expect(result.args).toEqual(["--login"]);
expect(result.shell).toBe('/bin/bash');
expect(result.args).toEqual(['--login']);
});
it("should detect user shell on Linux", () => {
vi.mocked(os.platform).mockReturnValue("linux");
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
it('should detect user shell on Linux', () => {
vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = service.detectShell();
expect(result.shell).toBe("/bin/bash");
expect(result.args).toEqual(["--login"]);
expect(result.shell).toBe('/bin/bash');
expect(result.args).toEqual(['--login']);
});
it("should fall back to bash on Linux if user shell not available", () => {
vi.mocked(os.platform).mockReturnValue("linux");
vi.spyOn(process, "env", "get").mockReturnValue({});
it('should fall back to bash on Linux if user shell not available', () => {
vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockImplementation((path: any) => {
return path === "/bin/bash";
return path === '/bin/bash';
});
const result = service.detectShell();
expect(result.shell).toBe("/bin/bash");
expect(result.args).toEqual(["--login"]);
expect(result.shell).toBe('/bin/bash');
expect(result.args).toEqual(['--login']);
});
it("should fall back to sh on Linux if bash not available", () => {
vi.mocked(os.platform).mockReturnValue("linux");
vi.spyOn(process, "env", "get").mockReturnValue({});
it('should fall back to sh on Linux if bash not available', () => {
vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({});
vi.mocked(fs.existsSync).mockReturnValue(false);
const result = service.detectShell();
expect(result.shell).toBe("/bin/sh");
expect(result.shell).toBe('/bin/sh');
expect(result.args).toEqual([]);
});
it("should detect WSL and use appropriate shell", () => {
vi.mocked(os.platform).mockReturnValue("linux");
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
it('should detect WSL and use appropriate shell', () => {
vi.mocked(os.platform).mockReturnValue('linux');
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-microsoft-standard-WSL2");
vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2');
const result = service.detectShell();
expect(result.shell).toBe("/bin/bash");
expect(result.args).toEqual(["--login"]);
expect(result.shell).toBe('/bin/bash');
expect(result.args).toEqual(['--login']);
});
});
describe("isWSL", () => {
it("should return true if /proc/version contains microsoft", () => {
describe('isWSL', () => {
it('should return true if /proc/version contains microsoft', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-microsoft-standard-WSL2");
vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2');
expect(service.isWSL()).toBe(true);
});
it("should return true if /proc/version contains wsl", () => {
it('should return true if /proc/version contains wsl', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("Linux version 5.10.0-wsl2");
vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-wsl2');
expect(service.isWSL()).toBe(true);
});
it("should return true if WSL_DISTRO_NAME is set", () => {
it('should return true if WSL_DISTRO_NAME is set', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.spyOn(process, "env", "get").mockReturnValue({ WSL_DISTRO_NAME: "Ubuntu" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ WSL_DISTRO_NAME: 'Ubuntu' });
expect(service.isWSL()).toBe(true);
});
it("should return true if WSLENV is set", () => {
it('should return true if WSLENV is set', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.spyOn(process, "env", "get").mockReturnValue({ WSLENV: "PATH/l" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ WSLENV: 'PATH/l' });
expect(service.isWSL()).toBe(true);
});
it("should return false if not in WSL", () => {
it('should return false if not in WSL', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.spyOn(process, "env", "get").mockReturnValue({});
vi.spyOn(process, 'env', 'get').mockReturnValue({});
expect(service.isWSL()).toBe(false);
});
it("should return false if error reading /proc/version", () => {
it('should return false if error reading /proc/version', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockImplementation(() => {
throw new Error("Permission denied");
throw new Error('Permission denied');
});
expect(service.isWSL()).toBe(false);
});
});
describe("getPlatformInfo", () => {
it("should return platform information", () => {
vi.mocked(os.platform).mockReturnValue("linux");
vi.mocked(os.arch).mockReturnValue("x64");
describe('getPlatformInfo', () => {
it('should return platform information', () => {
vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.arch).mockReturnValue('x64');
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const info = service.getPlatformInfo();
expect(info.platform).toBe("linux");
expect(info.arch).toBe("x64");
expect(info.defaultShell).toBe("/bin/bash");
expect(typeof info.isWSL).toBe("boolean");
expect(info.platform).toBe('linux');
expect(info.arch).toBe('x64');
expect(info.defaultShell).toBe('/bin/bash');
expect(typeof info.isWSL).toBe('boolean');
});
});
describe("createSession", () => {
it("should create a new terminal session", () => {
describe('createSession', () => {
it('should create a new terminal session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({
cwd: "/test/dir",
cwd: '/test/dir',
cols: 100,
rows: 30,
});
expect(session.id).toMatch(/^term-/);
expect(session.cwd).toBe("/test/dir");
expect(session.shell).toBe("/bin/bash");
expect(session.cwd).toBe('/test/dir');
expect(session.shell).toBe('/bin/bash');
expect(pty.spawn).toHaveBeenCalledWith(
"/bin/bash",
["--login"],
'/bin/bash',
['--login'],
expect.objectContaining({
cwd: "/test/dir",
cwd: '/test/dir',
cols: 100,
rows: 30,
})
);
});
it("should use default cols and rows if not provided", () => {
it('should use default cols and rows if not provided', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
service.createSession();
@@ -258,61 +258,61 @@ describe("terminal-service.ts", () => {
);
});
it("should fall back to home directory if cwd does not exist", () => {
it('should fall back to home directory if cwd does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockImplementation(() => {
throw new Error("ENOENT");
throw new Error('ENOENT');
});
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({
cwd: "/nonexistent",
cwd: '/nonexistent',
});
expect(session.cwd).toBe("/home/user");
expect(session.cwd).toBe('/home/user');
});
it("should fall back to home directory if cwd is not a directory", () => {
it('should fall back to home directory if cwd is not a directory', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => false } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({
cwd: "/file.txt",
cwd: '/file.txt',
});
expect(session.cwd).toBe("/home/user");
expect(session.cwd).toBe('/home/user');
});
it("should fix double slashes in path", () => {
it('should fix double slashes in path', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({
cwd: "//test/dir",
cwd: '//test/dir',
});
expect(session.cwd).toBe("/test/dir");
expect(session.cwd).toBe('/test/dir');
});
it("should preserve WSL UNC paths", () => {
it('should preserve WSL UNC paths', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession({
cwd: "//wsl$/Ubuntu/home",
cwd: '//wsl$/Ubuntu/home',
});
expect(session.cwd).toBe("//wsl$/Ubuntu/home");
expect(session.cwd).toBe('//wsl$/Ubuntu/home');
});
it("should handle data events from PTY", () => {
it('should handle data events from PTY', () => {
vi.useFakeTimers();
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const dataCallback = vi.fn();
service.onData(dataCallback);
@@ -321,7 +321,7 @@ describe("terminal-service.ts", () => {
// Simulate data event
const onDataHandler = mockPtyProcess.onData.mock.calls[0][0];
onDataHandler("test data");
onDataHandler('test data');
// Wait for throttled output
vi.advanceTimersByTime(20);
@@ -331,10 +331,10 @@ describe("terminal-service.ts", () => {
vi.useRealTimers();
});
it("should handle exit events from PTY", () => {
it('should handle exit events from PTY', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const exitCallback = vi.fn();
service.onExit(exitCallback);
@@ -350,32 +350,32 @@ describe("terminal-service.ts", () => {
});
});
describe("write", () => {
it("should write data to existing session", () => {
describe('write', () => {
it('should write data to existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession();
const result = service.write(session.id, "ls\n");
const result = service.write(session.id, 'ls\n');
expect(result).toBe(true);
expect(mockPtyProcess.write).toHaveBeenCalledWith("ls\n");
expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n');
});
it("should return false for non-existent session", () => {
const result = service.write("nonexistent", "data");
it('should return false for non-existent session', () => {
const result = service.write('nonexistent', 'data');
expect(result).toBe(false);
expect(mockPtyProcess.write).not.toHaveBeenCalled();
});
});
describe("resize", () => {
it("should resize existing session", () => {
describe('resize', () => {
it('should resize existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession();
const result = service.resize(session.id, 120, 40);
@@ -384,19 +384,19 @@ describe("terminal-service.ts", () => {
expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40);
});
it("should return false for non-existent session", () => {
const result = service.resize("nonexistent", 120, 40);
it('should return false for non-existent session', () => {
const result = service.resize('nonexistent', 120, 40);
expect(result).toBe(false);
expect(mockPtyProcess.resize).not.toHaveBeenCalled();
});
it("should handle resize errors", () => {
it('should handle resize errors', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
mockPtyProcess.resize.mockImplementation(() => {
throw new Error("Resize failed");
throw new Error('Resize failed');
});
const session = service.createSession();
@@ -406,40 +406,40 @@ describe("terminal-service.ts", () => {
});
});
describe("killSession", () => {
it("should kill existing session", () => {
describe('killSession', () => {
it('should kill existing session', () => {
vi.useFakeTimers();
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession();
const result = service.killSession(session.id);
expect(result).toBe(true);
expect(mockPtyProcess.kill).toHaveBeenCalledWith("SIGTERM");
expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGTERM');
// Session is removed after SIGKILL timeout (1 second)
vi.advanceTimersByTime(1000);
expect(mockPtyProcess.kill).toHaveBeenCalledWith("SIGKILL");
expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGKILL');
expect(service.getSession(session.id)).toBeUndefined();
vi.useRealTimers();
});
it("should return false for non-existent session", () => {
const result = service.killSession("nonexistent");
it('should return false for non-existent session', () => {
const result = service.killSession('nonexistent');
expect(result).toBe(false);
});
it("should handle kill errors", () => {
it('should handle kill errors', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
mockPtyProcess.kill.mockImplementation(() => {
throw new Error("Kill failed");
throw new Error('Kill failed');
});
const session = service.createSession();
@@ -449,11 +449,11 @@ describe("terminal-service.ts", () => {
});
});
describe("getSession", () => {
it("should return existing session", () => {
describe('getSession', () => {
it('should return existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession();
const retrieved = service.getSession(session.id);
@@ -461,84 +461,84 @@ describe("terminal-service.ts", () => {
expect(retrieved).toBe(session);
});
it("should return undefined for non-existent session", () => {
const retrieved = service.getSession("nonexistent");
it('should return undefined for non-existent session', () => {
const retrieved = service.getSession('nonexistent');
expect(retrieved).toBeUndefined();
});
});
describe("getScrollback", () => {
it("should return scrollback buffer for existing session", () => {
describe('getScrollback', () => {
it('should return scrollback buffer for existing session', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session = service.createSession();
session.scrollbackBuffer = "test scrollback";
session.scrollbackBuffer = 'test scrollback';
const scrollback = service.getScrollback(session.id);
expect(scrollback).toBe("test scrollback");
expect(scrollback).toBe('test scrollback');
});
it("should return null for non-existent session", () => {
const scrollback = service.getScrollback("nonexistent");
it('should return null for non-existent session', () => {
const scrollback = service.getScrollback('nonexistent');
expect(scrollback).toBeNull();
});
});
describe("getAllSessions", () => {
it("should return all active sessions", () => {
describe('getAllSessions', () => {
it('should return all active sessions', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session1 = service.createSession({ cwd: "/dir1" });
const session2 = service.createSession({ cwd: "/dir2" });
const session1 = service.createSession({ cwd: '/dir1' });
const session2 = service.createSession({ cwd: '/dir2' });
const sessions = service.getAllSessions();
expect(sessions).toHaveLength(2);
expect(sessions[0].id).toBe(session1.id);
expect(sessions[1].id).toBe(session2.id);
expect(sessions[0].cwd).toBe("/dir1");
expect(sessions[1].cwd).toBe("/dir2");
expect(sessions[0].cwd).toBe('/dir1');
expect(sessions[1].cwd).toBe('/dir2');
});
it("should return empty array if no sessions", () => {
it('should return empty array if no sessions', () => {
const sessions = service.getAllSessions();
expect(sessions).toEqual([]);
});
});
describe("onData and onExit", () => {
it("should allow subscribing and unsubscribing from data events", () => {
describe('onData and onExit', () => {
it('should allow subscribing and unsubscribing from data events', () => {
const callback = vi.fn();
const unsubscribe = service.onData(callback);
expect(typeof unsubscribe).toBe("function");
expect(typeof unsubscribe).toBe('function');
unsubscribe();
});
it("should allow subscribing and unsubscribing from exit events", () => {
it('should allow subscribing and unsubscribing from exit events', () => {
const callback = vi.fn();
const unsubscribe = service.onExit(callback);
expect(typeof unsubscribe).toBe("function");
expect(typeof unsubscribe).toBe('function');
unsubscribe();
});
});
describe("cleanup", () => {
it("should clean up all sessions", () => {
describe('cleanup', () => {
it('should clean up all sessions', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
const session1 = service.createSession();
const session2 = service.createSession();
@@ -550,12 +550,12 @@ describe("terminal-service.ts", () => {
expect(service.getAllSessions()).toHaveLength(0);
});
it("should handle cleanup errors gracefully", () => {
it('should handle cleanup errors gracefully', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
vi.spyOn(process, "env", "get").mockReturnValue({ SHELL: "/bin/bash" });
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
mockPtyProcess.kill.mockImplementation(() => {
throw new Error("Kill failed");
throw new Error('Kill failed');
});
service.createSession();
@@ -564,8 +564,8 @@ describe("terminal-service.ts", () => {
});
});
describe("getTerminalService", () => {
it("should return singleton instance", () => {
describe('getTerminalService', () => {
it('should return singleton instance', () => {
const instance1 = getTerminalService();
const instance2 = getTerminalService();

View File

@@ -24,7 +24,7 @@ export async function waitFor(
const start = Date.now();
while (!condition()) {
if (Date.now() - start > timeout) {
throw new Error("Timeout waiting for condition");
throw new Error('Timeout waiting for condition');
}
await new Promise((resolve) => setTimeout(resolve, interval));
}

View File

@@ -3,10 +3,10 @@
* Provides reusable mocks for common dependencies
*/
import { vi } from "vitest";
import type { ChildProcess } from "child_process";
import { EventEmitter } from "events";
import type { Readable } from "stream";
import { vi } from 'vitest';
import type { ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import type { Readable } from 'stream';
/**
* Mock child_process.spawn for subprocess tests
@@ -31,19 +31,19 @@ export function createMockChildProcess(options: {
process.nextTick(() => {
// Emit stdout lines
for (const line of stdout) {
mockProcess.stdout.emit("data", Buffer.from(line + "\n"));
mockProcess.stdout.emit('data', Buffer.from(line + '\n'));
}
// Emit stderr lines
for (const line of stderr) {
mockProcess.stderr.emit("data", Buffer.from(line + "\n"));
mockProcess.stderr.emit('data', Buffer.from(line + '\n'));
}
// Emit exit or error
if (shouldError) {
mockProcess.emit("error", new Error("Process error"));
mockProcess.emit('error', new Error('Process error'));
} else {
mockProcess.emit("exit", exitCode);
mockProcess.emit('exit', exitCode);
}
});