mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user