Merge pull request #1378 from eyaltoledano/next (Release 0.31.2)

This commit is contained in:
Ralph Khreish
2025-11-04 11:16:41 +01:00
committed by GitHub
14 changed files with 497 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix parse-prd schema to accept responses from models that omit optional fields (like Z.ai/GLM). Changed `metadata` field to use union pattern with `.default(null)` for better structured outputs compatibility.

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Fix ai response not showing price after its json was repaired

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Enable structured outputs for Z.ai providers. Added `supportsStructuredOutputs: true` to use `json_schema` mode for more reliable JSON generation in operations like parse-prd.

View File

@@ -426,7 +426,7 @@ async function analyzeTaskComplexity(options, context = {}) {
}
// With generateObject, we get structured data directly
complexityAnalysis = aiServiceResponse.mainResult.complexityAnalysis;
complexityAnalysis = aiServiceResponse.mainResult?.complexityAnalysis;
reportLog(
`Received ${complexityAnalysis.length} complexity analyses from AI.`,
'info'

View File

@@ -27,14 +27,19 @@ export const prdSingleTaskSchema = z.object({
// Define the Zod schema for the ENTIRE expected AI response object
export const prdResponseSchema = z.object({
tasks: z.array(prdSingleTaskSchema),
// Use union for better structured outputs compatibility
// Models understand "either return this object OR null" more reliably
metadata: z
.object({
projectName: z.string(),
totalTasks: z.number(),
sourceFile: z.string(),
generatedAt: z.string()
})
.nullable()
.union([
z.object({
projectName: z.string(),
totalTasks: z.number(),
sourceFile: z.string(),
generatedAt: z.string()
}),
z.null()
])
.default(null)
});
// ============================================================================

View File

@@ -357,8 +357,10 @@ export class BaseAIProvider {
object: parsed,
usage: {
// Extract usage information from the error if available
inputTokens: error.usage?.promptTokens || 0,
outputTokens: error.usage?.completionTokens || 0,
inputTokens:
error.usage?.promptTokens || error.usage?.inputTokens || 0,
outputTokens:
error.usage?.completionTokens || error.usage?.outputTokens || 0,
totalTokens: error.usage?.totalTokens || 0
}
};

View File

@@ -4,18 +4,17 @@
* Uses the exclusive coding API endpoint with OpenAI-compatible API.
*/
import { OpenAICompatibleProvider } from './openai-compatible.js';
import { ZAIProvider } from './zai.js';
/**
* Z.ai Coding Plan provider supporting GLM models through the dedicated coding endpoint.
* Extends ZAIProvider with only a different base URL.
*/
export class ZAICodingProvider extends OpenAICompatibleProvider {
export class ZAICodingProvider extends ZAIProvider {
constructor() {
super({
name: 'Z.ai (Coding Plan)',
apiKeyEnvVar: 'ZAI_API_KEY',
requiresApiKey: true,
defaultBaseURL: 'https://api.z.ai/api/coding/paas/v4/'
});
super();
// Override only the name and base URL
this.name = 'Z.ai (Coding Plan)';
this.defaultBaseURL = 'https://api.z.ai/api/coding/paas/v4/';
}
}

View File

@@ -15,7 +15,108 @@ export class ZAIProvider extends OpenAICompatibleProvider {
name: 'Z.ai',
apiKeyEnvVar: 'ZAI_API_KEY',
requiresApiKey: true,
defaultBaseURL: 'https://api.z.ai/api/paas/v4/'
defaultBaseURL: 'https://api.z.ai/api/paas/v4/',
supportsStructuredOutputs: true
});
}
/**
* Override token parameter preparation for ZAI
* ZAI API doesn't support max_tokens parameter
* @returns {object} Empty object for ZAI (doesn't support maxOutputTokens)
*/
prepareTokenParam() {
// ZAI API rejects max_tokens parameter with error code 1210
return {};
}
/**
* Introspects a Zod schema to find the property that expects an array
* @param {import('zod').ZodType} schema - The Zod schema to introspect
* @returns {string|null} The property name that expects an array, or null if not found
*/
findArrayPropertyInSchema(schema) {
try {
// Get the def object from Zod v4 API
const def = schema._zod.def;
// Check if schema is a ZodObject
const isObject = def?.type === 'object' || def?.typeName === 'ZodObject';
if (!isObject) {
return null;
}
// Get the shape - it can be a function, property, or getter
let shape = def.shape;
if (typeof shape === 'function') {
shape = shape();
}
if (!shape || typeof shape !== 'object') {
return null;
}
// Find the first property that is an array
for (const [key, value] of Object.entries(shape)) {
// Get the def object for the property using Zod v4 API
const valueDef = value._zod.def;
// Check if the property is a ZodArray
const isArray =
valueDef?.type === 'array' || valueDef?.typeName === 'ZodArray';
if (isArray) {
return key;
}
}
return null;
} catch (error) {
// If introspection fails, log and return null
console.warn('Failed to introspect Zod schema:', error.message);
return null;
}
}
/**
* Override generateObject to normalize GLM's response format
* GLM sometimes returns bare arrays instead of objects with properties,
* even when the schema has multiple properties.
* @param {object} params - Parameters for object generation
* @returns {Promise<object>} Normalized response
*/
async generateObject(params) {
const result = await super.generateObject(params);
// If result.object is an array, wrap it based on schema introspection
if (Array.isArray(result.object)) {
// Try to find the array property from the schema
const wrapperKey = this.findArrayPropertyInSchema(params.schema);
if (wrapperKey) {
return {
...result,
object: {
[wrapperKey]: result.object
}
};
}
// Fallback: if we can't introspect the schema, use the object name
// This handles edge cases where schema introspection might fail
console.warn(
`GLM returned a bare array for '${params.objectName}' but could not determine wrapper property from schema. Using objectName as fallback.`
);
return {
...result,
object: {
[params.objectName]: result.object
}
};
}
return result;
}
}

View File

@@ -61,7 +61,7 @@
},
"prompts": {
"default": {
"system": "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\nWhen determining dependencies for a new task, follow these principles:\n1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n2. Prioritize task dependencies that are semantically related to the functionality being built.\n3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\nThe dependencies array should contain task IDs (numbers) of prerequisite tasks.{{#if useResearch}}\n\nResearch current best practices and technologies relevant to this task.{{/if}}",
"system": "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema.\n\nIMPORTANT: Your response MUST be a JSON object with the following structure (no wrapper property, just these fields directly):\n{\n \"title\": \"string\",\n \"description\": \"string\",\n \"details\": \"string\",\n \"testStrategy\": \"string\",\n \"dependencies\": [array of numbers]\n}\n\nDo not include any other top-level properties. Do NOT wrap this in any additional object.\n\nPay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\nWhen determining dependencies for a new task, follow these principles:\n1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n2. Prioritize task dependencies that are semantically related to the functionality being built.\n3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\nThe dependencies array should contain task IDs (numbers) of prerequisite tasks.{{#if useResearch}}\n\nResearch current best practices and technologies relevant to this task.{{/if}}",
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, main entry points, and relevant source files\n4. Analyze the current implementation to understand what already exists\n\nBased on your analysis:\n- Identify existing components/features that relate to this new task\n- Understand the technology stack, frameworks, and patterns in use\n- Generate implementation details that align with the project's current architecture\n- Reference specific files, functions, or patterns from the codebase in your details\n\nProject Root: {{projectRoot}}\n\n{{/if}}You are generating the details for Task #{{newTaskId}}. Based on the user's request: \"{{prompt}}\", create a comprehensive new task for a software development project.\n \n {{gatheredContext}}\n \n {{#if useResearch}}Research current best practices, technologies, and implementation patterns relevant to this task. {{/if}}Based on the information about existing tasks provided above, include appropriate dependencies in the \"dependencies\" array. Only include task IDs that this new task directly depends on.\n \n Return your answer as a single JSON object matching the schema precisely:\n \n {\n \"title\": \"Task title goes here\",\n \"description\": \"A concise one or two sentence description of what the task involves\",\n \"details\": \"Detailed implementation steps, considerations, code examples, or technical approach\",\n \"testStrategy\": \"Specific steps to verify correct implementation and functionality\",\n \"dependencies\": [1, 3] // Example: IDs of tasks that must be completed before this task\n }\n \n Make sure the details and test strategy are comprehensive and specific{{#if useResearch}}, incorporating current best practices from your research{{/if}}. DO NOT include the task ID in the title.\n {{#if contextFromArgs}}{{contextFromArgs}}{{/if}}"
}
}

