@pi-archimedes/subagent 2.0.1 → 2.1.0
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/README.md +15 -9
- package/package.json +2 -2
- package/src/agent-manager.ts +45 -24
- package/src/agents.test.ts +37 -0
- package/src/agents.ts +7 -2
- package/src/compact.test.ts +437 -0
- package/src/cost.test.ts +62 -0
- package/src/frontmatter-io.test.ts +119 -0
- package/src/local-config.test.ts +96 -0
- package/src/local-config.ts +50 -6
- package/src/save-agent.test.ts +317 -1
package/src/local-config.test.ts
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
readLocalConfig,
|
|
13
13
|
writeLocalModel,
|
|
14
14
|
deleteLocalModel,
|
|
15
|
+
writeLocalThinking,
|
|
16
|
+
deleteLocalThinking,
|
|
17
|
+
deleteLocalAgent,
|
|
15
18
|
} from "./local-config.js";
|
|
16
19
|
|
|
17
20
|
// Redirect getAgentDir() to a temp directory via PI_CODING_AGENT_DIR.
|
|
@@ -102,6 +105,99 @@ describe("local-config", () => {
|
|
|
102
105
|
});
|
|
103
106
|
});
|
|
104
107
|
|
|
108
|
+
describe("writeLocalThinking", () => {
|
|
109
|
+
it("creates file and writes thinking entry", () => {
|
|
110
|
+
writeLocalThinking("codex", "high");
|
|
111
|
+
expect(readLocalConfig()).toEqual({ codex: { thinking: "high" } });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("preserves a model on the same entry when writing thinking after model", () => {
|
|
115
|
+
writeLocalModel("codex", "o1");
|
|
116
|
+
writeLocalThinking("codex", "high");
|
|
117
|
+
expect(readLocalConfig()).toEqual({
|
|
118
|
+
codex: { model: "o1", thinking: "high" },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("preserves other agent entries when updating one", () => {
|
|
123
|
+
writeLocalThinking("codex", "high");
|
|
124
|
+
writeLocalModel("claude", "claude-3.7");
|
|
125
|
+
expect(readLocalConfig()).toEqual({
|
|
126
|
+
codex: { thinking: "high" },
|
|
127
|
+
claude: { model: "claude-3.7" },
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("deleteLocalThinking", () => {
|
|
133
|
+
it("removes only the thinking field, keeping model on the same entry", () => {
|
|
134
|
+
writeLocalModel("codex", "o1");
|
|
135
|
+
writeLocalThinking("codex", "high");
|
|
136
|
+
deleteLocalThinking("codex");
|
|
137
|
+
expect(readLocalConfig()).toEqual({ codex: { model: "o1" } });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("removes the whole entry when thinking was the only field", () => {
|
|
141
|
+
writeLocalThinking("codex", "high");
|
|
142
|
+
deleteLocalThinking("codex");
|
|
143
|
+
expect(readLocalConfig()).toEqual({});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("is a no-op when agent does not exist", () => {
|
|
147
|
+
writeLocalModel("codex", "o1");
|
|
148
|
+
deleteLocalThinking("nonexistent");
|
|
149
|
+
expect(readLocalConfig()).toEqual({ codex: { model: "o1" } });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("does not create file when deleting absent agent on clean install", () => {
|
|
153
|
+
// No agents.local.json exists yet. Deleting an absent agent should
|
|
154
|
+
// be a true no-op — it must NOT materialise an empty file on disk.
|
|
155
|
+
deleteLocalThinking("nonexistent");
|
|
156
|
+
expect(existsSync(join(testDir, "agents.local.json"))).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("deleteLocalModel field-level semantics", () => {
|
|
161
|
+
it("leaves thinking intact on the same entry", () => {
|
|
162
|
+
writeLocalModel("codex", "o1");
|
|
163
|
+
writeLocalThinking("codex", "high");
|
|
164
|
+
deleteLocalModel("codex");
|
|
165
|
+
expect(readLocalConfig()).toEqual({ codex: { thinking: "high" } });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("deleteLocalAgent", () => {
|
|
170
|
+
it("removes the entire entry including all fields", () => {
|
|
171
|
+
writeLocalModel("codex", "o1");
|
|
172
|
+
writeLocalThinking("codex", "high");
|
|
173
|
+
deleteLocalAgent("codex");
|
|
174
|
+
expect(readLocalConfig()).toEqual({});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("preserves other agent entries", () => {
|
|
178
|
+
writeLocalModel("codex", "o1");
|
|
179
|
+
writeLocalThinking("codex", "high");
|
|
180
|
+
writeLocalModel("claude", "claude-3.7");
|
|
181
|
+
deleteLocalAgent("codex");
|
|
182
|
+
expect(readLocalConfig()).toEqual({
|
|
183
|
+
claude: { model: "claude-3.7" },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("is a no-op when agent does not exist", () => {
|
|
188
|
+
writeLocalModel("codex", "o1");
|
|
189
|
+
deleteLocalAgent("nonexistent");
|
|
190
|
+
expect(readLocalConfig()).toEqual({ codex: { model: "o1" } });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("does not create file when deleting absent agent on clean install", () => {
|
|
194
|
+
// No agents.local.json exists yet. Deleting an absent agent should
|
|
195
|
+
// be a true no-op — it must NOT materialise an empty file on disk.
|
|
196
|
+
deleteLocalAgent("nonexistent");
|
|
197
|
+
expect(existsSync(join(testDir, "agents.local.json"))).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
105
201
|
it("leaves no .tmp file behind after successful write", () => {
|
|
106
202
|
writeLocalModel("codex", "o1");
|
|
107
203
|
const files = readdirSync(testDir);
|
package/src/local-config.ts
CHANGED
|
@@ -8,8 +8,11 @@ import {
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
|
|
11
|
+
/** Fields that can be overridden per agent in agents.local.json. */
|
|
12
|
+
export type LocalField = "model" | "thinking";
|
|
13
|
+
|
|
11
14
|
/** Per-agent local overrides stored in agents.local.json */
|
|
12
|
-
export type LocalConfig = Record<string,
|
|
15
|
+
export type LocalConfig = Record<string, Partial<Record<LocalField, string>>>;
|
|
13
16
|
|
|
14
17
|
/** Returns the path to agents.local.json inside the agent directory. */
|
|
15
18
|
export function getLocalConfigPath(): string {
|
|
@@ -53,21 +56,62 @@ function writeConfigAtomic(config: LocalConfig): void {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
/** Set the local
|
|
57
|
-
export function
|
|
59
|
+
/** Set the local override for a field on a given agent, preserving existing entries. */
|
|
60
|
+
export function writeLocalField(agentName: string, field: LocalField, value: string): void {
|
|
58
61
|
const config = readLocalConfig();
|
|
59
|
-
config[agentName] = { ...config[agentName],
|
|
62
|
+
config[agentName] = { ...config[agentName], [field]: value };
|
|
60
63
|
writeConfigAtomic(config);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Delete the local override for a field on a given agent (no-op if absent).
|
|
68
|
+
* Removes ONLY the given field — other fields in the same entry are
|
|
69
|
+
* preserved. If no fields remain, the entry itself is removed.
|
|
70
|
+
*
|
|
71
|
+
* Note: deletion is field-level. An entry lacking the given field is left
|
|
72
|
+
* untouched (a degenerate empty entry `{ codex: {} }` is not removed),
|
|
73
|
+
* unlike the old entry-level delete. Entries are always written with at
|
|
74
|
+
* least one field in practice, so this is acceptable.
|
|
75
|
+
*/
|
|
76
|
+
export function deleteLocalField(agentName: string, field: LocalField): void {
|
|
77
|
+
const config = readLocalConfig();
|
|
78
|
+
const entry = config[agentName];
|
|
79
|
+
if (!entry || !(field in entry)) return;
|
|
80
|
+
delete entry[field];
|
|
81
|
+
if (Object.keys(entry).length === 0) {
|
|
82
|
+
delete config[agentName];
|
|
83
|
+
}
|
|
84
|
+
writeConfigAtomic(config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Delete the entire local override entry for an agent (no-op if absent). Used on rename. */
|
|
88
|
+
export function deleteLocalAgent(agentName: string): void {
|
|
65
89
|
const config = readLocalConfig();
|
|
66
90
|
if (!(agentName in config)) return;
|
|
67
91
|
delete config[agentName];
|
|
68
92
|
writeConfigAtomic(config);
|
|
69
93
|
}
|
|
70
94
|
|
|
95
|
+
/** Set the local model override for a given agent, preserving existing entries. */
|
|
96
|
+
export function writeLocalModel(agentName: string, model: string): void {
|
|
97
|
+
writeLocalField(agentName, "model", model);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Delete the local model override for a given agent (no-op if absent). */
|
|
101
|
+
export function deleteLocalModel(agentName: string): void {
|
|
102
|
+
deleteLocalField(agentName, "model");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Set the local thinking-level override for a given agent, preserving existing entries. */
|
|
106
|
+
export function writeLocalThinking(agentName: string, thinking: string): void {
|
|
107
|
+
writeLocalField(agentName, "thinking", thinking);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Delete the local thinking-level override for a given agent (no-op if absent). */
|
|
111
|
+
export function deleteLocalThinking(agentName: string): void {
|
|
112
|
+
deleteLocalField(agentName, "thinking");
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
/** Write the full local config atomically (backup+restore safe). */
|
|
72
116
|
export function setLocalConfig(config: LocalConfig): void {
|
|
73
117
|
writeConfigAtomic(config);
|
package/src/save-agent.test.ts
CHANGED
|
@@ -4,7 +4,12 @@ import { join } from "node:path";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { saveAgent } from "./agent-manager.js";
|
|
6
6
|
import { discoverAgentsAll } from "./agents.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
writeLocalModel,
|
|
9
|
+
writeLocalThinking,
|
|
10
|
+
deleteLocalThinking,
|
|
11
|
+
deleteLocalAgent,
|
|
12
|
+
} from "./local-config.js";
|
|
8
13
|
|
|
9
14
|
// Mock writeLocalModel to always throw (simulating a JSON store write
|
|
10
15
|
// failure). deleteLocalModel is a no-op since it's never reached in
|
|
@@ -19,6 +24,11 @@ vi.mock("./local-config.js", async (importOriginal) => {
|
|
|
19
24
|
throw new Error("JSON write failed");
|
|
20
25
|
}),
|
|
21
26
|
deleteLocalModel: vi.fn(),
|
|
27
|
+
writeLocalThinking: vi.fn(() => {
|
|
28
|
+
throw new Error("JSON thinking write failed");
|
|
29
|
+
}),
|
|
30
|
+
deleteLocalThinking: vi.fn(),
|
|
31
|
+
deleteLocalAgent: vi.fn(),
|
|
22
32
|
};
|
|
23
33
|
});
|
|
24
34
|
|
|
@@ -481,3 +491,309 @@ describe("saveAgent cleanup of model-less new files", () => {
|
|
|
481
491
|
});
|
|
482
492
|
});
|
|
483
493
|
|
|
494
|
+
describe("saveAgent thinking JSON round-trip", () => {
|
|
495
|
+
beforeEach(() => {
|
|
496
|
+
mkdirSync(testDir, { recursive: true });
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
afterEach(() => {
|
|
500
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("restores thinking to .md when the JSON thinking write fails", () => {
|
|
504
|
+
// writeLocalModel mocked to succeed for this call only (default throws),
|
|
505
|
+
// while writeLocalThinking keeps its default throwing implementation,
|
|
506
|
+
// simulating a failed JSON thinking write.
|
|
507
|
+
vi.mocked(writeLocalModel).mockImplementationOnce(() => {});
|
|
508
|
+
|
|
509
|
+
const state = {
|
|
510
|
+
editAgent: {
|
|
511
|
+
name: "codex",
|
|
512
|
+
description: "test agent",
|
|
513
|
+
systemPrompt: "hello world",
|
|
514
|
+
source: "user",
|
|
515
|
+
filePath: join(testDir, "codex.md"),
|
|
516
|
+
model: "openai/gpt-4o",
|
|
517
|
+
thinking: "high",
|
|
518
|
+
},
|
|
519
|
+
editOriginal: {
|
|
520
|
+
name: "codex",
|
|
521
|
+
description: "test agent",
|
|
522
|
+
systemPrompt: "hello world",
|
|
523
|
+
source: "user",
|
|
524
|
+
filePath: join(testDir, "codex.md"),
|
|
525
|
+
},
|
|
526
|
+
agents: [],
|
|
527
|
+
globalDir: null,
|
|
528
|
+
userDir: testDir,
|
|
529
|
+
projectDir: null,
|
|
530
|
+
editError: null,
|
|
531
|
+
editDirty: false,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
saveAgent(state as any, () => {});
|
|
535
|
+
|
|
536
|
+
// The .md should contain BOTH fields as rollback fallback.
|
|
537
|
+
const mdContent = readFileSync(join(testDir, "codex.md"), "utf-8");
|
|
538
|
+
expect(mdContent).toContain("model: openai/gpt-4o");
|
|
539
|
+
expect(mdContent).toContain("thinking: high");
|
|
540
|
+
|
|
541
|
+
// editError should be set.
|
|
542
|
+
expect(state.editError).toBeTruthy();
|
|
543
|
+
|
|
544
|
+
// The live edit object should retain both values for a safe retry.
|
|
545
|
+
expect(state.editAgent!.model).toBe("openai/gpt-4o");
|
|
546
|
+
expect(state.editAgent!.thinking).toBe("high");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("retains thinking for retry when re-discovery fails", () => {
|
|
550
|
+
// Both JSON writes succeed for this call only, then re-discovery throws.
|
|
551
|
+
vi.mocked(writeLocalModel).mockImplementationOnce(() => {});
|
|
552
|
+
vi.mocked(writeLocalThinking).mockImplementationOnce(() => {});
|
|
553
|
+
vi.mocked(discoverAgentsAll).mockImplementationOnce(() => {
|
|
554
|
+
throw new Error("discovery failed");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const state = {
|
|
558
|
+
editAgent: {
|
|
559
|
+
name: "codex",
|
|
560
|
+
description: "test agent",
|
|
561
|
+
systemPrompt: "hello world",
|
|
562
|
+
source: "user",
|
|
563
|
+
filePath: join(testDir, "codex.md"),
|
|
564
|
+
model: "openai/gpt-4o",
|
|
565
|
+
thinking: "high",
|
|
566
|
+
},
|
|
567
|
+
editOriginal: {
|
|
568
|
+
name: "codex",
|
|
569
|
+
description: "test agent",
|
|
570
|
+
systemPrompt: "hello world",
|
|
571
|
+
source: "user",
|
|
572
|
+
filePath: join(testDir, "codex.md"),
|
|
573
|
+
},
|
|
574
|
+
agents: [],
|
|
575
|
+
globalDir: null,
|
|
576
|
+
userDir: testDir,
|
|
577
|
+
projectDir: null,
|
|
578
|
+
editError: null,
|
|
579
|
+
editDirty: false,
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
saveAgent(state as any, () => {});
|
|
583
|
+
|
|
584
|
+
// The live edit object should still have the thinking — delete only
|
|
585
|
+
// runs after requestRender() which never executed.
|
|
586
|
+
expect(state.editAgent!.thinking).toBe("high");
|
|
587
|
+
|
|
588
|
+
// editError should contain the discovery failure message.
|
|
589
|
+
expect(state.editError).toContain("discovery failed");
|
|
590
|
+
|
|
591
|
+
// The rolled-back .md should contain the thinking.
|
|
592
|
+
const mdContent = readFileSync(join(testDir, "codex.md"), "utf-8");
|
|
593
|
+
expect(mdContent).toContain("thinking: high");
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("clearing thinking on save removes only the thinking field from JSON", async () => {
|
|
597
|
+
// Pre-seed JSON with both fields; the save carries a model but NO
|
|
598
|
+
// thinking, so only the thinking field should be removed.
|
|
599
|
+
writeFileSync(
|
|
600
|
+
join(testDir, "agents.local.json"),
|
|
601
|
+
JSON.stringify({ codex: { model: "o1", thinking: "high" } }),
|
|
602
|
+
"utf-8",
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Delegate to the real implementations — the no-op mock default for
|
|
606
|
+
// deleteLocalThinking would leave thinking: "high" behind.
|
|
607
|
+
const actual = await vi.importActual<typeof import("./local-config.js")>(
|
|
608
|
+
"./local-config.js",
|
|
609
|
+
);
|
|
610
|
+
vi.mocked(writeLocalModel).mockImplementationOnce((name, value) =>
|
|
611
|
+
actual.writeLocalModel(name, value),
|
|
612
|
+
);
|
|
613
|
+
vi.mocked(deleteLocalThinking).mockImplementationOnce((name) =>
|
|
614
|
+
actual.deleteLocalThinking(name),
|
|
615
|
+
);
|
|
616
|
+
vi.mocked(discoverAgentsAll).mockImplementationOnce(() => ({
|
|
617
|
+
global: [],
|
|
618
|
+
user: [],
|
|
619
|
+
project: [],
|
|
620
|
+
globalDir: null,
|
|
621
|
+
userDir: testDir,
|
|
622
|
+
projectDir: null,
|
|
623
|
+
}));
|
|
624
|
+
|
|
625
|
+
const state = {
|
|
626
|
+
editAgent: {
|
|
627
|
+
name: "codex",
|
|
628
|
+
description: "test agent",
|
|
629
|
+
systemPrompt: "hello world",
|
|
630
|
+
source: "user",
|
|
631
|
+
filePath: join(testDir, "codex.md"),
|
|
632
|
+
model: "o1",
|
|
633
|
+
},
|
|
634
|
+
editOriginal: {
|
|
635
|
+
name: "codex",
|
|
636
|
+
description: "test agent",
|
|
637
|
+
systemPrompt: "hello world",
|
|
638
|
+
source: "user",
|
|
639
|
+
filePath: join(testDir, "codex.md"),
|
|
640
|
+
},
|
|
641
|
+
agents: [],
|
|
642
|
+
globalDir: null,
|
|
643
|
+
userDir: testDir,
|
|
644
|
+
projectDir: null,
|
|
645
|
+
editError: null,
|
|
646
|
+
editDirty: false,
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
saveAgent(state as any, () => {});
|
|
650
|
+
|
|
651
|
+
// Thinking removed, model preserved.
|
|
652
|
+
const config = JSON.parse(
|
|
653
|
+
readFileSync(join(testDir, "agents.local.json"), "utf-8"),
|
|
654
|
+
);
|
|
655
|
+
expect(config).toEqual({ codex: { model: "o1" } });
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("writes thinking to agents.local.json and strips it from .md on a successful save", async () => {
|
|
659
|
+
const actual = await vi.importActual<typeof import("./local-config.js")>(
|
|
660
|
+
"./local-config.js",
|
|
661
|
+
);
|
|
662
|
+
vi.mocked(writeLocalModel).mockImplementationOnce((name, value) =>
|
|
663
|
+
actual.writeLocalModel(name, value),
|
|
664
|
+
);
|
|
665
|
+
vi.mocked(writeLocalThinking).mockImplementationOnce((name, value) =>
|
|
666
|
+
actual.writeLocalThinking(name, value),
|
|
667
|
+
);
|
|
668
|
+
vi.mocked(deleteLocalAgent).mockImplementationOnce((name) =>
|
|
669
|
+
actual.deleteLocalAgent(name),
|
|
670
|
+
);
|
|
671
|
+
vi.mocked(discoverAgentsAll).mockImplementationOnce(() => ({
|
|
672
|
+
global: [],
|
|
673
|
+
user: [],
|
|
674
|
+
project: [],
|
|
675
|
+
globalDir: null,
|
|
676
|
+
userDir: testDir,
|
|
677
|
+
projectDir: null,
|
|
678
|
+
}));
|
|
679
|
+
|
|
680
|
+
const state = {
|
|
681
|
+
editAgent: {
|
|
682
|
+
name: "codex",
|
|
683
|
+
description: "test agent",
|
|
684
|
+
systemPrompt: "hello world",
|
|
685
|
+
source: "user",
|
|
686
|
+
filePath: join(testDir, "codex.md"),
|
|
687
|
+
model: "openai/gpt-4o",
|
|
688
|
+
thinking: "high",
|
|
689
|
+
},
|
|
690
|
+
editOriginal: {
|
|
691
|
+
name: "codex",
|
|
692
|
+
description: "test agent",
|
|
693
|
+
systemPrompt: "hello world",
|
|
694
|
+
source: "user",
|
|
695
|
+
filePath: join(testDir, "codex.md"),
|
|
696
|
+
},
|
|
697
|
+
agents: [],
|
|
698
|
+
globalDir: null,
|
|
699
|
+
userDir: testDir,
|
|
700
|
+
projectDir: null,
|
|
701
|
+
editError: null,
|
|
702
|
+
editDirty: false,
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
saveAgent(state as any, () => {});
|
|
706
|
+
|
|
707
|
+
// Both fields landed in agents.local.json.
|
|
708
|
+
const config = JSON.parse(
|
|
709
|
+
readFileSync(join(testDir, "agents.local.json"), "utf-8"),
|
|
710
|
+
);
|
|
711
|
+
expect(config).toEqual({
|
|
712
|
+
codex: { model: "openai/gpt-4o", thinking: "high" },
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// The .md has neither field in its frontmatter.
|
|
716
|
+
const mdContent = readFileSync(join(testDir, "codex.md"), "utf-8");
|
|
717
|
+
expect(mdContent).not.toContain("model:");
|
|
718
|
+
expect(mdContent).not.toContain("thinking:");
|
|
719
|
+
|
|
720
|
+
// Both fields stripped from the live edit object.
|
|
721
|
+
expect(state.editAgent!.model).toBeUndefined();
|
|
722
|
+
expect(state.editAgent!.thinking).toBeUndefined();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("migrates model and thinking JSON entries on rename", async () => {
|
|
726
|
+
// Pre-seed JSON under the OLD name with both fields.
|
|
727
|
+
writeFileSync(
|
|
728
|
+
join(testDir, "agents.local.json"),
|
|
729
|
+
JSON.stringify({ oldcodex: { model: "old-model", thinking: "high" } }),
|
|
730
|
+
"utf-8",
|
|
731
|
+
);
|
|
732
|
+
writeFileSync(
|
|
733
|
+
join(testDir, "oldcodex.md"),
|
|
734
|
+
"original rename content",
|
|
735
|
+
"utf-8",
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const actual = await vi.importActual<typeof import("./local-config.js")>(
|
|
739
|
+
"./local-config.js",
|
|
740
|
+
);
|
|
741
|
+
vi.mocked(writeLocalModel).mockImplementationOnce((name, value) =>
|
|
742
|
+
actual.writeLocalModel(name, value),
|
|
743
|
+
);
|
|
744
|
+
vi.mocked(writeLocalThinking).mockImplementationOnce((name, value) =>
|
|
745
|
+
actual.writeLocalThinking(name, value),
|
|
746
|
+
);
|
|
747
|
+
vi.mocked(deleteLocalAgent).mockImplementationOnce((name) =>
|
|
748
|
+
actual.deleteLocalAgent(name),
|
|
749
|
+
);
|
|
750
|
+
vi.mocked(discoverAgentsAll).mockImplementationOnce(() => ({
|
|
751
|
+
global: [],
|
|
752
|
+
user: [],
|
|
753
|
+
project: [],
|
|
754
|
+
globalDir: null,
|
|
755
|
+
userDir: testDir,
|
|
756
|
+
projectDir: null,
|
|
757
|
+
}));
|
|
758
|
+
|
|
759
|
+
const state = {
|
|
760
|
+
editAgent: {
|
|
761
|
+
name: "codex",
|
|
762
|
+
description: "test agent",
|
|
763
|
+
systemPrompt: "hello world",
|
|
764
|
+
source: "user",
|
|
765
|
+
filePath: join(testDir, "oldcodex.md"), // oldPath
|
|
766
|
+
model: "old-model",
|
|
767
|
+
thinking: "high",
|
|
768
|
+
},
|
|
769
|
+
editOriginal: {
|
|
770
|
+
name: "oldcodex",
|
|
771
|
+
description: "test agent",
|
|
772
|
+
systemPrompt: "hello world",
|
|
773
|
+
source: "user",
|
|
774
|
+
filePath: join(testDir, "oldcodex.md"),
|
|
775
|
+
},
|
|
776
|
+
agents: [],
|
|
777
|
+
globalDir: null,
|
|
778
|
+
userDir: testDir,
|
|
779
|
+
projectDir: null,
|
|
780
|
+
editError: null,
|
|
781
|
+
editDirty: false,
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
saveAgent(state as any, () => {});
|
|
785
|
+
|
|
786
|
+
// Both fields migrated to the new name; no stale entry under the old name.
|
|
787
|
+
const config = JSON.parse(
|
|
788
|
+
readFileSync(join(testDir, "agents.local.json"), "utf-8"),
|
|
789
|
+
);
|
|
790
|
+
expect(config).toEqual({ codex: { model: "old-model", thinking: "high" } });
|
|
791
|
+
|
|
792
|
+
// The old .md is unlinked and the new one has no model/thinking lines.
|
|
793
|
+
expect(existsSync(join(testDir, "oldcodex.md"))).toBe(false);
|
|
794
|
+
const newContent = readFileSync(join(testDir, "codex.md"), "utf-8");
|
|
795
|
+
expect(newContent).not.toContain("model:");
|
|
796
|
+
expect(newContent).not.toContain("thinking:");
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|