UNPKG

convex

Version:

Client for the Convex Cloud

196 lines (167 loc) 6.11 kB
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import fs from "fs"; import os from "os"; import path from "path"; import type { Context } from "../../../bundler/context.js"; import { AGENTS_MD_END_MARKER, AGENTS_MD_START_MARKER, } from "../../codegen_templates/agentsmd.js"; const { mockPromptYesNo } = vi.hoisted(() => { return { mockPromptYesNo: vi.fn() }; }); vi.mock("@sentry/node", () => ({ captureException: vi.fn(), captureMessage: vi.fn(), })); vi.mock("../../../bundler/log.js", () => ({ logMessage: vi.fn(), logFinishedStep: vi.fn(), })); vi.mock("../versionApi.js", () => ({ downloadGuidelines: vi.fn(async () => "prompt test guidelines content"), fetchAgentSkillsSha: vi.fn(async () => "prompt-test-sha"), getVersion: vi.fn(async () => ({ kind: "ok", data: { message: null, guidelinesHash: "prompt-test-guidelines-hash", agentSkillsSha: "prompt-test-agent-skills-sha", disableSkillsCli: false, }, })), })); vi.mock("child_process", () => ({ default: { spawn: vi.fn(() => ({ stdout: { on: vi.fn() }, stderr: { on: vi.fn() }, on: (event: string, cb: (code: number) => void) => { if (event === "close") cb(0); }, })), }, })); vi.mock("../utils/prompts.js", async (importOriginal) => { const actual = await importOriginal<typeof import("../utils/prompts.js")>(); return { ...actual, promptYesNo: mockPromptYesNo, }; }); import { maybeSetupAiFiles } from "./index.js"; function makeTmpDir(): string { return fs.mkdtempSync(`${os.tmpdir()}${path.sep}`); } const fakeCtx: Context = { fs: { readFile: (p: string) => fs.readFileSync(p, { encoding: "utf8" }), exists: (p: string) => fs.existsSync(p), readdir: (p: string) => fs.readdirSync(p), stat: (p: string) => fs.statSync(p), readUtf8File: (p: string) => fs.readFileSync(p, { encoding: "utf8" }), lstat: (p: string) => fs.lstatSync(p), } as any, deprecationMessagePrinted: false, crash: async (args) => { throw new Error(args.printedMessage ?? "crash"); }, registerCleanup: () => "handle", removeCleanup: () => null as any, bigBrainAuth: () => null, _updateBigBrainAuth: () => {}, }; describe("maybeSetupAiFiles interactive prompt", () => { let tmpDir: string; let convexDir: string; let originalIsTTY: boolean | undefined; const aiDir = () => path.join(convexDir, "_generated", "ai"); const guidelinesPath = () => path.join(aiDir(), "guidelines.md"); const statePath = () => path.join(aiDir(), "ai-files.state.json"); const projectConfigPath = () => path.join(tmpDir, "convex.json"); beforeEach(() => { tmpDir = makeTmpDir(); convexDir = path.join(tmpDir, "convex"); fs.mkdirSync(convexDir, { recursive: true }); fs.writeFileSync(path.join(convexDir, "schema.ts"), ""); originalIsTTY = process.stdin.isTTY; process.stdin.isTTY = true; mockPromptYesNo.mockResolvedValue(true); }); afterEach(() => { process.stdin.isTTY = originalIsTTY!; fs.rmSync(tmpDir, { recursive: true, force: true }); vi.unstubAllEnvs(); vi.clearAllMocks(); }); test("user accepts prompt: AI files are installed", async () => { mockPromptYesNo.mockResolvedValue(true); await maybeSetupAiFiles({ ctx: fakeCtx, convexDir, projectDir: tmpDir }); expect(fs.existsSync(guidelinesPath())).toBe(true); expect(fs.existsSync(statePath())).toBe(true); expect(fs.existsSync(path.join(tmpDir, "AGENTS.md"))).toBe(true); expect(fs.existsSync(path.join(tmpDir, "CLAUDE.md"))).toBe(true); expect(fs.readFileSync(guidelinesPath(), "utf8")).toBe( "prompt test guidelines content", ); }); test("user declines prompt: no config and no AI files are written", async () => { mockPromptYesNo.mockResolvedValue(false); await maybeSetupAiFiles({ ctx: fakeCtx, convexDir, projectDir: tmpDir }); expect(fs.existsSync(guidelinesPath())).toBe(false); expect(fs.existsSync(path.join(tmpDir, "AGENTS.md"))).toBe(false); expect(fs.existsSync(path.join(tmpDir, "CLAUDE.md"))).toBe(false); expect(fs.existsSync(projectConfigPath())).toBe(false); }); test("non-interactive terminal skips the prompt and does not install AI files", async () => { process.stdin.isTTY = false; await maybeSetupAiFiles({ ctx: fakeCtx, convexDir, projectDir: tmpDir }); expect(fs.existsSync(guidelinesPath())).toBe(false); expect(fs.existsSync(statePath())).toBe(false); expect(fs.existsSync(path.join(tmpDir, "AGENTS.md"))).toBe(false); expect(fs.existsSync(path.join(tmpDir, "CLAUDE.md"))).toBe(false); expect(fs.existsSync(projectConfigPath())).toBe(false); }); test("existing state file updates AI files without prompting", async () => { const stateDir = path.join(convexDir, "_generated", "ai"); fs.mkdirSync(stateDir, { recursive: true }); fs.writeFileSync( path.join(stateDir, "ai-files.state.json"), JSON.stringify( { guidelinesHash: "hash", agentsMdSectionHash: "hash", claudeMdHash: "hash", agentSkillsSha: "sha", installedSkillNames: [], }, null, 2, ), ); await maybeSetupAiFiles({ ctx: fakeCtx, convexDir, projectDir: tmpDir }); expect(fs.existsSync(path.join(stateDir, "ai-files.state.json"))).toBe( true, ); expect(fs.existsSync(guidelinesPath())).toBe(true); expect(fs.readFileSync(guidelinesPath(), "utf8")).toBe( "prompt test guidelines content", ); }); test("existing AGENTS.md managed section rebuilds state without prompting", async () => { fs.writeFileSync( path.join(tmpDir, "AGENTS.md"), [ "# Team notes", "", AGENTS_MD_START_MARKER, "Managed section", AGENTS_MD_END_MARKER, "", ].join("\n"), ); await maybeSetupAiFiles({ ctx: fakeCtx, convexDir, projectDir: tmpDir }); expect(fs.existsSync(statePath())).toBe(true); expect(fs.existsSync(guidelinesPath())).toBe(true); }); });