postgresai 0.15.0-dev.3 → 0.15.0-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/postgres-ai.ts +275 -50
- package/dist/bin/postgres-ai.js +833 -394
- package/lib/checkup.ts +16 -10
- package/lib/config.ts +3 -0
- package/lib/init.ts +1 -1
- package/lib/issues.ts +72 -72
- package/lib/reports.ts +12 -12
- package/lib/storage.ts +291 -0
- package/lib/util.ts +7 -1
- package/package.json +1 -1
- package/test/compose-cmd.test.ts +120 -0
- package/test/init.test.ts +1 -1
- package/test/issues.cli.test.ts +230 -1
- package/test/mcp-server.test.ts +69 -0
- package/test/reports.test.ts +3 -3
- package/test/storage.test.ts +761 -0
package/test/issues.cli.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
-
import { mkdtempSync } from "fs";
|
|
3
|
+
import { mkdtempSync, writeFileSync, existsSync, readFileSync } from "fs";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
5
|
|
|
6
6
|
function runCli(args: string[], env: Record<string, string> = {}) {
|
|
@@ -536,3 +536,232 @@ describe("CLI action items commands", () => {
|
|
|
536
536
|
});
|
|
537
537
|
});
|
|
538
538
|
|
|
539
|
+
async function startFakeStorageServer() {
|
|
540
|
+
const requests: Array<{
|
|
541
|
+
method: string;
|
|
542
|
+
pathname: string;
|
|
543
|
+
headers: Record<string, string>;
|
|
544
|
+
}> = [];
|
|
545
|
+
|
|
546
|
+
const server = Bun.serve({
|
|
547
|
+
hostname: "127.0.0.1",
|
|
548
|
+
port: 0,
|
|
549
|
+
async fetch(req) {
|
|
550
|
+
const url = new URL(req.url);
|
|
551
|
+
const headers: Record<string, string> = {};
|
|
552
|
+
for (const [k, v] of req.headers.entries()) headers[k.toLowerCase()] = v;
|
|
553
|
+
|
|
554
|
+
// Consume body to avoid warnings
|
|
555
|
+
await req.arrayBuffer().catch(() => {});
|
|
556
|
+
|
|
557
|
+
requests.push({
|
|
558
|
+
method: req.method,
|
|
559
|
+
pathname: url.pathname,
|
|
560
|
+
headers,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Upload endpoint
|
|
564
|
+
if (req.method === "POST" && url.pathname === "/upload") {
|
|
565
|
+
if (!headers["access-token"]) {
|
|
566
|
+
return new Response(
|
|
567
|
+
JSON.stringify({ code: "INVALID_API_TOKEN", message: "Missing token" }),
|
|
568
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return new Response(
|
|
573
|
+
JSON.stringify({
|
|
574
|
+
success: true,
|
|
575
|
+
url: "/files/123/1707500000000_test-uuid.png",
|
|
576
|
+
metadata: {
|
|
577
|
+
originalName: "test-file.png",
|
|
578
|
+
size: 16,
|
|
579
|
+
mimeType: "image/png",
|
|
580
|
+
uploadedAt: "2025-02-09T12:00:00.000Z",
|
|
581
|
+
duration: 50,
|
|
582
|
+
},
|
|
583
|
+
requestId: "req-test-123",
|
|
584
|
+
}),
|
|
585
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Download endpoint
|
|
590
|
+
if (req.method === "GET" && url.pathname.startsWith("/files/")) {
|
|
591
|
+
if (!headers["access-token"]) {
|
|
592
|
+
return new Response(
|
|
593
|
+
JSON.stringify({ code: "INVALID_API_TOKEN", message: "Missing token" }),
|
|
594
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return new Response(Buffer.from("fake-file-content"), {
|
|
599
|
+
status: 200,
|
|
600
|
+
headers: { "Content-Type": "image/png" },
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return new Response("not found", { status: 404 });
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const storageBaseUrl = `http://${server.hostname}:${server.port}`;
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
storageBaseUrl,
|
|
612
|
+
requests,
|
|
613
|
+
stop: () => server.stop(true),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
describe("CLI issues files commands", () => {
|
|
618
|
+
test("issues files upload fails fast when API key is missing", () => {
|
|
619
|
+
const r = runCli(["issues", "files", "upload", "/tmp/test.png"], isolatedEnv());
|
|
620
|
+
expect(r.status).toBe(1);
|
|
621
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("issues files upload fails when file does not exist", () => {
|
|
625
|
+
const r = runCli(
|
|
626
|
+
["issues", "files", "upload", "/tmp/nonexistent-file-99999.png"],
|
|
627
|
+
isolatedEnv({ PGAI_API_KEY: "test-key" })
|
|
628
|
+
);
|
|
629
|
+
expect(r.status).toBe(1);
|
|
630
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("File not found");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("issues files upload succeeds and shows URL and markdown", async () => {
|
|
634
|
+
const storage = await startFakeStorageServer();
|
|
635
|
+
const tmpFile = resolve(mkdtempSync(resolve(tmpdir(), "upload-test-")), "screenshot.png");
|
|
636
|
+
writeFileSync(tmpFile, "fake-png-content");
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
const r = await runCliAsync(
|
|
640
|
+
["issues", "files", "upload", tmpFile],
|
|
641
|
+
isolatedEnv({
|
|
642
|
+
PGAI_API_KEY: "test-key",
|
|
643
|
+
PGAI_STORAGE_BASE_URL: storage.storageBaseUrl,
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
expect(r.status).toBe(0);
|
|
648
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
649
|
+
expect(out).toContain("URL:");
|
|
650
|
+
expect(out).toContain("/files/123/");
|
|
651
|
+
expect(out).toContain("Markdown:");
|
|
652
|
+
expect(out).toContain("![");
|
|
653
|
+
|
|
654
|
+
const uploadReq = storage.requests.find((x) => x.pathname === "/upload");
|
|
655
|
+
expect(uploadReq).toBeTruthy();
|
|
656
|
+
expect(uploadReq!.headers["access-token"]).toBe("test-key");
|
|
657
|
+
expect(uploadReq!.method).toBe("POST");
|
|
658
|
+
} finally {
|
|
659
|
+
storage.stop();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("issues files upload --json returns structured JSON", async () => {
|
|
664
|
+
const storage = await startFakeStorageServer();
|
|
665
|
+
const tmpFile = resolve(mkdtempSync(resolve(tmpdir(), "upload-json-test-")), "data.txt");
|
|
666
|
+
writeFileSync(tmpFile, "test-content");
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const r = await runCliAsync(
|
|
670
|
+
["issues", "files", "upload", tmpFile, "--json"],
|
|
671
|
+
isolatedEnv({
|
|
672
|
+
PGAI_API_KEY: "test-key",
|
|
673
|
+
PGAI_STORAGE_BASE_URL: storage.storageBaseUrl,
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
expect(r.status).toBe(0);
|
|
678
|
+
const out = JSON.parse(r.stdout.trim());
|
|
679
|
+
expect(out.success).toBe(true);
|
|
680
|
+
expect(out.url).toContain("/files/");
|
|
681
|
+
expect(out.metadata.originalName).toBe("test-file.png");
|
|
682
|
+
expect(out.metadata.mimeType).toBe("image/png");
|
|
683
|
+
} finally {
|
|
684
|
+
storage.stop();
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("issues files download fails fast when API key is missing", () => {
|
|
689
|
+
const r = runCli(["issues", "files", "download", "/files/123/test.png"], isolatedEnv());
|
|
690
|
+
expect(r.status).toBe(1);
|
|
691
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("issues files download succeeds and saves file", async () => {
|
|
695
|
+
const storage = await startFakeStorageServer();
|
|
696
|
+
const tmpOutDir = mkdtempSync(resolve(tmpdir(), "download-test-"));
|
|
697
|
+
const outputPath = resolve(tmpOutDir, "downloaded.png");
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
const r = await runCliAsync(
|
|
701
|
+
["issues", "files", "download", "/files/123/image.png", "-o", outputPath],
|
|
702
|
+
isolatedEnv({
|
|
703
|
+
PGAI_API_KEY: "test-key",
|
|
704
|
+
PGAI_STORAGE_BASE_URL: storage.storageBaseUrl,
|
|
705
|
+
})
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
expect(r.status).toBe(0);
|
|
709
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
710
|
+
expect(out).toContain("Saved:");
|
|
711
|
+
expect(out).not.toContain("Size:");
|
|
712
|
+
expect(out).not.toContain("Type:");
|
|
713
|
+
|
|
714
|
+
expect(existsSync(outputPath)).toBe(true);
|
|
715
|
+
expect(readFileSync(outputPath).toString()).toBe("fake-file-content");
|
|
716
|
+
|
|
717
|
+
const downloadReq = storage.requests.find((x) => x.pathname.startsWith("/files/"));
|
|
718
|
+
expect(downloadReq).toBeTruthy();
|
|
719
|
+
expect(downloadReq!.headers["access-token"]).toBe("test-key");
|
|
720
|
+
expect(downloadReq!.method).toBe("GET");
|
|
721
|
+
} finally {
|
|
722
|
+
storage.stop();
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("issues help shows files subcommand", () => {
|
|
727
|
+
const r = runCli(["issues", "--help"], isolatedEnv());
|
|
728
|
+
expect(r.status).toBe(0);
|
|
729
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
730
|
+
expect(out).toContain("files");
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("issues files help shows upload and download", () => {
|
|
734
|
+
const r = runCli(["issues", "files", "--help"], isolatedEnv());
|
|
735
|
+
expect(r.status).toBe(0);
|
|
736
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
737
|
+
expect(out).toContain("upload");
|
|
738
|
+
expect(out).toContain("download");
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
describe("CLI set-storage-url command", () => {
|
|
743
|
+
test("saves valid URL to config", () => {
|
|
744
|
+
const env = isolatedEnv();
|
|
745
|
+
const r = runCli(["set-storage-url", "https://v2.postgres.ai/storage"], env);
|
|
746
|
+
expect(r.status).toBe(0);
|
|
747
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("Storage URL saved: https://v2.postgres.ai/storage");
|
|
748
|
+
|
|
749
|
+
// Verify persisted in config
|
|
750
|
+
const cfgPath = resolve(env.XDG_CONFIG_HOME, "postgresai", "config.json");
|
|
751
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
752
|
+
expect(cfg.storageBaseUrl).toBe("https://v2.postgres.ai/storage");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test("normalizes trailing slash", () => {
|
|
756
|
+
const env = isolatedEnv();
|
|
757
|
+
const r = runCli(["set-storage-url", "https://example.com/storage/"], env);
|
|
758
|
+
expect(r.status).toBe(0);
|
|
759
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("Storage URL saved: https://example.com/storage");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("rejects invalid URL", () => {
|
|
763
|
+
const r = runCli(["set-storage-url", "not-a-url"], isolatedEnv());
|
|
764
|
+
expect(r.status).toBe(1);
|
|
765
|
+
expect(`${r.stdout}\n${r.stderr}`).toContain("invalid URL");
|
|
766
|
+
});
|
|
767
|
+
});
|