View File

@@ -44,7 +44,7 @@
},
"prompts": {
"default": {
"system": "You are an expert software architect and project manager analyzing task complexity. Your analysis should consider implementation effort, technical challenges, dependencies, and testing requirements.\n\nIMPORTANT: For each task, provide an analysis object with ALL of the following fields:\n- taskId: The ID of the task being analyzed (positive integer)\n- taskTitle: The title of the task\n- complexityScore: A score from 1-10 indicating complexity\n- recommendedSubtasks: Number of subtasks recommended (non-negative integer; 0 if no expansion needed)\n- expansionPrompt: A prompt to guide subtask generation\n- reasoning: Your reasoning for the complexity score",
"system": "You are an expert software architect and project manager analyzing task complexity. Your analysis should consider implementation effort, technical challenges, dependencies, and testing requirements.\n\nIMPORTANT: Your response MUST be a JSON object with a \"complexityAnalysis\" property containing an array of analysis objects. Each analysis object must have ALL of the following fields:\n- taskId: The ID of the task being analyzed (positive integer)\n- taskTitle: The title of the task\n- complexityScore: A score from 1-10 indicating complexity\n- recommendedSubtasks: Number of subtasks recommended (non-negative integer; 0 if no expansion needed)\n- expansionPrompt: A prompt to guide subtask generation\n- reasoning: Your reasoning for the complexity score\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before analyzing task complexity:\n\n1. Use the Glob tool to explore the project structure and understand the codebase size\n2. Use the Grep tool to search for existing implementations related to each task\n3. Use the Read tool to examine key files that would be affected by these tasks\n4. Understand the current implementation state, patterns used, and technical debt\n\nBased on your codebase analysis:\n- Assess complexity based on ACTUAL code that needs to be modified/created\n- Consider existing abstractions and patterns that could simplify implementation\n- Identify tasks that require refactoring vs. greenfield development\n- Factor in dependencies between existing code and new features\n- Provide more accurate subtask recommendations based on real code structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each.{{#if useResearch}} Consider current best practices, common implementation patterns, and industry standards in your analysis.{{/if}}\n\nTasks:\n{{{json tasks}}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n"
}
}

