jest.mock("@skyramp/skyramp", () => ({
    WorkspaceConfigManager: jest.fn(),
}));
jest.mock("../../services/AnalyticsService.js", () => ({
    AnalyticsService: { pushMCPToolEvent: jest.fn() },
}));
jest.mock("../../playwright/traceRecordingPrompt.js", () => ({
    getTraceRecordingPromptText: () => "",
}));
import { getTestbotPrompt } from "./testbot-prompts.js";
// Minimal args to invoke getTestbotPrompt — only services matter for these tests
const baseArgs = {
    prTitle: "Test PR",
    prDescription: "desc",
    summaryOutputFile: "/tmp/summary.json",
    repositoryPath: "/repo",
};
function callWithServices(services) {
    return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
    undefined, // maxRecommendations
    undefined, // maxGenerate
    undefined, // maxCritical
    undefined, // prNumber
    undefined, // userPrompt
    services);
}
function callWithStateOutputFile(stateOutputFile) {
    return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
    undefined, // maxRecommendations
    undefined, // maxGenerate
    undefined, // maxCritical
    undefined, // prNumber
    undefined, // userPrompt
    undefined, // services
    stateOutputFile);
}
function callFollowUpWithStateOutputFile(stateOutputFile) {
    return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
    undefined, // maxRecommendations
    undefined, // maxGenerate
    undefined, // maxCritical
    undefined, // prNumber
    "add more tests", // userPrompt — triggers follow-up path
    undefined, // services
    stateOutputFile);
}
describe("buildServiceContext (via getTestbotPrompt)", () => {
    it("renders full service with all fields", () => {
        const prompt = callWithServices([
            {
                serviceName: "backend",
                language: "python",
                framework: "pytest",
                testDirectory: "tests/python",
                api: { baseUrl: "http://localhost:8000" },
            },
        ]);
        expect(prompt).toContain('<service name="backend">');
        expect(prompt).toContain("<language>python</language>");
        expect(prompt).toContain("<framework>pytest</framework>");
        expect(prompt).toContain("<base_url>http://localhost:8000</base_url>");
        expect(prompt).toContain("<test_directory>tests/python</test_directory>");
        expect(prompt).toContain("</service>");
        expect(prompt).toContain("<services>");
        expect(prompt).toContain("</services>");
    });
    it("omits optional fields when absent", () => {
        const prompt = callWithServices([{ serviceName: "minimal" }]);
        expect(prompt).toContain('<service name="minimal">');
        expect(prompt).not.toContain("<language>");
        expect(prompt).not.toContain("<framework>");
        expect(prompt).not.toContain("<base_url>");
        expect(prompt).not.toContain("<test_directory>");
    });
    it("renders multiple services", () => {
        const prompt = callWithServices([
            { serviceName: "api", language: "python" },
            { serviceName: "frontend", language: "typescript" },
        ]);
        expect(prompt).toContain('<service name="api">');
        expect(prompt).toContain('<service name="frontend">');
    });
    it("does not render services block when services array is empty", () => {
        const prompt = callWithServices([]);
        expect(prompt).not.toContain("<services>");
        expect(prompt).not.toContain("<service");
    });
    it("does not render services block when services is undefined", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        expect(prompt).not.toContain("<services>");
    });
    it("escapes XML special characters in service name", () => {
        const prompt = callWithServices([
            { serviceName: 'my<service>&"name' },
        ]);
        expect(prompt).toContain('<service name="my&lt;service&gt;&amp;&quot;name">');
        expect(prompt).not.toContain('my<service>&"name">');
    });
    it("escapes XML special characters in field values", () => {
        const prompt = callWithServices([
            {
                serviceName: "svc",
                testDirectory: "tests/a&b",
                api: { baseUrl: "http://host?a=1&b=2" },
            },
        ]);
        expect(prompt).toContain("<test_directory>tests/a&amp;b</test_directory>");
        expect(prompt).toContain("<base_url>http://host?a=1&amp;b=2</base_url>");
    });
    it("places services block between REPOSITORY PATH and instruction line", () => {
        const prompt = callWithServices([{ serviceName: "svc" }]);
        const repoIdx = prompt.indexOf("<REPOSITORY PATH>");
        const servicesIdx = prompt.indexOf("<services>");
        const instructionIdx = prompt.indexOf("Use the Skyramp MCP server tools");
        expect(repoIdx).toBeLessThan(servicesIdx);
        expect(servicesIdx).toBeLessThan(instructionIdx);
    });
    it("has no extra blank line when services are absent", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        // Should go directly from REPOSITORY PATH closing tag to "Use the Skyramp"
        expect(prompt).toContain("</REPOSITORY PATH>\nUse the Skyramp MCP server tools");
    });
});
describe("stateOutputFile in getTestbotPrompt", () => {
    it("includes stateOutputFile in skyramp_analyze_changes call for first-run prompt", () => {
        const stateFile = "/tmp/skyramp/analyze-changes-state.json";
        const prompt = callWithStateOutputFile(stateFile);
        // The prompt must pass stateOutputFile to skyramp_analyze_changes
        expect(prompt).toContain(`\`stateOutputFile\`: "${stateFile}"`);
    });
    it("includes stateOutputFile in skyramp_analyze_changes call for follow-up prompt", () => {
        const stateFile = "/tmp/skyramp/analyze-changes-state.json";
        const prompt = callFollowUpWithStateOutputFile(stateFile);
        expect(prompt).toContain(`\`stateOutputFile\`: "${stateFile}"`);
    });
    it("omits stateOutputFile from skyramp_analyze_changes call when not provided", () => {
        const prompt = callWithStateOutputFile(undefined);
        expect(prompt).not.toContain("stateOutputFile");
    });
    it("omits stateOutputFile from follow-up prompt when not provided", () => {
        const prompt = callFollowUpWithStateOutputFile(undefined);
        expect(prompt).not.toContain("stateOutputFile");
    });
});
describe("uiCredentials in getTestbotPrompt", () => {
    function callWithUiCredentials(uiCredentials) {
        return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
        undefined, // maxRecommendations
        undefined, // maxGenerate
        undefined, // maxCritical
        undefined, // prNumber
        undefined, // userPrompt
        undefined, // services
        undefined, // stateOutputFile
        uiCredentials);
    }
    // The instructions text references `<ui-credentials>` in prose (backticked);
    // the injected block puts the opening tag alone on its own line. Match on
    // that to distinguish block from reference.
    const UI_CREDS_BLOCK_RE = /^<ui-credentials>$/m;
    it("does not render ui-credentials block when uiCredentials is undefined", () => {
        const prompt = callWithUiCredentials(undefined);
        expect(prompt).not.toMatch(UI_CREDS_BLOCK_RE);
    });
    it("does not render ui-credentials block when uiCredentials is empty string", () => {
        const prompt = callWithUiCredentials("");
        expect(prompt).not.toMatch(UI_CREDS_BLOCK_RE);
    });
    it("does not render ui-credentials block when uiCredentials is whitespace only", () => {
        const prompt = callWithUiCredentials("   ");
        expect(prompt).not.toMatch(UI_CREDS_BLOCK_RE);
    });
    it("renders ui-credentials block verbatim when credentials are provided", () => {
        const creds = "user@example.com:password123";
        const prompt = callWithUiCredentials(creds);
        expect(prompt).toMatch(UI_CREDS_BLOCK_RE);
        expect(prompt).toContain(creds);
        expect(prompt).toContain("</ui-credentials>");
    });
    it("places ui-credentials block before the instruction line", () => {
        const prompt = callWithUiCredentials("user@example.com:secret");
        const openIdx = prompt.search(UI_CREDS_BLOCK_RE);
        const instructionIdx = prompt.indexOf("Use the Skyramp MCP server tools");
        expect(openIdx).toBeGreaterThanOrEqual(0);
        expect(openIdx).toBeLessThan(instructionIdx);
    });
    it("passes XML special characters through unchanged (not real XML)", () => {
        // The <ui-credentials> block is framing for the agent's prompt context,
        // not real XML. Passwords with &, <, >, ", ' are typed verbatim into
        // login forms, so we must not escape them.
        const creds = 'user&name:<p>assword>"';
        const prompt = callWithUiCredentials(creds);
        expect(prompt).toContain(creds);
        expect(prompt).not.toContain("&amp;");
        expect(prompt).not.toContain("&lt;");
    });
    it("rejects credentials that would break the block framing", () => {
        // The only string we cannot safely inject is the closing tag itself,
        // since it would terminate the block and let subsequent content inject
        // prose into the credential context.
        expect(() => callWithUiCredentials("user:pass</ui-credentials>extra"))
            .toThrow("</ui-credentials>");
    });
});
describe("drift analysis inline embedding", () => {
    beforeAll(() => { process.env.SKYRAMP_FEATURE_TESTBOT = "1"; });
    afterAll(() => { delete process.env.SKYRAMP_FEATURE_TESTBOT; });
    function basePrompt() {
        return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
    }
    it("wraps inline drift rules in XML tags", () => {
        const prompt = basePrompt();
        expect(prompt).toContain("<drift_analysis_rules>");
        expect(prompt).toContain("</drift_analysis_rules>");
    });
    it("does not include a persona statement inside the inline XML block", () => {
        const prompt = basePrompt();
        const start = prompt.indexOf("<drift_analysis_rules>");
        const end = prompt.indexOf("</drift_analysis_rules>");
        const block = prompt.slice(start, end);
        expect(block).not.toContain("You are acting as a Skyramp Integration Architect");
    });
    it("drift_analysis_rules block appears inside Task 1, before Task 2", () => {
        const prompt = basePrompt();
        const task1Pos = prompt.indexOf("## Task 1");
        const rulesPos = prompt.indexOf("<drift_analysis_rules>");
        const task2Pos = prompt.indexOf("## Task 2");
        expect(rulesPos).toBeGreaterThan(task1Pos);
        expect(rulesPos).toBeLessThan(task2Pos);
    });
    it("Task 1 step 3 prose references drift_analysis_rules tag", () => {
        const prompt = basePrompt();
        expect(prompt).toContain("rules in `<drift_analysis_rules>`");
    });
});
describe("UI grounding via Task 2 capture-act-capture", () => {
    it("surfaces uiContext as guidance, not a contract", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        // uiContext fields are explained inline so the agent knows what to do with
        // them. Step 1 provides candidate URLs but gives fallback instructions
        // ("navigate from the workspace baseUrl and explore") for 404s/redirects,
        // treating candidates as guidance not a rigid contract.
        expect(prompt).toContain("uiContext");
        expect(prompt).toContain("candidateUiPages");
        expect(prompt).toContain("changedFrontendFiles");
        expect(prompt).toMatch(/navigate from the workspace baseUrl and explore/i);
    });
    it("step 5 enforces Blueprint Citation Invariant in natural prose", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        // Step 5 is the citation-invariant guardrail, not a "fill in tuples"
        // post-processing step (slice 4 cleanup: recs are grounded upstream).
        expect(prompt).toContain("Blueprint Citation Invariant");
        // Reasoning must be natural prose, NOT internal-identifier syntax.
        expect(prompt).toMatch(/natural prose/i);
        expect(prompt).toMatch(/internal-identifier syntax/i);
    });
    it("Task 2 no longer instructs the agent to fill in tuples post-hoc", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        // After slice 4 cleanup: Task 2 captures are for trace recording's own
        // assertions, not for retroactively rewriting recommendation reasoning.
        // The phrase "fill in tuples" must NOT appear anywhere in the prompt.
        expect(prompt).not.toMatch(/fill in tuples/i);
        expect(prompt).not.toMatch(/return to step 5 and fill/i);
    });
    it("Task 2 step 5 mentions possibleAssertions as available, NOT as required (slice 5.5 softening)", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        // Slice 5: AFTER-action browser_blueprint response includes
        // possibleAssertions[] — mechanically translated candidates.
        expect(prompt).toContain("possibleAssertions");
        // Slice 5.5: prompt explicitly tells the agent NOT to feel obligated.
        // Two P09 runs with the prior "emit at least one" directive showed the
        // agent over-using shallow visibility assertions at the expense of
        // integration-test depth. The softened version says: read them, use
        // when they happen to match what you'd write anyway, ignore otherwise.
        expect(prompt).toMatch(/do not feel obligated/i);
        expect(prompt).toMatch(/biased toward visibility/i);
        // The candidate format is still documented.
        expect(prompt).toMatch(/\bcode\b.*\brationale\b.*\btier\b/i);
        // The pre-existing "at least one browser_assert per page navigated"
        // rule should be preserved (it's about meaningful business-outcome
        // assertions, not about possibleAssertions).
        expect(prompt).toMatch(/at least one .browser_assert. per page navigated/i);
    });
});
describe("buildWorkspaceRecoveryPrefix", () => {
    const { buildWorkspaceRecoveryPrefix } = require("./testbot-prompts.js");
    it("includes repositoryPath in both init_scan and init_workspace instructions", () => {
        const prefix = buildWorkspaceRecoveryPrefix("/home/user/repo");
        expect(prefix).toContain('skyramp_init_scan with workspacePath "/home/user/repo"');
        expect(prefix).toContain('skyramp_init_workspace with workspacePath "/home/user/repo"');
    });
    it("includes force: true for both tool calls", () => {
        const prefix = buildWorkspaceRecoveryPrefix("/repo");
        expect(prefix).toContain("force: true, then call skyramp_init_workspace");
        expect(prefix).toContain("force: true to regenerate");
    });
    it("starts with IMPORTANT", () => {
        const prefix = buildWorkspaceRecoveryPrefix("/repo");
        expect(prefix).toMatch(/^IMPORTANT:/);
    });
});
describe("testsRepoDir in getTestbotPrompt", () => {
    function callWithTestsRepoDir(testsRepoDir) {
        return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
        undefined, // maxRecommendations
        undefined, // maxGenerate
        undefined, // maxCritical
        undefined, // prNumber
        undefined, // userPrompt
        undefined, // services
        undefined, // stateOutputFile
        undefined, // uiCredentials
        testsRepoDir);
    }
    function callFollowUpWithTestsRepoDir(testsRepoDir) {
        return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
        undefined, // maxRecommendations
        undefined, // maxGenerate
        undefined, // maxCritical
        undefined, // prNumber
        "add more tests", // userPrompt — triggers follow-up path
        undefined, // services
        undefined, // stateOutputFile
        undefined, // uiCredentials
        testsRepoDir);
    }
    it("includes testsRepoDir in skyramp_analyze_changes call for first-run prompt", () => {
        const dir = "/home/runner/work/_temp/skyramp/test-repo";
        const prompt = callWithTestsRepoDir(dir);
        expect(prompt).toContain(`\`testsRepoDir\`: "${dir}"`);
    });
    it("includes testsRepoDir in skyramp_analyze_changes call for follow-up prompt", () => {
        const dir = "/home/runner/work/_temp/skyramp/test-repo";
        const prompt = callFollowUpWithTestsRepoDir(dir);
        expect(prompt).toContain(`\`testsRepoDir\`: "${dir}"`);
    });
    it("omits testsRepoDir when not provided", () => {
        const prompt = callWithTestsRepoDir(undefined);
        expect(prompt).not.toContain("testsRepoDir");
    });
    it("omits testsRepoDir from follow-up prompt when not provided", () => {
        const prompt = callFollowUpWithTestsRepoDir(undefined);
        expect(prompt).not.toContain("testsRepoDir");
    });
});
describe("testbot prompt blueprint-grounded recommendations (slice 4)", () => {
    it("instructs the agent to call skyramp_ui_analyze_changes before skyramp_analyze_changes", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        const uiacIdx = prompt.indexOf("skyramp_ui_analyze_changes");
        const acIdx = prompt.indexOf("skyramp_analyze_changes");
        expect(uiacIdx).toBeGreaterThan(-1);
        expect(acIdx).toBeGreaterThan(uiacIdx);
    });
    it("Task 1 step 1 instructs the agent to capture blueprints (without threading them through a param)", () => {
        const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
        // Captures stay in tool-result history; analyze_changes returns the
        // authoring rules and the agent supplies the captured vocabulary.
        expect(prompt).toMatch(/browser_blueprint`?\s*to capture/i);
        expect(prompt).toMatch(/tool-result history/i);
        // Make sure we removed the old capturedBlueprints threading directive.
        expect(prompt).not.toMatch(/capturedBlueprints/);
    });
});
