Skip to content

Commit 42749bf

Browse files
committed
fix(pi-model-modes): reject prototype property names
1 parent 3e6f100 commit 42749bf

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

‎extensions/pi-model-modes/src/mode-picker.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const THINKING_UNSET_LABEL = "(don't change)";
3939
function isReservedModeName(name: string): boolean {
4040
return (
4141
name === CUSTOM_MODE_NAME ||
42+
name === "__proto__" ||
43+
name === "constructor" ||
44+
name === "prototype" ||
4245
name === MODE_UI_CONFIGURE ||
4346
name === MODE_UI_ADD ||
4447
name === MODE_UI_BACK

‎extensions/pi-model-modes/test/mode-controller.test.ts‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import assert from "node:assert/strict";
2-
import { test } from "vitest";
2+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import {
6+
ExtensionRunner,
7+
ModelRegistry,
8+
ModelRuntime,
9+
SessionManager,
10+
discoverAndLoadExtensions,
11+
} from "@earendil-works/pi-coding-agent";
12+
import { test, vi } from "vitest";
313

414
import { ModeController } from "../src/mode-controller.ts";
15+
import { ModePicker } from "../src/mode-picker.ts";
516

617
function createController(): ModeController {
718
return new ModeController({
@@ -27,6 +38,61 @@ test("mode controllers own independent runtime state", () => {
2738
assert.equal(second.thinkingLevelStatusEnabled, false);
2839
});
2940

41+
test.each(["__proto__", "constructor", "prototype"])(
42+
"renaming a mode to %s is rejected without losing the saved mode",
43+
async (name) => {
44+
const root = await mkdtemp(join(tmpdir(), "pi-model-modes-rename-"));
45+
vi.stubEnv("PI_CODING_AGENT_DIR", join(root, "agent"));
46+
const configPath = join(root, "agent", "extension-settings", "pi-model-modes.json");
47+
const original = JSON.stringify({
48+
currentMode: "review",
49+
modes: { review: { provider: "local", modelId: "small" } },
50+
});
51+
try {
52+
await mkdir(join(configPath, ".."), { recursive: true });
53+
await writeFile(configPath, original);
54+
const loaded = await discoverAndLoadExtensions([], root, join(root, "agent"));
55+
const models = await ModelRuntime.create({
56+
authPath: join(root, "auth.json"),
57+
modelsPath: null,
58+
refreshOnCreate: false,
59+
});
60+
const runner = new ExtensionRunner(
61+
loaded.extensions,
62+
loaded.runtime,
63+
root,
64+
SessionManager.inMemory(root),
65+
new ModelRegistry(models),
66+
);
67+
const choices = ["Configure modes…", "review", "Change name"];
68+
const inputs = [name];
69+
const notifications: string[] = [];
70+
runner.setUIContext(
71+
{
72+
...runner.getUIContext(),
73+
select: async () => choices.shift(),
74+
input: async () => inputs.shift(),
75+
notify: (message) => {
76+
notifications.push(message);
77+
},
78+
},
79+
"rpc",
80+
);
81+
const controller = createController();
82+
83+
await new ModePicker(controller).select(runner.createContext());
84+
assert.equal(await readFile(configPath, "utf8"), original);
85+
assert.deepEqual(controller.modes.modes, {
86+
review: { provider: "local", modelId: "small" },
87+
});
88+
assert.deepEqual(notifications, [`Mode name "${name}" is reserved`]);
89+
} finally {
90+
vi.unstubAllEnvs();
91+
await rm(root, { recursive: true, force: true });
92+
}
93+
},
94+
);
95+
3096
test("mode controller derives settings reads from the explicit extension context", () => {
3197
const controller = createController();
3298
assert.deepEqual(

0 commit comments

Comments
 (0)