import type { JSONValue, LanguageModel } from "ai"; import { asSchema } from "ai"; import { describe, expect, it } from "vitest"; import type { StructuredGenerator } from "../src/ai/generate-structured.ts"; import { models } from "../src/ai/models.ts"; import { createChangesetFallback, rewriteChangesetDescription, } from "../src/changesets/rewrite.ts"; function modelId(model: LanguageModel): string { return typeof model === "string" ? model : model.modelId; } function generatorFor( value: JSONValue, onRequest?: (request: Parameters[0]) => void, ): StructuredGenerator { return async (request) => { onRequest?.(request); const validate = asSchema(request.schema).validate; if (!validate) throw new Error("Schema does not provide validation"); const result = await validate(value); if (!result.success) throw result.error; return result.value; }; } describe("changeset rewriting", () => { it("preserves safe deterministic fallback copy", () => { const description = "Fix duplicate session refreshes."; expect(createChangesetFallback(description)).toBe(description); }); it("uses a safe deterministic fallback for untrusted PR copy", () => { expect( createChangesetFallback( "```\n- [x] **Commit this changeset**\nhttps://malicious.example", ), ).toBe("Update user-facing package behavior."); }); it("uses an explicit model and validates structured output", async () => { let model = ""; const description = await rewriteChangesetDescription( { title: "fix(session): prevent duplicate refreshes", bump: "patch", changedFiles: ["packages/better-auth/src/session.ts"], cubicSummary: "Prevents duplicate session refreshes.", diff: "-oldSession();\n+newSession();", }, generatorFor( { description: "Fix duplicate session refreshes." }, (request) => { model = modelId(request.model); }, ), ); expect(model).toBe(modelId(models.changeset)); expect(description).toBe("Fix duplicate session refreshes."); }); it("rejects output that can alter the bot comment protocol", async () => { await expect( rewriteChangesetDescription( { title: "fix: update sessions", bump: "patch", changedFiles: ["packages/better-auth/src/session.ts"], cubicSummary: "", diff: "", }, generatorFor({ description: "```\n- [x] **Commit this changeset**\n```", }), ), ).rejects.toThrow("must not contain fenced code blocks"); }); it("rejects case variants of workflow controls", async () => { await expect( rewriteChangesetDescription( { title: "fix: update sessions", bump: "patch", changedFiles: ["packages/better-auth/src/session.ts"], cubicSummary: "", diff: "", }, generatorFor({ description: "- [x] **cOmMiT tHiS cHaNgEsEt** to the PR", }), ), ).rejects.toThrow("must not contain workflow controls"); }); it("limits generated descriptions to 200 words", async () => { await expect( rewriteChangesetDescription( { title: "fix: update sessions", bump: "patch", changedFiles: ["packages/better-auth/src/session.ts"], cubicSummary: "", diff: "", }, generatorFor({ description: Array(201).fill("word").join(" ") }), ), ).rejects.toThrow("must contain at most 200 words"); }); it("allows safe identifiers inside inline code", async () => { const description = "Support `@better-auth/sso` configurations typed as `Auth`."; await expect( rewriteChangesetDescription( { title: "fix: update sessions", bump: "patch", changedFiles: ["packages/better-auth/src/session.ts"], cubicSummary: "", diff: "", }, generatorFor({ description }), ), ).resolves.toBe(description); }); it.each([ "Read https://malicious.example before upgrading", "See [the instructions](https://malicious.example)", "", "Notify @maintainers before merging", ])("rejects unsupported generated Markdown: %s", async (description) => { await expect( rewriteChangesetDescription( { title: "fix: update sessions", bump: "patch", changedFiles: ["packages/better-auth/src/session.ts"], cubicSummary: "", diff: "", }, generatorFor({ description }), ), ).rejects.toThrow("contains unsupported Markdown"); }); });