mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
ade08de14c
Fixes #3942 Fixes #3980 Fixes #3922
201 lines
7.6 KiB
TypeScript
201 lines
7.6 KiB
TypeScript
import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { ENV_AGENT_DIR } from "../src/config.js";
|
|
import { main } from "../src/main.js";
|
|
|
|
describe("package commands", () => {
|
|
let tempDir: string;
|
|
let agentDir: string;
|
|
let projectDir: string;
|
|
let packageDir: string;
|
|
let originalCwd: string;
|
|
let originalAgentDir: string | undefined;
|
|
let originalPiPackageDir: string | undefined;
|
|
let originalExitCode: typeof process.exitCode;
|
|
let originalExecPath: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
agentDir = join(tempDir, "agent");
|
|
projectDir = join(tempDir, "project");
|
|
packageDir = join(tempDir, "local-package");
|
|
mkdirSync(agentDir, { recursive: true });
|
|
mkdirSync(projectDir, { recursive: true });
|
|
mkdirSync(packageDir, { recursive: true });
|
|
|
|
originalCwd = process.cwd();
|
|
originalAgentDir = process.env[ENV_AGENT_DIR];
|
|
originalPiPackageDir = process.env.PI_PACKAGE_DIR;
|
|
originalExitCode = process.exitCode;
|
|
originalExecPath = process.execPath;
|
|
process.exitCode = undefined;
|
|
process.env[ENV_AGENT_DIR] = agentDir;
|
|
process.chdir(projectDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(originalCwd);
|
|
process.exitCode = originalExitCode;
|
|
if (originalAgentDir === undefined) {
|
|
delete process.env[ENV_AGENT_DIR];
|
|
} else {
|
|
process.env[ENV_AGENT_DIR] = originalAgentDir;
|
|
}
|
|
if (originalPiPackageDir === undefined) {
|
|
delete process.env.PI_PACKAGE_DIR;
|
|
} else {
|
|
process.env.PI_PACKAGE_DIR = originalPiPackageDir;
|
|
}
|
|
Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("should persist global relative local package paths relative to settings.json", async () => {
|
|
const relativePkgDir = join(projectDir, "packages", "local-package");
|
|
mkdirSync(relativePkgDir, { recursive: true });
|
|
|
|
await main(["install", "./packages/local-package"]);
|
|
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
|
expect(settings.packages?.length).toBe(1);
|
|
const stored = settings.packages?.[0] ?? "";
|
|
const resolvedFromSettings = realpathSync(join(agentDir, stored));
|
|
expect(resolvedFromSettings).toBe(realpathSync(relativePkgDir));
|
|
});
|
|
|
|
it("should remove local packages using a path with a trailing slash", async () => {
|
|
await main(["install", `${packageDir}/`]);
|
|
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
const installedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
|
expect(installedSettings.packages?.length).toBe(1);
|
|
|
|
await main(["remove", `${packageDir}/`]);
|
|
|
|
const removedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
|
expect(removedSettings.packages ?? []).toHaveLength(0);
|
|
});
|
|
|
|
it("shows install subcommand help", async () => {
|
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
try {
|
|
await expect(main(["install", "--help"])).resolves.toBeUndefined();
|
|
|
|
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
|
expect(stdout).toContain("Usage:");
|
|
expect(stdout).toContain("pi install <source> [-l]");
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBeUndefined();
|
|
} finally {
|
|
logSpy.mockRestore();
|
|
errorSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("shows a friendly error for unknown install options", async () => {
|
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
try {
|
|
await expect(main(["install", "--unknown"])).resolves.toBeUndefined();
|
|
|
|
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
|
expect(stderr).toContain('Unknown option --unknown for "install".');
|
|
expect(stderr).toContain('Use "pi --help" or "pi install <source> [-l]".');
|
|
expect(process.exitCode).toBe(1);
|
|
} finally {
|
|
errorSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("shows a friendly error for missing install source", async () => {
|
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
try {
|
|
await expect(main(["install"])).resolves.toBeUndefined();
|
|
|
|
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
|
expect(stderr).toContain("Missing install source.");
|
|
expect(stderr).toContain("Usage: pi install <source> [-l]");
|
|
expect(stderr).not.toContain("at ");
|
|
expect(process.exitCode).toBe(1);
|
|
} finally {
|
|
errorSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("uses global npmCommand for self updates", async () => {
|
|
const globalPrefix = join(tempDir, "global-prefix");
|
|
const projectPrefix = join(tempDir, "project-prefix");
|
|
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
|
const fakeNpmPath = join(tempDir, "fake-npm.cjs");
|
|
const recordPath = join(tempDir, "self-update.json");
|
|
mkdirSync(selfPackageDir, { recursive: true });
|
|
mkdirSync(join(projectDir, ".pi"), { recursive: true });
|
|
writeFileSync(
|
|
fakeNpmPath,
|
|
`const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];
|
|
if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules"));
|
|
else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
|
`,
|
|
);
|
|
writeFileSync(
|
|
join(agentDir, "settings.json"),
|
|
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2),
|
|
);
|
|
writeFileSync(
|
|
join(projectDir, ".pi", "settings.json"),
|
|
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", projectPrefix] }, null, 2),
|
|
);
|
|
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
|
Object.defineProperty(process, "execPath", {
|
|
value: join(selfPackageDir, "dist", "cli.js"),
|
|
configurable: true,
|
|
});
|
|
|
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
try {
|
|
await expect(main(["update", "--self", "--force"])).resolves.toBeUndefined();
|
|
|
|
expect(process.exitCode).toBeUndefined();
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
|
expect(recordedArgs).toContain(globalPrefix);
|
|
expect(recordedArgs).not.toContain(projectPrefix);
|
|
} finally {
|
|
logSpy.mockRestore();
|
|
errorSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("suggests the configured source when update input omits the npm prefix", async () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ packages: ["npm:pi-formatter"] }, null, 2));
|
|
|
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
|
|
try {
|
|
await expect(main(["update", "pi-formatter"])).resolves.toBeUndefined();
|
|
|
|
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
|
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
|
expect(stderr).toContain("Did you mean npm:pi-formatter?");
|
|
expect(stdout).not.toContain("Updated pi-formatter");
|
|
expect(process.exitCode).toBe(1);
|
|
|
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] };
|
|
expect(settings.packages).toContain("npm:pi-formatter");
|
|
} finally {
|
|
errorSpy.mockRestore();
|
|
logSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|