feat: improve ham connection (#1451)

This commit is contained in:
Ralph Khreish
2025-11-26 23:20:44 +01:00
committed by GitHub
parent 77b3b7780d
commit 28fcc27411
17 changed files with 642 additions and 95 deletions

View File

@@ -504,8 +504,19 @@ export class TaskService {
/**
* Get current active tag
* For API storage, uses the brief ID from auth context
* For file storage, uses the tag from local config/state
*/
getActiveTag(): string {
// For API storage, use brief ID from auth context if available
if (this.initialized && this.getStorageType() === 'api') {
const briefName = this.storage.getCurrentBriefName();
if (briefName) {
return briefName;
}
}
// Fall back to config-based tag resolution
return this.configManager.getActiveTag();
}

View File

@@ -0,0 +1,13 @@
/**
* @fileoverview Task validation utilities and Zod schemas
*/
export {
TASK_ID_PATTERN,
isValidTaskIdFormat,
taskIdSchema,
taskIdsSchema,
parseTaskIds,
extractParentId,
isSubtaskId
} from './task-id.js';

View File

@@ -0,0 +1,165 @@
/**
* @fileoverview Tests for task ID validation utilities
*/
import { describe, it, expect } from 'vitest';
import {
TASK_ID_PATTERN,
isValidTaskIdFormat,
taskIdSchema,
taskIdsSchema,
parseTaskIds,
extractParentId,
isSubtaskId
} from './task-id.js';
describe('Task ID Validation', () => {
describe('TASK_ID_PATTERN', () => {
it('matches simple numeric IDs', () => {
expect(TASK_ID_PATTERN.test('1')).toBe(true);
expect(TASK_ID_PATTERN.test('15')).toBe(true);
expect(TASK_ID_PATTERN.test('999')).toBe(true);
});
it('matches alphanumeric display IDs', () => {
expect(TASK_ID_PATTERN.test('HAM-123')).toBe(true);
expect(TASK_ID_PATTERN.test('PROJ-456')).toBe(true);
expect(TASK_ID_PATTERN.test('TAS-1')).toBe(true);
expect(TASK_ID_PATTERN.test('abc-999')).toBe(true);
});
it('matches subtask IDs', () => {
expect(TASK_ID_PATTERN.test('1.2')).toBe(true);
expect(TASK_ID_PATTERN.test('15.3')).toBe(true);
});
it('matches sub-subtask IDs', () => {
expect(TASK_ID_PATTERN.test('1.2.3')).toBe(true);
expect(TASK_ID_PATTERN.test('15.3.1')).toBe(true);
});
it('rejects alphanumeric subtasks (not supported)', () => {
expect(TASK_ID_PATTERN.test('HAM-123.2')).toBe(false);
expect(TASK_ID_PATTERN.test('PROJ-456.1.2')).toBe(false);
});
it('rejects invalid formats', () => {
expect(TASK_ID_PATTERN.test('')).toBe(false);
expect(TASK_ID_PATTERN.test('abc')).toBe(false);
expect(TASK_ID_PATTERN.test('1.a')).toBe(false);
expect(TASK_ID_PATTERN.test('.1')).toBe(false);
expect(TASK_ID_PATTERN.test('1.')).toBe(false);
expect(TASK_ID_PATTERN.test('HAM')).toBe(false);
expect(TASK_ID_PATTERN.test('123-HAM')).toBe(false);
});
});
describe('isValidTaskIdFormat', () => {
it('returns true for valid IDs', () => {
expect(isValidTaskIdFormat('1')).toBe(true);
expect(isValidTaskIdFormat('1.2')).toBe(true);
expect(isValidTaskIdFormat('1.2.3')).toBe(true);
expect(isValidTaskIdFormat('HAM-123')).toBe(true);
});
it('returns false for invalid IDs', () => {
expect(isValidTaskIdFormat('')).toBe(false);
expect(isValidTaskIdFormat('abc')).toBe(false);
expect(isValidTaskIdFormat('1.a')).toBe(false);
expect(isValidTaskIdFormat('HAM-123.2')).toBe(false);
});
});
describe('taskIdSchema', () => {
it('parses valid single IDs', () => {
expect(taskIdSchema.parse('1')).toBe('1');
expect(taskIdSchema.parse('15.2')).toBe('15.2');
expect(taskIdSchema.parse('HAM-123')).toBe('HAM-123');
});
it('throws on invalid IDs', () => {
expect(() => taskIdSchema.parse('')).toThrow();
expect(() => taskIdSchema.parse('abc')).toThrow();
expect(() => taskIdSchema.parse('HAM-123.2')).toThrow();
});
});
describe('taskIdsSchema', () => {
it('parses single ID', () => {
expect(taskIdsSchema.parse('1')).toBe('1');
expect(taskIdsSchema.parse('HAM-123')).toBe('HAM-123');
});
it('parses comma-separated IDs', () => {
expect(taskIdsSchema.parse('1,2,3')).toBe('1,2,3');
expect(taskIdsSchema.parse('1.2, 3.4')).toBe('1.2, 3.4');
expect(taskIdsSchema.parse('HAM-123, PROJ-456')).toBe('HAM-123, PROJ-456');
});
it('throws on invalid IDs', () => {
expect(() => taskIdsSchema.parse('abc')).toThrow();
expect(() => taskIdsSchema.parse('1,abc,3')).toThrow();
expect(() => taskIdsSchema.parse('HAM-123.2')).toThrow();
});
});
describe('parseTaskIds', () => {
it('parses single ID', () => {
expect(parseTaskIds('1')).toEqual(['1']);
expect(parseTaskIds('HAM-123')).toEqual(['HAM-123']);
});
it('parses comma-separated IDs', () => {
expect(parseTaskIds('1, 2, 3')).toEqual(['1', '2', '3']);
expect(parseTaskIds('HAM-123, PROJ-456')).toEqual(['HAM-123', 'PROJ-456']);
});
it('trims whitespace', () => {
expect(parseTaskIds(' 1 , 2 ')).toEqual(['1', '2']);
});
it('filters empty entries', () => {
expect(parseTaskIds('1,,2')).toEqual(['1', '2']);
});
it('throws on invalid IDs', () => {
expect(() => parseTaskIds('abc')).toThrow(/Invalid task ID format/);
expect(() => parseTaskIds('HAM-123.2')).toThrow(/Invalid task ID format/);
});
it('throws on empty input', () => {
expect(() => parseTaskIds('')).toThrow(/No valid task IDs/);
});
});
describe('extractParentId', () => {
it('extracts parent from numeric subtask ID', () => {
expect(extractParentId('1.2')).toBe('1');
expect(extractParentId('15.3.1')).toBe('15');
});
it('returns same ID for main tasks', () => {
expect(extractParentId('1')).toBe('1');
expect(extractParentId('15')).toBe('15');
expect(extractParentId('HAM-123')).toBe('HAM-123');
});
});
describe('isSubtaskId', () => {
it('returns true for numeric subtask IDs (local storage)', () => {
expect(isSubtaskId('1.2')).toBe(true);
expect(isSubtaskId('1.2.3')).toBe(true);
});
it('returns false for main task IDs', () => {
expect(isSubtaskId('1')).toBe(false);
expect(isSubtaskId('15')).toBe(false);
});
it('returns false for alphanumeric IDs (remote subtasks use separate IDs)', () => {
// In remote mode, subtasks have their own alphanumeric IDs (HAM-2, HAM-3)
// not dot notation, so HAM-123 is never a "subtask ID" in the dot-notation sense
expect(isSubtaskId('HAM-123')).toBe(false);
});
});
});

View File

@@ -0,0 +1,161 @@
/**
* @fileoverview Task ID validation utilities and Zod schemas
* Provides validation for task IDs used in MCP tools and CLI
*
* Supported formats:
* - Simple numeric: "1", "2", "15" (local file storage)
* - Numeric subtask: "1.2", "15.3" (local file storage, dot notation)
* - Numeric sub-subtask: "1.2.3", "15.3.1" (local file storage, dot notation)
* - Alphanumeric display IDs: "HAM-123", "PROJ-456" (remote API storage)
* Note: In remote mode, subtasks also use alphanumeric IDs (HAM-2, HAM-3),
* they don't use dot notation like local storage.
*
* NOT supported:
* - Alphanumeric with dot notation: "HAM-123.2" (doesn't exist in any mode)
*/
import { z } from 'zod';
/**
* Pattern for validating a single task ID
* Supports:
* - Numeric: "1", "15", "999"
* - Numeric subtasks: "1.2", "15.3.1"
* - Alphanumeric display IDs: "HAM-123", "PROJ-456" (main tasks only, no subtask notation)
*/
export const TASK_ID_PATTERN = /^(\d+(\.\d+)*|[A-Za-z]+-\d+)$/;
/**
* Validates a single task ID string
*
* @param id - The task ID to validate
* @returns True if the ID is valid
*
* @example
* ```typescript
* isValidTaskIdFormat("1"); // true
* isValidTaskIdFormat("15.2"); // true
* isValidTaskIdFormat("1.2.3"); // true
* isValidTaskIdFormat("HAM-123"); // true
* isValidTaskIdFormat("HAM-123.2"); // false (alphanumeric subtasks not supported)
* isValidTaskIdFormat("abc"); // false
* isValidTaskIdFormat(""); // false
* ```
*/
export function isValidTaskIdFormat(id: string): boolean {
return TASK_ID_PATTERN.test(id);
}
/**
* Zod schema for a single task ID
* Validates format: numeric, alphanumeric display ID, or numeric subtask
*/
export const taskIdSchema = z
.string()
.min(1, 'Task ID cannot be empty')
.refine(isValidTaskIdFormat, {
message:
"Invalid task ID format. Expected numeric (e.g., '15'), subtask (e.g., '15.2'), or display ID (e.g., 'HAM-123')"
});
/**
* Zod schema for comma-separated task IDs
* Validates that each ID in the comma-separated list is valid
*
* @example
* ```typescript
* taskIdsSchema.parse("1"); // valid
* taskIdsSchema.parse("1,2,3"); // valid
* taskIdsSchema.parse("1.2, 3.4"); // valid (spaces trimmed)
* taskIdsSchema.parse("HAM-123"); // valid
* taskIdsSchema.parse("abc"); // throws
* taskIdsSchema.parse("HAM-123.2"); // throws (alphanumeric subtasks not supported)
* ```
*/
export const taskIdsSchema = z
.string()
.min(1, 'Task ID(s) cannot be empty')
.refine(
(value) => {
const ids = value
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0);
return ids.length > 0 && ids.every(isValidTaskIdFormat);
},
{
message:
"Invalid task ID format. Expected numeric (e.g., '15'), subtask (e.g., '15.2'), or display ID (e.g., 'HAM-123'). Multiple IDs should be comma-separated."
}
);
/**
* Parse and validate comma-separated task IDs
*
* @param input - Comma-separated task ID string
* @returns Array of validated task IDs
* @throws Error if any ID is invalid
*
* @example
* ```typescript
* parseTaskIds("1, 2, 3"); // ["1", "2", "3"]
* parseTaskIds("1.2,3.4"); // ["1.2", "3.4"]
* parseTaskIds("HAM-123"); // ["HAM-123"]
* parseTaskIds("invalid"); // throws Error
* parseTaskIds("HAM-123.2"); // throws Error (alphanumeric subtasks not supported)
* ```
*/
export function parseTaskIds(input: string): string[] {
const ids = input
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0);
if (ids.length === 0) {
throw new Error('No valid task IDs provided');
}
const invalidIds = ids.filter((id) => !isValidTaskIdFormat(id));
if (invalidIds.length > 0) {
throw new Error(
`Invalid task ID format: ${invalidIds.join(', ')}. Expected numeric (e.g., '15'), subtask (e.g., '15.2'), or display ID (e.g., 'HAM-123')`
);
}
return ids;
}
/**
* Extract parent task ID from a subtask ID
*
* @param taskId - Task ID (e.g., "1.2.3")
* @returns Parent ID (e.g., "1") or the original ID if not a subtask
*
* @example
* ```typescript
* extractParentId("1.2.3"); // "1"
* extractParentId("1.2"); // "1"
* extractParentId("1"); // "1"
* ```
*/
export function extractParentId(taskId: string): string {
const parts = taskId.split('.');
return parts[0];
}
/**
* Check if a task ID represents a subtask
*
* @param taskId - Task ID to check
* @returns True if the ID contains a dot (subtask notation)
*
* @example
* ```typescript
* isSubtaskId("1.2"); // true
* isSubtaskId("1.2.3"); // true
* isSubtaskId("1"); // false
* ```
*/
export function isSubtaskId(taskId: string): boolean {
return taskId.includes('.');
}