feat(coding-agent): support renamed self-update package
This commit is contained in:
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "no
|
||||
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 { ENV_AGENT_DIR, PACKAGE_NAME } from "../src/config.js";
|
||||
import { main } from "../src/main.js";
|
||||
|
||||
describe("package commands", () => {
|
||||
@@ -36,6 +36,7 @@ describe("package commands", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
process.chdir(originalCwd);
|
||||
process.exitCode = originalExitCode;
|
||||
if (originalAgentDir === undefined) {
|
||||
@@ -128,7 +129,7 @@ describe("package commands", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses global npmCommand for self updates", async () => {
|
||||
it("uses global npmCommand and current package name for forced self updates without checking the api", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const projectPrefix = join(tempDir, "project-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
@@ -156,6 +157,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
@@ -165,8 +168,10 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
||||
expect(recordedArgs).toContain(globalPrefix);
|
||||
expect(recordedArgs).toContain(PACKAGE_NAME);
|
||||
expect(recordedArgs).not.toContain(projectPrefix);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
@@ -174,6 +179,155 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the current package name when the update check omits packageName", async () => {
|
||||
const globalPrefix = join(tempDir, "global-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 });
|
||||
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),
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
Object.defineProperty(process, "execPath", {
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const fetchMock = vi.fn(async () => Response.json({ version: "0.73.1" }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(main(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
||||
expect(recordedArgs).toContain(PACKAGE_NAME);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("installs the active package name from the update check during self-update", async () => {
|
||||
const globalPrefix = join(tempDir, "global-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 });
|
||||
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 {
|
||||
const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[];
|
||||
records.push(args);
|
||||
fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records));
|
||||
}
|
||||
`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2),
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
Object.defineProperty(process, "execPath", {
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })),
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(main(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][];
|
||||
expect(recordedCalls).toEqual([
|
||||
expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]),
|
||||
expect.arrayContaining(["install", "-g", activePackageName]),
|
||||
]);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("fails self-update when renamed npm package installation fails", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
const fakeNpmPath = join(tempDir, "fake-npm-fail.cjs");
|
||||
const recordPath = join(tempDir, "self-update-fail.json");
|
||||
mkdirSync(selfPackageDir, { 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"));
|
||||
process.exit(0);
|
||||
}
|
||||
const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[];
|
||||
records.push(args);
|
||||
fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records));
|
||||
if(args.includes("install")) process.exit(23);
|
||||
`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2),
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
Object.defineProperty(process, "execPath", {
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })),
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(main(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(process.exitCode).toBe(1);
|
||||
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
expect(stdout).not.toContain(`Updated pi`);
|
||||
expect(stderr).toContain("exited with code 23");
|
||||
const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][];
|
||||
expect(recordedCalls).toEqual([
|
||||
expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]),
|
||||
expect.arrayContaining(["install", "-g", activePackageName]),
|
||||
]);
|
||||
} 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));
|
||||
|
||||
Reference in New Issue
Block a user