mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
ralph/chore/fix.tests (#1578)
This commit is contained in:
@@ -16,8 +16,8 @@
|
|||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:unit": "vitest run -t unit",
|
"test:unit": "vitest run '**/*.spec.ts'",
|
||||||
"test:integration": "vitest run -t integration",
|
"test:integration": "vitest run '**/*.test.ts'",
|
||||||
"test:e2e": "vitest run --dir tests/e2e",
|
"test:e2e": "vitest run --dir tests/e2e",
|
||||||
"test:ci": "vitest run --coverage --reporter=dot"
|
"test:ci": "vitest run --coverage --reporter=dot"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ describe('LoopCommand', () => {
|
|||||||
mockTmCore = {
|
mockTmCore = {
|
||||||
loop: {
|
loop: {
|
||||||
run: mockLoopRun,
|
run: mockLoopRun,
|
||||||
checkSandboxAuth: vi.fn().mockReturnValue(true),
|
checkSandboxAuth: vi.fn().mockReturnValue({ ready: true }),
|
||||||
runInteractiveAuth: vi.fn(),
|
runInteractiveAuth: vi.fn().mockReturnValue({ success: true }),
|
||||||
resolveIterations: vi.fn().mockImplementation((opts) => {
|
resolveIterations: vi.fn().mockImplementation((opts) => {
|
||||||
// Mirror the real implementation logic for accurate testing
|
// Mirror the real implementation logic for accurate testing
|
||||||
if (opts.userIterations !== undefined) return opts.userIterations;
|
if (opts.userIterations !== undefined) return opts.userIterations;
|
||||||
@@ -400,7 +400,7 @@ describe('LoopCommand', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should run interactive auth when sandbox not ready', async () => {
|
it('should run interactive auth when sandbox not ready', async () => {
|
||||||
mockTmCore.loop.checkSandboxAuth.mockReturnValue(false);
|
mockTmCore.loop.checkSandboxAuth.mockReturnValue({ ready: false });
|
||||||
const result = createMockResult();
|
const result = createMockResult();
|
||||||
mockLoopRun.mockResolvedValue(result);
|
mockLoopRun.mockResolvedValue(result);
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:unit": "vitest run -t unit",
|
"test:unit": "vitest run '**/*.spec.ts'",
|
||||||
"test:integration": "vitest run -t integration",
|
"test:integration": "vitest run '**/*.test.ts'",
|
||||||
"test:ci": "vitest run --coverage --reporter=dot"
|
"test:ci": "vitest run --coverage --reporter=dot"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ describe('generate MCP tool', () => {
|
|||||||
const response = callMCPTool('generate', { projectRoot: testDir });
|
const response = callMCPTool('generate', { projectRoot: testDir });
|
||||||
|
|
||||||
expect(response.data.orphanedFilesRemoved).toBe(1);
|
expect(response.data.orphanedFilesRemoved).toBe(1);
|
||||||
}, 15000);
|
}, 30000); // Longer timeout: this test makes 2 MCP calls (generate, then regenerate)
|
||||||
|
|
||||||
it('should accept output parameter for custom directory', () => {
|
it('should accept output parameter for custom directory', () => {
|
||||||
const testData = createTasksFile({
|
const testData = createTasksFile({
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
"turbo:dev": "turbo dev",
|
"turbo:dev": "turbo dev",
|
||||||
"turbo:build": "turbo build",
|
"turbo:build": "turbo build",
|
||||||
"turbo:typecheck": "turbo typecheck",
|
"turbo:typecheck": "turbo typecheck",
|
||||||
|
"turbo:test": "turbo test",
|
||||||
|
"turbo:test:unit": "turbo test:unit",
|
||||||
|
"turbo:test:integration": "turbo test:integration",
|
||||||
"build:build-config": "npm run build -w @tm/build-config",
|
"build:build-config": "npm run build -w @tm/build-config",
|
||||||
"test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/.bin/jest",
|
"test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/.bin/jest",
|
||||||
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
|
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:unit": "vitest run '**/*.spec.ts'",
|
||||||
|
"test:integration": "vitest run '**/*.test.ts'",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"lint": "biome check --write",
|
"lint": "biome check --write",
|
||||||
|
|||||||
@@ -34,72 +34,64 @@ describe('ConfigManager', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup default mock behaviors
|
// Setup default mock behaviors using class syntax for proper constructor mocking
|
||||||
vi.mocked(ConfigLoader).mockImplementation(
|
vi.mocked(ConfigLoader).mockImplementation(function (this: any) {
|
||||||
() =>
|
this.getDefaultConfig = vi.fn().mockReturnValue({
|
||||||
({
|
|
||||||
getDefaultConfig: vi.fn().mockReturnValue({
|
|
||||||
models: { main: 'default-model', fallback: 'fallback-model' },
|
models: { main: 'default-model', fallback: 'fallback-model' },
|
||||||
storage: { type: 'file' },
|
storage: { type: 'file' },
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
}),
|
});
|
||||||
loadLocalConfig: vi.fn().mockResolvedValue(null),
|
this.loadLocalConfig = vi.fn().mockResolvedValue(null);
|
||||||
loadGlobalConfig: vi.fn().mockResolvedValue(null),
|
this.loadGlobalConfig = vi.fn().mockResolvedValue(null);
|
||||||
hasLocalConfig: vi.fn().mockResolvedValue(false),
|
this.hasLocalConfig = vi.fn().mockResolvedValue(false);
|
||||||
hasGlobalConfig: vi.fn().mockResolvedValue(false)
|
this.hasGlobalConfig = vi.fn().mockResolvedValue(false);
|
||||||
}) as any
|
return this;
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
vi.mocked(ConfigMerger).mockImplementation(
|
vi.mocked(ConfigMerger).mockImplementation(function (this: any) {
|
||||||
() =>
|
this.addSource = vi.fn();
|
||||||
({
|
this.clearSources = vi.fn();
|
||||||
addSource: vi.fn(),
|
this.merge = vi.fn().mockReturnValue({
|
||||||
clearSources: vi.fn(),
|
|
||||||
merge: vi.fn().mockReturnValue({
|
|
||||||
models: { main: 'merged-model', fallback: 'fallback-model' },
|
models: { main: 'merged-model', fallback: 'fallback-model' },
|
||||||
storage: { type: 'file' }
|
storage: { type: 'file' }
|
||||||
}),
|
});
|
||||||
getSources: vi.fn().mockReturnValue([]),
|
this.getSources = vi.fn().mockReturnValue([]);
|
||||||
hasSource: vi.fn().mockReturnValue(false),
|
this.hasSource = vi.fn().mockReturnValue(false);
|
||||||
removeSource: vi.fn().mockReturnValue(false)
|
this.removeSource = vi.fn().mockReturnValue(false);
|
||||||
}) as any
|
return this;
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
vi.mocked(RuntimeStateManager).mockImplementation(
|
vi.mocked(RuntimeStateManager).mockImplementation(function (this: any) {
|
||||||
() =>
|
this.loadState = vi.fn().mockResolvedValue({ activeTag: 'master' });
|
||||||
({
|
this.saveState = vi.fn().mockResolvedValue(undefined);
|
||||||
loadState: vi.fn().mockResolvedValue({ activeTag: 'master' }),
|
this.getCurrentTag = vi.fn().mockReturnValue('master');
|
||||||
saveState: vi.fn().mockResolvedValue(undefined),
|
this.setCurrentTag = vi.fn().mockResolvedValue(undefined);
|
||||||
getCurrentTag: vi.fn().mockReturnValue('master'),
|
this.getState = vi.fn().mockReturnValue({ activeTag: 'master' });
|
||||||
setCurrentTag: vi.fn().mockResolvedValue(undefined),
|
this.updateMetadata = vi.fn().mockResolvedValue(undefined);
|
||||||
getState: vi.fn().mockReturnValue({ activeTag: 'master' }),
|
this.clearState = vi.fn().mockResolvedValue(undefined);
|
||||||
updateMetadata: vi.fn().mockResolvedValue(undefined),
|
return this;
|
||||||
clearState: vi.fn().mockResolvedValue(undefined)
|
} as any);
|
||||||
}) as any
|
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(ConfigPersistence).mockImplementation(
|
vi.mocked(ConfigPersistence).mockImplementation(function (this: any) {
|
||||||
() =>
|
this.saveConfig = vi.fn().mockResolvedValue(undefined);
|
||||||
({
|
this.configExists = vi.fn().mockResolvedValue(false);
|
||||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
this.deleteConfig = vi.fn().mockResolvedValue(undefined);
|
||||||
configExists: vi.fn().mockResolvedValue(false),
|
this.getBackups = vi.fn().mockResolvedValue([]);
|
||||||
deleteConfig: vi.fn().mockResolvedValue(undefined),
|
this.restoreFromBackup = vi.fn().mockResolvedValue(undefined);
|
||||||
getBackups: vi.fn().mockResolvedValue([]),
|
return this;
|
||||||
restoreFromBackup: vi.fn().mockResolvedValue(undefined)
|
} as any);
|
||||||
}) as any
|
|
||||||
);
|
|
||||||
|
|
||||||
vi.mocked(EnvironmentConfigProvider).mockImplementation(
|
vi.mocked(EnvironmentConfigProvider).mockImplementation(function (
|
||||||
() =>
|
this: any
|
||||||
({
|
) {
|
||||||
loadConfig: vi.fn().mockReturnValue({}),
|
this.loadConfig = vi.fn().mockReturnValue({});
|
||||||
getRuntimeState: vi.fn().mockReturnValue({}),
|
this.getRuntimeState = vi.fn().mockReturnValue({});
|
||||||
hasEnvVar: vi.fn().mockReturnValue(false),
|
this.hasEnvVar = vi.fn().mockReturnValue(false);
|
||||||
getAllTaskmasterEnvVars: vi.fn().mockReturnValue({}),
|
this.getAllTaskmasterEnvVars = vi.fn().mockReturnValue({});
|
||||||
addMapping: vi.fn(),
|
this.addMapping = vi.fn();
|
||||||
getMappings: vi.fn().mockReturnValue([])
|
this.getMappings = vi.fn().mockReturnValue([]);
|
||||||
}) as any
|
return this;
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
// Since constructor is private, we need to use the factory method
|
// Since constructor is private, we need to use the factory method
|
||||||
// But for testing, we'll create a test instance using create()
|
// But for testing, we'll create a test instance using create()
|
||||||
@@ -178,28 +170,30 @@ describe('ConfigManager', () => {
|
|||||||
|
|
||||||
it('should return storage configuration', () => {
|
it('should return storage configuration', () => {
|
||||||
const storage = manager.getStorageConfig();
|
const storage = manager.getStorageConfig();
|
||||||
expect(storage).toEqual({ type: 'file' });
|
expect(storage).toEqual({
|
||||||
|
type: 'file',
|
||||||
|
basePath: testProjectRoot,
|
||||||
|
apiConfigured: false
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return API storage configuration when configured', async () => {
|
it('should return API storage configuration when configured', async () => {
|
||||||
// Create a new instance with API storage config
|
// Create a new instance with API storage config
|
||||||
vi.mocked(ConfigMerger).mockImplementationOnce(
|
vi.mocked(ConfigMerger).mockImplementationOnce(function (this: any) {
|
||||||
() =>
|
this.addSource = vi.fn();
|
||||||
({
|
this.clearSources = vi.fn();
|
||||||
addSource: vi.fn(),
|
this.merge = vi.fn().mockReturnValue({
|
||||||
clearSources: vi.fn(),
|
|
||||||
merge: vi.fn().mockReturnValue({
|
|
||||||
storage: {
|
storage: {
|
||||||
type: 'api',
|
type: 'api',
|
||||||
apiEndpoint: 'https://api.example.com',
|
apiEndpoint: 'https://api.example.com',
|
||||||
apiAccessToken: 'token123'
|
apiAccessToken: 'token123'
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
getSources: vi.fn().mockReturnValue([]),
|
this.getSources = vi.fn().mockReturnValue([]);
|
||||||
hasSource: vi.fn().mockReturnValue(false),
|
this.hasSource = vi.fn().mockReturnValue(false);
|
||||||
removeSource: vi.fn().mockReturnValue(false)
|
this.removeSource = vi.fn().mockReturnValue(false);
|
||||||
}) as any
|
return this;
|
||||||
);
|
} as any);
|
||||||
|
|
||||||
const apiManager = await ConfigManager.create(testProjectRoot);
|
const apiManager = await ConfigManager.create(testProjectRoot);
|
||||||
|
|
||||||
@@ -207,7 +201,9 @@ describe('ConfigManager', () => {
|
|||||||
expect(storage).toEqual({
|
expect(storage).toEqual({
|
||||||
type: 'api',
|
type: 'api',
|
||||||
apiEndpoint: 'https://api.example.com',
|
apiEndpoint: 'https://api.example.com',
|
||||||
apiAccessToken: 'token123'
|
apiAccessToken: 'token123',
|
||||||
|
basePath: testProjectRoot,
|
||||||
|
apiConfigured: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,12 @@
|
|||||||
* @fileoverview Unit tests for ConfigLoader service
|
* @fileoverview Unit tests for ConfigLoader service
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'node:fs/promises';
|
import * as fsPromises from 'node:fs/promises';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
||||||
import { ConfigLoader } from './config-loader.service.js';
|
import { ConfigLoader } from './config-loader.service.js';
|
||||||
|
|
||||||
vi.mock('node:fs', () => ({
|
vi.mock('node:fs/promises');
|
||||||
promises: {
|
|
||||||
readFile: vi.fn(),
|
|
||||||
access: vi.fn()
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ConfigLoader', () => {
|
describe('ConfigLoader', () => {
|
||||||
let configLoader: ConfigLoader;
|
let configLoader: ConfigLoader;
|
||||||
@@ -56,11 +51,13 @@ describe('ConfigLoader', () => {
|
|||||||
storage: { type: 'api' as const }
|
storage: { type: 'api' as const }
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
|
vi.mocked(fsPromises.readFile).mockResolvedValue(
|
||||||
|
JSON.stringify(mockConfig)
|
||||||
|
);
|
||||||
|
|
||||||
const result = await configLoader.loadLocalConfig();
|
const result = await configLoader.loadLocalConfig();
|
||||||
|
|
||||||
expect(fs.readFile).toHaveBeenCalledWith(
|
expect(fsPromises.readFile).toHaveBeenCalledWith(
|
||||||
'/test/project/.taskmaster/config.json',
|
'/test/project/.taskmaster/config.json',
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
@@ -70,7 +67,7 @@ describe('ConfigLoader', () => {
|
|||||||
it('should return null when config file does not exist', async () => {
|
it('should return null when config file does not exist', async () => {
|
||||||
const error = new Error('File not found') as any;
|
const error = new Error('File not found') as any;
|
||||||
error.code = 'ENOENT';
|
error.code = 'ENOENT';
|
||||||
vi.mocked(fs.readFile).mockRejectedValue(error);
|
vi.mocked(fsPromises.readFile).mockRejectedValue(error);
|
||||||
|
|
||||||
const result = await configLoader.loadLocalConfig();
|
const result = await configLoader.loadLocalConfig();
|
||||||
|
|
||||||
@@ -79,7 +76,7 @@ describe('ConfigLoader', () => {
|
|||||||
|
|
||||||
it('should throw TaskMasterError for other file errors', async () => {
|
it('should throw TaskMasterError for other file errors', async () => {
|
||||||
const error = new Error('Permission denied');
|
const error = new Error('Permission denied');
|
||||||
vi.mocked(fs.readFile).mockRejectedValue(error);
|
vi.mocked(fsPromises.readFile).mockRejectedValue(error);
|
||||||
|
|
||||||
await expect(configLoader.loadLocalConfig()).rejects.toThrow(
|
await expect(configLoader.loadLocalConfig()).rejects.toThrow(
|
||||||
'Failed to load local configuration'
|
'Failed to load local configuration'
|
||||||
@@ -87,7 +84,7 @@ describe('ConfigLoader', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error for invalid JSON', async () => {
|
it('should throw error for invalid JSON', async () => {
|
||||||
vi.mocked(fs.readFile).mockResolvedValue('invalid json');
|
vi.mocked(fsPromises.readFile).mockResolvedValue('invalid json');
|
||||||
|
|
||||||
await expect(configLoader.loadLocalConfig()).rejects.toThrow();
|
await expect(configLoader.loadLocalConfig()).rejects.toThrow();
|
||||||
});
|
});
|
||||||
@@ -102,18 +99,18 @@ describe('ConfigLoader', () => {
|
|||||||
|
|
||||||
describe('hasLocalConfig', () => {
|
describe('hasLocalConfig', () => {
|
||||||
it('should return true when local config exists', async () => {
|
it('should return true when local config exists', async () => {
|
||||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
vi.mocked(fsPromises.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const result = await configLoader.hasLocalConfig();
|
const result = await configLoader.hasLocalConfig();
|
||||||
|
|
||||||
expect(fs.access).toHaveBeenCalledWith(
|
expect(fsPromises.access).toHaveBeenCalledWith(
|
||||||
'/test/project/.taskmaster/config.json'
|
'/test/project/.taskmaster/config.json'
|
||||||
);
|
);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when local config does not exist', async () => {
|
it('should return false when local config does not exist', async () => {
|
||||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
vi.mocked(fsPromises.access).mockRejectedValue(new Error('Not found'));
|
||||||
|
|
||||||
const result = await configLoader.hasLocalConfig();
|
const result = await configLoader.hasLocalConfig();
|
||||||
|
|
||||||
@@ -123,18 +120,18 @@ describe('ConfigLoader', () => {
|
|||||||
|
|
||||||
describe('hasGlobalConfig', () => {
|
describe('hasGlobalConfig', () => {
|
||||||
it('should check global config path', async () => {
|
it('should check global config path', async () => {
|
||||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
vi.mocked(fsPromises.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const result = await configLoader.hasGlobalConfig();
|
const result = await configLoader.hasGlobalConfig();
|
||||||
|
|
||||||
expect(fs.access).toHaveBeenCalledWith(
|
expect(fsPromises.access).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('.taskmaster/config.json')
|
expect.stringContaining('.taskmaster/config.json')
|
||||||
);
|
);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when global config does not exist', async () => {
|
it('should return false when global config does not exist', async () => {
|
||||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
vi.mocked(fsPromises.access).mockRejectedValue(new Error('Not found'));
|
||||||
|
|
||||||
const result = await configLoader.hasGlobalConfig();
|
const result = await configLoader.hasGlobalConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,12 @@
|
|||||||
* @fileoverview Unit tests for RuntimeStateManager service
|
* @fileoverview Unit tests for RuntimeStateManager service
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'node:fs/promises';
|
import * as fs from 'node:fs/promises';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
|
||||||
import { RuntimeStateManager } from './runtime-state-manager.service.js';
|
import { RuntimeStateManager } from './runtime-state-manager.service.js';
|
||||||
|
|
||||||
vi.mock('node:fs', () => ({
|
vi.mock('node:fs/promises');
|
||||||
promises: {
|
|
||||||
readFile: vi.fn(),
|
|
||||||
writeFile: vi.fn(),
|
|
||||||
mkdir: vi.fn(),
|
|
||||||
unlink: vi.fn()
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('RuntimeStateManager', () => {
|
describe('RuntimeStateManager', () => {
|
||||||
let stateManager: RuntimeStateManager;
|
let stateManager: RuntimeStateManager;
|
||||||
@@ -96,15 +89,10 @@ describe('RuntimeStateManager', () => {
|
|||||||
it('should handle invalid JSON gracefully', async () => {
|
it('should handle invalid JSON gracefully', async () => {
|
||||||
vi.mocked(fs.readFile).mockResolvedValue('invalid json');
|
vi.mocked(fs.readFile).mockResolvedValue('invalid json');
|
||||||
|
|
||||||
// Mock console.warn to avoid noise in tests
|
|
||||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
||||||
|
|
||||||
const state = await stateManager.loadState();
|
const state = await stateManager.loadState();
|
||||||
|
|
||||||
|
// Should return default state when JSON is invalid
|
||||||
expect(state.currentTag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
|
expect(state.currentTag).toBe(DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG);
|
||||||
expect(warnSpy).toHaveBeenCalled();
|
|
||||||
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,7 +112,7 @@ describe('RuntimeStateManager', () => {
|
|||||||
// Verify writeFile was called with correct data
|
// Verify writeFile was called with correct data
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||||
'/test/project/.taskmaster/state.json',
|
'/test/project/.taskmaster/state.json',
|
||||||
expect.stringContaining('"activeTag":"test-tag"'),
|
expect.stringContaining('"currentTag": "test-tag"'),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe('LoopDomain', () => {
|
|||||||
tag: 'feature-branch'
|
tag: 'feature-branch'
|
||||||
};
|
};
|
||||||
const config = (loopDomain as any).buildConfig(fullConfig);
|
const config = (loopDomain as any).buildConfig(fullConfig);
|
||||||
expect(config).toEqual(fullConfig);
|
expect(config).toMatchObject(fullConfig);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ describe('LoopService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('checkSandboxAuth()', () => {
|
describe('checkSandboxAuth()', () => {
|
||||||
it('should return true when output contains ok', () => {
|
it('should return ready=true when output contains ok', () => {
|
||||||
mockSpawnSync.mockReturnValue({
|
mockSpawnSync.mockReturnValue({
|
||||||
stdout: 'OK',
|
stdout: 'OK',
|
||||||
stderr: '',
|
stderr: '',
|
||||||
@@ -135,7 +135,7 @@ describe('LoopService', () => {
|
|||||||
const service = new LoopService(defaultOptions);
|
const service = new LoopService(defaultOptions);
|
||||||
const result = service.checkSandboxAuth();
|
const result = service.checkSandboxAuth();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.ready).toBe(true);
|
||||||
expect(mockSpawnSync).toHaveBeenCalledWith(
|
expect(mockSpawnSync).toHaveBeenCalledWith(
|
||||||
'docker',
|
'docker',
|
||||||
['sandbox', 'run', 'claude', '-p', 'Say OK'],
|
['sandbox', 'run', 'claude', '-p', 'Say OK'],
|
||||||
@@ -146,7 +146,7 @@ describe('LoopService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when output does not contain ok', () => {
|
it('should return ready=false when output does not contain ok', () => {
|
||||||
mockSpawnSync.mockReturnValue({
|
mockSpawnSync.mockReturnValue({
|
||||||
stdout: 'Error: not authenticated',
|
stdout: 'Error: not authenticated',
|
||||||
stderr: '',
|
stderr: '',
|
||||||
@@ -159,7 +159,7 @@ describe('LoopService', () => {
|
|||||||
const service = new LoopService(defaultOptions);
|
const service = new LoopService(defaultOptions);
|
||||||
const result = service.checkSandboxAuth();
|
const result = service.checkSandboxAuth();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result.ready).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should check stderr as well as stdout', () => {
|
it('should check stderr as well as stdout', () => {
|
||||||
@@ -175,7 +175,7 @@ describe('LoopService', () => {
|
|||||||
const service = new LoopService(defaultOptions);
|
const service = new LoopService(defaultOptions);
|
||||||
const result = service.checkSandboxAuth();
|
const result = service.checkSandboxAuth();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.ready).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ describe('LoopService', () => {
|
|||||||
expect(mockSpawnSync).toHaveBeenCalledTimes(3);
|
expect(mockSpawnSync).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call spawnSync with docker sandbox run claude -p', async () => {
|
it('should call spawnSync with claude -p by default (non-sandbox)', async () => {
|
||||||
mockSpawnSync.mockReturnValue({
|
mockSpawnSync.mockReturnValue({
|
||||||
stdout: 'Done',
|
stdout: 'Done',
|
||||||
stderr: '',
|
stderr: '',
|
||||||
@@ -274,14 +274,8 @@ describe('LoopService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockSpawnSync).toHaveBeenCalledWith(
|
expect(mockSpawnSync).toHaveBeenCalledWith(
|
||||||
'docker',
|
|
||||||
expect.arrayContaining([
|
|
||||||
'sandbox',
|
|
||||||
'run',
|
|
||||||
'claude',
|
'claude',
|
||||||
'-p',
|
expect.arrayContaining(['-p', expect.any(String)]),
|
||||||
expect.any(String)
|
|
||||||
]),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
cwd: '/test/project'
|
cwd: '/test/project'
|
||||||
})
|
})
|
||||||
@@ -397,7 +391,8 @@ describe('LoopService', () => {
|
|||||||
expect(fsPromises.mkdir).toHaveBeenCalledWith('/test', {
|
expect(fsPromises.mkdir).toHaveBeenCalledWith('/test', {
|
||||||
recursive: true
|
recursive: true
|
||||||
});
|
});
|
||||||
expect(fsPromises.writeFile).toHaveBeenCalledWith(
|
// Uses appendFile instead of writeFile to preserve existing progress
|
||||||
|
expect(fsPromises.appendFile).toHaveBeenCalledWith(
|
||||||
'/test/progress.txt',
|
'/test/progress.txt',
|
||||||
expect.stringContaining('# Task Master Loop Progress'),
|
expect.stringContaining('# Task Master Loop Progress'),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
@@ -449,8 +444,8 @@ describe('LoopService', () => {
|
|||||||
|
|
||||||
// Verify spawn was called with prompt containing iteration info
|
// Verify spawn was called with prompt containing iteration info
|
||||||
const spawnCall = mockSpawnSync.mock.calls[0];
|
const spawnCall = mockSpawnSync.mock.calls[0];
|
||||||
// Args are ['sandbox', 'run', 'claude', '-p', prompt]
|
// Args are ['-p', prompt, '--dangerously-skip-permissions'] for non-sandbox
|
||||||
const promptArg = spawnCall[1][4];
|
const promptArg = spawnCall[1][1];
|
||||||
expect(promptArg).toContain('iteration 1 of 1');
|
expect(promptArg).toContain('iteration 1 of 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,15 @@
|
|||||||
*
|
*
|
||||||
* FILE STORAGE (local):
|
* FILE STORAGE (local):
|
||||||
* - Main tasks: "1", "2", "15"
|
* - Main tasks: "1", "2", "15"
|
||||||
* - Subtasks: "1.2", "15.3" (one level only)
|
* - Subtasks: "1.2", "15.3"
|
||||||
|
* - Sub-subtasks: "1.2.3", "15.3.1"
|
||||||
*
|
*
|
||||||
* API STORAGE (Hamster):
|
* API STORAGE (Hamster):
|
||||||
* - Main tasks: "HAM-1", "ham-1", "HAM1", "ham1" (all normalized to "HAM-1")
|
* - Main tasks: "HAM-1", "ham-1", "HAM1", "ham1" (all normalized to "HAM-1")
|
||||||
|
* - Variable-length prefixes: "PROJ-456", "TAS-1", "abc-999"
|
||||||
* - No subtasks (API doesn't use dot notation)
|
* - No subtasks (API doesn't use dot notation)
|
||||||
*
|
*
|
||||||
* NOT supported:
|
* NOT supported:
|
||||||
* - Deep nesting: "1.2.3" (file storage only has one subtask level)
|
|
||||||
* - API subtasks: "HAM-1.2" (doesn't exist)
|
* - API subtasks: "HAM-1.2" (doesn't exist)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -24,10 +25,10 @@ import { normalizeDisplayId } from '../../../common/schemas/task-id.schema.js';
|
|||||||
* Pattern for validating a single task ID
|
* Pattern for validating a single task ID
|
||||||
* Permissive input - accepts with or without hyphen for API IDs
|
* Permissive input - accepts with or without hyphen for API IDs
|
||||||
* - Numeric: "1", "15", "999"
|
* - Numeric: "1", "15", "999"
|
||||||
* - Numeric subtasks: "1.2" (one level only)
|
* - Numeric subtasks: "1.2", "1.2.3" (multi-level supported)
|
||||||
* - API display IDs: "HAM-1", "ham-1", "HAM1", "ham1"
|
* - API display IDs: "HAM-1", "PROJ-456", "TAS-1", "abc-999"
|
||||||
*/
|
*/
|
||||||
export const TASK_ID_PATTERN = /^(\d+(\.\d+)?|[A-Za-z]{3}-?\d+)$/;
|
export const TASK_ID_PATTERN = /^(\d+(\.\d+)*|[A-Za-z]+-?\d+)$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a single task ID string
|
* Validates a single task ID string
|
||||||
@@ -39,9 +40,10 @@ export const TASK_ID_PATTERN = /^(\d+(\.\d+)?|[A-Za-z]{3}-?\d+)$/;
|
|||||||
* ```typescript
|
* ```typescript
|
||||||
* isValidTaskIdFormat("1"); // true
|
* isValidTaskIdFormat("1"); // true
|
||||||
* isValidTaskIdFormat("1.2"); // true
|
* isValidTaskIdFormat("1.2"); // true
|
||||||
|
* isValidTaskIdFormat("1.2.3"); // true (multi-level subtasks)
|
||||||
* isValidTaskIdFormat("HAM-1"); // true
|
* isValidTaskIdFormat("HAM-1"); // true
|
||||||
|
* isValidTaskIdFormat("PROJ-456"); // true (variable-length prefix)
|
||||||
* isValidTaskIdFormat("ham1"); // true (permissive input)
|
* isValidTaskIdFormat("ham1"); // true (permissive input)
|
||||||
* isValidTaskIdFormat("1.2.3"); // false (too deep)
|
|
||||||
* isValidTaskIdFormat("HAM-1.2"); // false (no API subtasks)
|
* isValidTaskIdFormat("HAM-1.2"); // false (no API subtasks)
|
||||||
* isValidTaskIdFormat("abc"); // false
|
* isValidTaskIdFormat("abc"); // false
|
||||||
* ```
|
* ```
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:unit": "vitest run '**/*.spec.ts'",
|
||||||
|
"test:integration": "vitest run '**/*.test.ts'",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"lint": "biome check --write",
|
"lint": "biome check --write",
|
||||||
|
|||||||
30
turbo.json
30
turbo.json
@@ -39,6 +39,36 @@
|
|||||||
"!{packages,apps}/**/node_modules/**"
|
"!{packages,apps}/**/node_modules/**"
|
||||||
],
|
],
|
||||||
"outputLogs": "new-only"
|
"outputLogs": "new-only"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": [
|
||||||
|
"$TURBO_DEFAULT$",
|
||||||
|
"!{packages,apps}/**/dist/**",
|
||||||
|
"!{packages,apps}/**/node_modules/**"
|
||||||
|
],
|
||||||
|
"outputLogs": "new-only",
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"test:unit": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": [
|
||||||
|
"$TURBO_DEFAULT$",
|
||||||
|
"!{packages,apps}/**/dist/**",
|
||||||
|
"!{packages,apps}/**/node_modules/**"
|
||||||
|
],
|
||||||
|
"outputLogs": "new-only",
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"test:integration": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": [
|
||||||
|
"$TURBO_DEFAULT$",
|
||||||
|
"!{packages,apps}/**/dist/**",
|
||||||
|
"!{packages,apps}/**/node_modules/**"
|
||||||
|
],
|
||||||
|
"outputLogs": "new-only",
|
||||||
|
"cache": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globalDependencies": ["turbo.json", "tsconfig.json", ".env*"]
|
"globalDependencies": ["turbo.json", "tsconfig.json", ".env*"]
|
||||||
|
|||||||
Reference in New Issue
Block a user