View File

@@ -68,17 +68,17 @@
"prompts": {
"complexity-report": {
"condition": "expansionPrompt",
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\n\nIMPORTANT: Your response MUST be a JSON object with a \"subtasks\" property containing an array of subtask objects. Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "Break down the following task:\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}\n\n{{expansionPrompt}}{{#if additionalContext}}\n\n{{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\n\n{{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nGenerate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks. CRITICAL: Use sequential IDs starting from {{nextSubtaskId}} (first={{nextSubtaskId}}, second={{nextSubtaskId}}+1, etc.)."
},
"research": {
"condition": "useResearch === true && !expansionPrompt",
"system": "You are an AI assistant with research capabilities analyzing and breaking down software development tasks.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
"system": "You are an AI assistant with research capabilities analyzing and breaking down software development tasks.\n\nIMPORTANT: Your response MUST be a JSON object with a \"subtasks\" property containing an array of subtask objects. Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks. Each subtask should be actionable and well-defined.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
},
"default": {
"system": "You are an AI assistant helping with task breakdown for software development. Break down high-level tasks into specific, actionable subtasks that can be implemented sequentially.\n\nIMPORTANT: Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)",
"system": "You are an AI assistant helping with task breakdown for software development. Break down high-level tasks into specific, actionable subtasks that can be implemented sequentially.\n\nIMPORTANT: Your response MUST be a JSON object with a \"subtasks\" property containing an array of subtask objects. Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
}
}

View File

@@ -0,0 +1,113 @@
import { jest } from '@jest/globals';
// Mock the OpenAI-compatible creation
const mockCreateOpenAICompatible = jest.fn(() => jest.fn());
jest.unstable_mockModule('@ai-sdk/openai-compatible', () => ({
createOpenAICompatible: mockCreateOpenAICompatible
}));
// Mock utils
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
log: jest.fn(),
resolveEnvVariable: jest.fn((key) => process.env[key])
}));
// Import after mocking
const { ZAIProvider } = await import('../../../src/ai-providers/zai.js');
const { ZAICodingProvider } = await import(
'../../../src/ai-providers/zai-coding.js'
);
describe('ZAI Provider', () => {
let provider;
beforeEach(() => {
jest.clearAllMocks();
provider = new ZAIProvider();
});
describe('Configuration', () => {
it('should have correct base configuration', () => {
expect(provider.name).toBe('Z.ai');
expect(provider.apiKeyEnvVar).toBe('ZAI_API_KEY');
expect(provider.requiresApiKey).toBe(true);
expect(provider.defaultBaseURL).toBe('https://api.z.ai/api/paas/v4/');
expect(provider.supportsStructuredOutputs).toBe(true);
});
});
describe('Token Parameter Handling', () => {
it('should not include max_tokens in requests', () => {
// ZAI API rejects max_tokens parameter (error code 1210)
const result = provider.prepareTokenParam('glm-4.6', 2000);
// Should return empty object instead of { maxOutputTokens: 2000 }
expect(result).toEqual({});
});
it('should return empty object even with undefined maxTokens', () => {
const result = provider.prepareTokenParam('glm-4.6', undefined);
expect(result).toEqual({});
});
it('should return empty object even with very large maxTokens', () => {
// ZAI may have lower limits than other providers
const result = provider.prepareTokenParam('glm-4.6', 204800);
expect(result).toEqual({});
});
});
describe('API Key Handling', () => {
it('should require API key', () => {
expect(provider.isRequiredApiKey()).toBe(true);
expect(provider.getRequiredApiKeyName()).toBe('ZAI_API_KEY');
});
it('should validate when API key is missing', () => {
expect(() => provider.validateAuth({})).toThrow(
'Z.ai API key is required'
);
});
it('should pass validation when API key is provided', () => {
expect(() => provider.validateAuth({ apiKey: 'test-key' })).not.toThrow();
});
});
});
describe('ZAI Coding Provider', () => {
let provider;
beforeEach(() => {
jest.clearAllMocks();
provider = new ZAICodingProvider();
});
describe('Configuration', () => {
it('should have correct base configuration', () => {
expect(provider.name).toBe('Z.ai (Coding Plan)');
expect(provider.apiKeyEnvVar).toBe('ZAI_API_KEY');
expect(provider.requiresApiKey).toBe(true);
expect(provider.defaultBaseURL).toBe(
'https://api.z.ai/api/coding/paas/v4/'
);
expect(provider.supportsStructuredOutputs).toBe(true);
});
});
describe('Token Parameter Handling', () => {
it('should not include max_tokens in requests', () => {
// ZAI Coding API also rejects max_tokens parameter
const result = provider.prepareTokenParam('glm-4.6', 2000);
// Should return empty object instead of { maxOutputTokens: 2000 }
expect(result).toEqual({});
});
it('should return empty object even with undefined maxTokens', () => {
const result = provider.prepareTokenParam('glm-4.6', undefined);
expect(result).toEqual({});
});
});
});

