"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const strict_1 = __importDefault(require("node:assert/strict"));
const run_1 = require("../commands/run");
// ─── helpers ──────────────────────────────────────────────────────────────────
function stubTask(overrides = {}) {
    return {
        id: 'TASK-1',
        name: 'Break down feature X',
        description: 'Plan sub-tickets for feature X.',
        status: 'open',
        url: 'https://example.test/t/TASK-1',
        tags: ['planning'],
        ...overrides,
    };
}
function stubConfig(overrides = {}) {
    return {
        provider: 'clickup',
        clickupApiKey: '',
        clickupTeamId: '',
        clickupTag: '',
        clickupPendingStatus: 'pending',
        clickupOpenStatus: 'open',
        clickupInReviewStatus: 'review',
        jiraBaseUrl: '',
        jiraEmail: '',
        jiraApiToken: '',
        jiraProject: '',
        jiraLabel: '',
        jiraPendingStatus: '',
        jiraInReviewStatus: '',
        linearApiKey: '',
        linearTeamId: '',
        linearLabel: '',
        linearPendingStatus: '',
        linearInReviewStatus: '',
        mondayApiToken: '',
        mondayBoardId: '',
        mondayStatusColumnId: '',
        mondayGroupId: '',
        notionApiKey: '',
        notionDatabaseId: '',
        notionStatusProperty: '',
        notionPendingStatus: '',
        notionInReviewStatus: '',
        trelloApiKey: '',
        trelloToken: '',
        trelloBoardId: '',
        trelloLabel: '',
        trelloOpenList: '',
        trelloPendingList: '',
        trelloInProgressList: '',
        trelloInReviewList: '',
        trelloOpenStatus: '',
        trelloPendingStatus: '',
        trelloInReviewStatus: '',
        nonCodeTag: '',
        nonCodeClickupTeamId: '',
        nonCodeJiraProject: '',
        nonCodeLinearTeamId: '',
        clickupListId: '',
        assigneeTag: '',
        gitRemote: 'origin',
        githubBaseBranch: 'main',
        githubRepo: '',
        agents: ['claude'],
        devNotesMode: 'smart',
        triggerWord: 'aidev-continue',
        thinkingTag: 'thinking',
        planningTag: 'planning',
        commentPrefix: '[aidev]',
        hooksPath: '',
        acceptedTag: '',
        doneStatus: '',
        autoCompress: false,
        compressThreshold: 0,
        ...overrides,
    };
}
function makeMockProvider(opts = {}) {
    const statusUpdates = [];
    const postedComments = [];
    const createdTasks = [];
    const removedTags = [];
    const provider = {
        statusUpdates,
        postedComments,
        createdTasks,
        removedTags,
        fetchTasks: async () => [],
        fetchTasksByStatus: async () => [],
        getComments: async () => opts.comments ?? [],
        postComment: async (_taskId, text) => {
            postedComments.push(text);
        },
        updateStatus: async (_taskId, status) => {
            statusUpdates.push(status);
        },
        createTask: async (params) => {
            if (opts.createTaskImpl)
                return opts.createTaskImpl(params);
            const id = `sub-${createdTasks.length + 1}`;
            createdTasks.push({ params });
            return { id, url: `https://example.test/t/${id}` };
        },
    };
    if (opts.availableStatuses) {
        provider.fetchAvailableStatuses = async () => opts.availableStatuses;
    }
    if (opts.hasRemoveTag !== false) {
        provider.removeTag = async (taskId, tag) => {
            removedTags.push({ taskId, tag });
        };
    }
    // Wrap createTask to also record params even when impl is provided
    // (including when impl throws — we still want to know it was attempted).
    if (opts.createTaskImpl) {
        const impl = opts.createTaskImpl;
        provider.createTask = async (params) => {
            createdTasks.push({ params });
            return impl(params);
        };
    }
    return provider;
}
function stubRunner(output, opts = {}) {
    return {
        name: 'stub',
        isAvailable: () => opts.available ?? true,
        run: async () => ({
            success: opts.success ?? true,
            output,
            error: '',
        }),
    };
}
// ─── isPlanningTask ───────────────────────────────────────────────────────────
(0, node_test_1.describe)('isPlanningTask', () => {
    (0, node_test_1.it)('returns true when task has the configured planning tag', () => {
        const task = stubTask({ tags: ['planning', 'frontend'] });
        const config = stubConfig({ planningTag: 'planning' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), true);
    });
    (0, node_test_1.it)('returns false when task does not have the planning tag', () => {
        const task = stubTask({ tags: ['frontend', 'urgent'] });
        const config = stubConfig({ planningTag: 'planning' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), false);
    });
    (0, node_test_1.it)('is case-insensitive on the planning tag', () => {
        const task = stubTask({ tags: ['PLANNING'] });
        const config = stubConfig({ planningTag: 'planning' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), true);
    });
    (0, node_test_1.it)('is case-insensitive on the configured tag value', () => {
        const task = stubTask({ tags: ['breakdown'] });
        const config = stubConfig({ planningTag: 'BREAKDOWN' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), true);
    });
    (0, node_test_1.it)('returns false when planningTag is missing/empty in config', () => {
        const task = stubTask({ tags: ['planning'] });
        const config = stubConfig({ planningTag: '' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), false);
    });
    (0, node_test_1.it)('returns false when task has no tags', () => {
        const task = stubTask({ tags: [] });
        const config = stubConfig({ planningTag: 'planning' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), false);
    });
});
// ─── buildPlanningAnalysisPrompt ──────────────────────────────────────────────
(0, node_test_1.describe)('buildPlanningAnalysisPrompt', () => {
    (0, node_test_1.it)('includes the task name', () => {
        const task = stubTask({ name: 'Migrate auth to JWT' });
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(task, '');
        strict_1.default.ok(prompt.includes('Migrate auth to JWT'));
    });
    (0, node_test_1.it)('includes the task description', () => {
        const task = stubTask({ description: 'Replace session cookies with JWT tokens.' });
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(task, '');
        strict_1.default.ok(prompt.includes('Replace session cookies with JWT tokens.'));
    });
    (0, node_test_1.it)('falls back to a placeholder when description is empty', () => {
        const task = stubTask({ description: '' });
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(task, '');
        strict_1.default.ok(prompt.includes('no description provided'));
    });
    (0, node_test_1.it)('includes the parent task tags', () => {
        const task = stubTask({ tags: ['planning', 'frontend', 'urgent'] });
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(task, '');
        strict_1.default.ok(prompt.includes('Parent task tags:'));
        strict_1.default.ok(prompt.includes('planning'));
        strict_1.default.ok(prompt.includes('frontend'));
        strict_1.default.ok(prompt.includes('urgent'));
    });
    (0, node_test_1.it)('omits the tags line when the task has no tags', () => {
        const task = stubTask({ tags: [] });
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(task, '');
        strict_1.default.ok(!prompt.includes('Parent task tags:'));
    });
    (0, node_test_1.it)('includes the conversation context when provided', () => {
        const task = stubTask();
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(task, '\n\nConversation context:\nuser: do it carefully');
        strict_1.default.ok(prompt.includes('do it carefully'));
    });
    (0, node_test_1.it)('instructs the AI that sub-tasks must be self-contained', () => {
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(stubTask(), '');
        strict_1.default.ok(/self-contained|isolated/i.test(prompt));
    });
    (0, node_test_1.it)('describes the JSON response schema', () => {
        const prompt = (0, run_1.buildPlanningAnalysisPrompt)(stubTask(), '');
        strict_1.default.ok(prompt.includes('"clarification"'));
        strict_1.default.ok(prompt.includes('"subtasks"'));
    });
});
// ─── parsePlanningResponse ────────────────────────────────────────────────────
(0, node_test_1.describe)('parsePlanningResponse', () => {
    (0, node_test_1.it)('parses a well-formed list of sub-tasks', () => {
        const output = JSON.stringify({
            clarification: null,
            subtasks: [
                { title: 'Add login form', description: 'Build the login UI in src/login.tsx.', priority: 2 },
                { title: 'Add server route', description: 'Add POST /auth/login in server/auth.ts.' },
            ],
        });
        const parsed = (0, run_1.parsePlanningResponse)(output);
        strict_1.default.ok(parsed !== null);
        strict_1.default.equal(parsed.clarification, undefined);
        strict_1.default.equal(parsed.subtasks.length, 2);
        strict_1.default.equal(parsed.subtasks[0].title, 'Add login form');
        strict_1.default.equal(parsed.subtasks[0].priority, 2);
        strict_1.default.equal(parsed.subtasks[1].priority, undefined);
    });
    (0, node_test_1.it)('returns a clarification when the AI asks one', () => {
        const output = JSON.stringify({
            clarification: 'Which auth provider should be used?',
            subtasks: [],
        });
        const parsed = (0, run_1.parsePlanningResponse)(output);
        strict_1.default.ok(parsed !== null);
        strict_1.default.equal(parsed.clarification, 'Which auth provider should be used?');
        strict_1.default.equal(parsed.subtasks.length, 0);
    });
    (0, node_test_1.it)('returns null on malformed JSON', () => {
        strict_1.default.equal((0, run_1.parsePlanningResponse)('this is not json at all'), null);
        strict_1.default.equal((0, run_1.parsePlanningResponse)('{ unterminated'), null);
    });
    (0, node_test_1.it)('returns null on empty sub-tasks with no clarification', () => {
        const output = JSON.stringify({ clarification: null, subtasks: [] });
        strict_1.default.equal((0, run_1.parsePlanningResponse)(output), null);
    });
    (0, node_test_1.it)('returns null when "null" string is given as clarification and subtasks are empty', () => {
        const output = JSON.stringify({ clarification: 'null', subtasks: [] });
        strict_1.default.equal((0, run_1.parsePlanningResponse)(output), null);
    });
    (0, node_test_1.it)('skips sub-tasks missing a title or description', () => {
        const output = JSON.stringify({
            clarification: null,
            subtasks: [
                { title: 'Valid', description: 'Has a body.' },
                { title: '', description: 'No title' },
                { title: 'No description', description: '' },
                { description: 'Title missing entirely' },
            ],
        });
        const parsed = (0, run_1.parsePlanningResponse)(output);
        strict_1.default.ok(parsed !== null);
        strict_1.default.equal(parsed.subtasks.length, 1);
        strict_1.default.equal(parsed.subtasks[0].title, 'Valid');
    });
    (0, node_test_1.it)('extracts JSON from output that has surrounding text', () => {
        const output = `Here is the plan:\n${JSON.stringify({
            clarification: null,
            subtasks: [{ title: 'A', description: 'B' }],
        })}\nLet me know!`;
        const parsed = (0, run_1.parsePlanningResponse)(output);
        strict_1.default.ok(parsed !== null);
        strict_1.default.equal(parsed.subtasks.length, 1);
    });
});
// ─── implementPlanningTask: happy path ────────────────────────────────────────
(0, node_test_1.describe)('implementPlanningTask — happy path', () => {
    (0, node_test_1.it)('creates tasks, strips planning tag, threads listId, removes tag, transitions to done', async () => {
        const task = stubTask({
            id: 'T-100',
            tags: ['Planning', 'Frontend', 'urgent'],
            sourceListId: 'LIST-42',
        });
        const config = stubConfig({ planningTag: 'planning', doneStatus: 'closed' });
        const provider = makeMockProvider();
        const runner = stubRunner(JSON.stringify({
            clarification: null,
            subtasks: [
                { title: 'Sub A', description: 'Implement A in src/a.ts.', priority: 2 },
                { title: 'Sub B', description: 'Implement B in src/b.ts.' },
            ],
        }));
        await (0, run_1.implementPlanningTask)(task, config, provider, [runner]);
        strict_1.default.equal(provider.createdTasks.length, 2);
        // planning tag stripped (case-insensitive), other tags preserved with original casing.
        for (const rec of provider.createdTasks) {
            strict_1.default.ok(!rec.params.tags.some((t) => t.toLowerCase() === 'planning'));
            strict_1.default.deepEqual(rec.params.tags, ['Frontend', 'urgent']);
            strict_1.default.equal(rec.params.listId, 'LIST-42');
        }
        strict_1.default.equal(provider.createdTasks[0].params.title, 'Sub A');
        strict_1.default.equal(provider.createdTasks[0].params.priority, 2);
        strict_1.default.equal(provider.createdTasks[1].params.priority, undefined);
        // parent planning tag removed.
        strict_1.default.equal(provider.removedTags.length, 1);
        strict_1.default.equal(provider.removedTags[0].taskId, 'T-100');
        strict_1.default.equal(provider.removedTags[0].tag, 'planning');
        // status transitions: first "in progress", last "closed" (done).
        strict_1.default.equal(provider.statusUpdates[0], 'in progress');
        strict_1.default.equal(provider.statusUpdates[provider.statusUpdates.length - 1], 'closed');
        // a summary comment listing both tickets was posted.
        const summary = provider.postedComments.find((c) => c.includes('Planning complete'));
        strict_1.default.ok(summary, 'expected planning summary comment');
        strict_1.default.ok(summary.includes('Sub A'));
        strict_1.default.ok(summary.includes('Sub B'));
    });
    (0, node_test_1.it)('auto-detects done status via fetchAvailableStatuses when doneStatus is empty', async () => {
        const task = stubTask({ sourceListId: 'L1' });
        const config = stubConfig({ planningTag: 'planning', doneStatus: '' });
        const provider = makeMockProvider({ availableStatuses: ['open', 'in progress', 'Done'] });
        const runner = stubRunner(JSON.stringify({
            clarification: null,
            subtasks: [{ title: 'X', description: 'Y' }],
        }));
        await (0, run_1.implementPlanningTask)(task, config, provider, [runner]);
        strict_1.default.equal(provider.statusUpdates[provider.statusUpdates.length - 1], 'Done');
    });
    (0, node_test_1.it)('omits removeTag silently when the provider does not implement it', async () => {
        const task = stubTask({ sourceListId: 'L1' });
        const config = stubConfig({ planningTag: 'planning', doneStatus: 'closed' });
        const provider = makeMockProvider({ hasRemoveTag: false });
        const runner = stubRunner(JSON.stringify({
            clarification: null,
            subtasks: [{ title: 'X', description: 'Y' }],
        }));
        await (0, run_1.implementPlanningTask)(task, config, provider, [runner]);
        strict_1.default.equal(provider.removedTags.length, 0);
        strict_1.default.equal(provider.statusUpdates[provider.statusUpdates.length - 1], 'closed');
    });
    (0, node_test_1.it)('passes undefined listId when task.sourceListId is unset', async () => {
        const task = stubTask({ sourceListId: undefined });
        const config = stubConfig({ planningTag: 'planning', doneStatus: 'closed' });
        const provider = makeMockProvider();
        const runner = stubRunner(JSON.stringify({
            clarification: null,
            subtasks: [{ title: 'X', description: 'Y' }],
        }));
        await (0, run_1.implementPlanningTask)(task, config, provider, [runner]);
        strict_1.default.equal(provider.createdTasks.length, 1);
        strict_1.default.equal(provider.createdTasks[0].params.listId, undefined);
    });
});
// ─── implementPlanningTask: clarification path ────────────────────────────────
(0, node_test_1.describe)('implementPlanningTask — clarification path', () => {
    (0, node_test_1.it)('posts the clarification, moves to pending, does not create sub-tasks', async () => {
        const task = stubTask();
        const config = stubConfig({ planningTag: 'planning', clickupPendingStatus: 'pending' });
        const provider = makeMockProvider();
        const runner = stubRunner(JSON.stringify({
            clarification: 'Which database driver should be used?',
            subtasks: [],
        }));
        await (0, run_1.implementPlanningTask)(task, config, provider, [runner]);
        strict_1.default.equal(provider.createdTasks.length, 0);
        strict_1.default.equal(provider.removedTags.length, 0);
        // status order: in progress, then pending. No transition to done.
        strict_1.default.deepEqual(provider.statusUpdates, ['in progress', 'pending']);
        const clarification = provider.postedComments.find((c) => c.includes('Which database driver should be used?'));
        strict_1.default.ok(clarification, 'expected clarification comment');
        strict_1.default.ok(clarification.startsWith('[aidev]'));
    });
});
// ─── implementPlanningTask: per-subtask failure path ──────────────────────────
(0, node_test_1.describe)('implementPlanningTask — per-subtask failure', () => {
    (0, node_test_1.it)('continues after a createTask failure and still transitions parent to done', async () => {
        const task = stubTask({ sourceListId: 'L1' });
        const config = stubConfig({ planningTag: 'planning', doneStatus: 'closed' });
        let calls = 0;
        const provider = makeMockProvider({
            createTaskImpl: async (_params) => {
                calls++;
                if (calls === 2)
                    throw new Error('boom — provider rejected ticket');
                return { id: `s-${calls}`, url: `https://example.test/t/s-${calls}` };
            },
        });
        const runner = stubRunner(JSON.stringify({
            clarification: null,
            subtasks: [
                { title: 'First', description: 'do first' },
                { title: 'Second', description: 'do second' },
                { title: 'Third', description: 'do third' },
            ],
        }));
        await (0, run_1.implementPlanningTask)(task, config, provider, [runner]);
        // All three drafts attempted.
        strict_1.default.equal(provider.createdTasks.length, 3);
        // Parent still transitions to done despite mid-loop failure.
        strict_1.default.equal(provider.statusUpdates[provider.statusUpdates.length - 1], 'closed');
        // Planning tag removed.
        strict_1.default.equal(provider.removedTags.length, 1);
        // Summary mentions the failure.
        const summary = provider.postedComments.find((c) => c.includes('Planning complete'));
        strict_1.default.ok(summary, 'expected planning summary comment');
        strict_1.default.ok(summary.includes('Second'), 'failed sub-task should be listed in summary');
        strict_1.default.ok(summary.includes('boom'), 'failure reason should be surfaced');
        strict_1.default.ok(/Failed to create/.test(summary));
    });
});
// ─── routing precedence ──────────────────────────────────────────────────────
(0, node_test_1.describe)('routing precedence', () => {
    (0, node_test_1.it)('a task tagged with both planning and thinking routes to planning', () => {
        const task = stubTask({ tags: ['planning', 'thinking'] });
        const config = stubConfig({ planningTag: 'planning', thinkingTag: 'thinking' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), true);
        strict_1.default.equal((0, run_1.isThinkingTask)(task, config), true);
        // Mirrors processTask / processNonCodeTask:
        //   if (isPlanningTask) implementPlanningTask
        //   else if (isThinkingTask) implementThinkingTask
        //   else implementTask
        const route = (0, run_1.isPlanningTask)(task, config)
            ? 'planning'
            : (0, run_1.isThinkingTask)(task, config)
                ? 'thinking'
                : 'normal';
        strict_1.default.equal(route, 'planning');
    });
    (0, node_test_1.it)('a thinking-only task routes to thinking', () => {
        const task = stubTask({ tags: ['thinking'] });
        const config = stubConfig({ planningTag: 'planning', thinkingTag: 'thinking' });
        strict_1.default.equal((0, run_1.isPlanningTask)(task, config), false);
        strict_1.default.equal((0, run_1.isThinkingTask)(task, config), true);
    });
});
//# sourceMappingURL=planning.test.js.map