mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23: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,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user