View File

@@ -0,0 +1,84 @@
import { describe, it, expect } from '@jest/globals';
import { z } from 'zod';
import { ZAIProvider } from '../../../src/ai-providers/zai.js';
describe('ZAIProvider - Schema Introspection', () => {
const provider = new ZAIProvider();
it('should find array property in schema with single array', () => {
const schema = z.object({
subtasks: z.array(z.string()),
metadata: z.object({ count: z.number() }).nullable()
});
const result = provider.findArrayPropertyInSchema(schema);
expect(result).toBe('subtasks');
});
it('should find first array property when multiple arrays exist', () => {
const schema = z.object({
tasks: z.array(z.string()),
items: z.array(z.number())
});
const result = provider.findArrayPropertyInSchema(schema);
expect(result).toBe('tasks');
});
it('should handle schema with no arrays', () => {
const schema = z.object({
name: z.string(),
count: z.number()
});
const result = provider.findArrayPropertyInSchema(schema);
expect(result).toBeNull();
});
it('should handle non-object schemas gracefully', () => {
const schema = z.array(z.string());
const result = provider.findArrayPropertyInSchema(schema);
expect(result).toBeNull();
});
it('should find complexityAnalysis array property', () => {
const schema = z.object({
complexityAnalysis: z.array(
z.object({
taskId: z.number(),
score: z.number()
})
),
metadata: z
.union([z.object({ total: z.number() }), z.null()])
.default(null)
});
const result = provider.findArrayPropertyInSchema(schema);
expect(result).toBe('complexityAnalysis');
});
it('should work with actual PRD response schema', () => {
const schema = z.object({
tasks: z.array(
z.object({
id: z.number(),
title: z.string()
})
),
metadata: z
.union([
z.object({
projectName: z.string(),
totalTasks: z.number()
}),
z.null()
])
.default(null)
});
const result = provider.findArrayPropertyInSchema(schema);
expect(result).toBe('tasks');
});
});

View File

@@ -0,0 +1,154 @@
import { describe, it, expect } from '@jest/globals';
import { prdResponseSchema } from '../../../../../scripts/modules/task-manager/parse-prd/parse-prd-config.js';
describe('PRD Response Schema', () => {
const validTask = {
id: 1,
title: 'Test Task',
description: 'Test description',
details: 'Test details',
testStrategy: 'Test strategy',
priority: 'high',
dependencies: [],
status: 'pending'
};
describe('Valid responses', () => {
it('should accept response with tasks and metadata', () => {
const response = {
tasks: [validTask],
metadata: {
projectName: 'Test Project',
totalTasks: 1,
sourceFile: 'test.txt',
generatedAt: '2025-01-01T00:00:00Z'
}
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(true);
});
it('should accept response with tasks and null metadata', () => {
const response = {
tasks: [validTask],
metadata: null
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(true);
});
it('should accept response with only tasks (no metadata field)', () => {
// This is what ZAI returns - just the tasks array without metadata
const response = {
tasks: [validTask]
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(true);
if (result.success) {
// With .default(null), omitted metadata becomes null
expect(result.data.metadata).toBeNull();
}
});
it('should accept response with multiple tasks', () => {
const response = {
tasks: [validTask, { ...validTask, id: 2, title: 'Second Task' }]
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(true);
});
});
describe('Invalid responses', () => {
it('should reject response without tasks field', () => {
const response = {
metadata: null
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(false);
});
it('should reject response with empty tasks array and invalid metadata', () => {
const response = {
tasks: [],
metadata: 'invalid'
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(false);
});
it('should reject task with missing required fields', () => {
const response = {
tasks: [
{
id: 1,
title: 'Test'
// missing other required fields
}
]
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(false);
});
it('should reject task with invalid priority', () => {
const response = {
tasks: [
{
...validTask,
priority: 'invalid'
}
]
};
const result = prdResponseSchema.safeParse(response);
expect(result.success).toBe(false);
});
});
describe('ZAI-specific response format', () => {
it('should handle ZAI response format (tasks only, no metadata)', () => {
// This is the actual format ZAI returns
const zaiResponse = {
tasks: [
{
id: 24,
title: 'Core Todo Data Management',
description:
'Implement the core data structure and CRUD operations',
status: 'pending',
dependencies: [],
priority: 'high',
details: 'Create a Todo data model with properties...',
testStrategy: 'Unit tests for TodoManager class...'
},
{
id: 25,
title: 'Todo UI and User Interactions',
description: 'Create the user interface components',
status: 'pending',
dependencies: [24],
priority: 'high',
details: 'Build a simple HTML/CSS/JS interface...',
testStrategy: 'UI component tests...'
}
]
};
const result = prdResponseSchema.safeParse(zaiResponse);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.tasks).toHaveLength(2);
// With .default(null), omitted metadata becomes null (not undefined)
expect(result.data.metadata).toBeNull();
}
});
});
});