@vellumai/assistant 0.8.12-staging.1 → 0.8.12
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/openapi.yaml +507 -0
- package/package.json +1 -1
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/bundler/app-compiler.ts +8 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +10 -4
- package/src/cli/lib/upgrade-plugin.ts +13 -7
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openai/chat-completions-provider.ts +2 -1
- package/src/providers/openai/responses-provider.ts +2 -1
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +347 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +359 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/security/redact-json.ts +61 -0
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -10,6 +10,10 @@ mock.module("../config/loader.js", () => ({
|
|
|
10
10
|
|
|
11
11
|
import { createToolAuditListener } from "../events/tool-audit-listener.js";
|
|
12
12
|
import type { ToolInvocationRecord } from "../memory/tool-usage-store.js";
|
|
13
|
+
import {
|
|
14
|
+
OPENAI_PROJECT_KEY_REDACTION_MARKER,
|
|
15
|
+
SYNTHETIC_OPENAI_PROJECT_KEY,
|
|
16
|
+
} from "./secret-fixtures.js";
|
|
13
17
|
|
|
14
18
|
describe("tool audit listener", () => {
|
|
15
19
|
beforeEach(() => {
|
|
@@ -101,6 +105,88 @@ describe("tool audit listener", () => {
|
|
|
101
105
|
expect(records[0].result).toContain("<redacted");
|
|
102
106
|
});
|
|
103
107
|
|
|
108
|
+
test("redacts known-pattern secrets in tool inputs before recording", () => {
|
|
109
|
+
const records: ToolInvocationRecord[] = [];
|
|
110
|
+
const listener = createToolAuditListener((record) => records.push(record));
|
|
111
|
+
|
|
112
|
+
const input = {
|
|
113
|
+
command: `export OPENAI_API_KEY="${SYNTHETIC_OPENAI_PROJECT_KEY}"`,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
listener({
|
|
117
|
+
type: "executed",
|
|
118
|
+
toolName: "bash",
|
|
119
|
+
input,
|
|
120
|
+
workingDir: "/tmp",
|
|
121
|
+
conversationId: "conv-input-redact",
|
|
122
|
+
riskLevel: "low",
|
|
123
|
+
decision: "allow",
|
|
124
|
+
durationMs: 5,
|
|
125
|
+
result: { content: "ok", isError: false },
|
|
126
|
+
});
|
|
127
|
+
listener({
|
|
128
|
+
type: "error",
|
|
129
|
+
toolName: "bash",
|
|
130
|
+
input,
|
|
131
|
+
workingDir: "/tmp",
|
|
132
|
+
conversationId: "conv-input-redact",
|
|
133
|
+
riskLevel: "low",
|
|
134
|
+
decision: "error",
|
|
135
|
+
durationMs: 5,
|
|
136
|
+
errorMessage: "boom",
|
|
137
|
+
isExpected: false,
|
|
138
|
+
errorCategory: "tool_failure",
|
|
139
|
+
});
|
|
140
|
+
listener({
|
|
141
|
+
type: "permission_denied",
|
|
142
|
+
toolName: "bash",
|
|
143
|
+
input,
|
|
144
|
+
workingDir: "/tmp",
|
|
145
|
+
conversationId: "conv-input-redact",
|
|
146
|
+
riskLevel: "high",
|
|
147
|
+
decision: "deny",
|
|
148
|
+
reason: "Permission denied by user",
|
|
149
|
+
durationMs: 5,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(records).toHaveLength(3);
|
|
153
|
+
for (const record of records) {
|
|
154
|
+
expect(record.input).not.toContain(SYNTHETIC_OPENAI_PROJECT_KEY);
|
|
155
|
+
// The stored input must remain parseable JSON: string leaves are
|
|
156
|
+
// redacted before stringification, so the marker lands inside a JSON
|
|
157
|
+
// string value (its quotes escaped) instead of corrupting the
|
|
158
|
+
// serialized form.
|
|
159
|
+
const parsed = JSON.parse(record.input) as { command: string };
|
|
160
|
+
expect(parsed.command).toContain(OPENAI_PROJECT_KEY_REDACTION_MARKER);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("benign inputs round-trip byte-identical (no false-positive mangling)", () => {
|
|
165
|
+
const records: ToolInvocationRecord[] = [];
|
|
166
|
+
const listener = createToolAuditListener((record) => records.push(record));
|
|
167
|
+
|
|
168
|
+
const input = {
|
|
169
|
+
command: "git log --oneline -5 && echo done",
|
|
170
|
+
cwd: "/tmp/project",
|
|
171
|
+
env: { LANG: "en_US.UTF-8" },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
listener({
|
|
175
|
+
type: "executed",
|
|
176
|
+
toolName: "bash",
|
|
177
|
+
input,
|
|
178
|
+
workingDir: "/tmp",
|
|
179
|
+
conversationId: "conv-benign-input",
|
|
180
|
+
riskLevel: "low",
|
|
181
|
+
decision: "allow",
|
|
182
|
+
durationMs: 2,
|
|
183
|
+
result: { content: "ok", isError: false },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(records).toHaveLength(1);
|
|
187
|
+
expect(records[0].input).toBe(JSON.stringify(input));
|
|
188
|
+
});
|
|
189
|
+
|
|
104
190
|
test("does not redact non-secret content like UUIDs or hashes", () => {
|
|
105
191
|
const records: ToolInvocationRecord[] = [];
|
|
106
192
|
const listener = createToolAuditListener((record) => records.push(record));
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { upgradeQualityProfileToFable5Migration } from "../workspace/migrations/100-upgrade-quality-profile-to-fable-5.js";
|
|
13
|
+
|
|
14
|
+
let workspaceDir: string;
|
|
15
|
+
|
|
16
|
+
function writeConfig(data: Record<string, unknown>): void {
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(workspaceDir, "config.json"),
|
|
19
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readConfig(): Record<string, unknown> {
|
|
24
|
+
return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readProfiles(): Record<string, Record<string, unknown>> {
|
|
28
|
+
const llm = readConfig().llm as Record<string, unknown>;
|
|
29
|
+
return llm.profiles as Record<string, Record<string, unknown>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
workspaceDir = join(
|
|
34
|
+
tmpdir(),
|
|
35
|
+
`vellum-migration-100-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
36
|
+
);
|
|
37
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
if (existsSync(workspaceDir)) {
|
|
42
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("100-upgrade-quality-profile-to-fable-5 migration", () => {
|
|
47
|
+
test("no-op when config.json does not exist", () => {
|
|
48
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
49
|
+
expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("no-op when config has no llm.profiles", () => {
|
|
53
|
+
const original = { llm: { default: { provider: "anthropic" } } };
|
|
54
|
+
writeConfig(original);
|
|
55
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
56
|
+
expect(readConfig()).toEqual(original);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("gracefully handles invalid JSON", () => {
|
|
60
|
+
writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
|
|
61
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
62
|
+
expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
|
|
63
|
+
"not-valid-json",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("upgrades quality-optimized from claude-opus-4-7", () => {
|
|
68
|
+
writeConfig({
|
|
69
|
+
llm: {
|
|
70
|
+
profiles: {
|
|
71
|
+
"quality-optimized": {
|
|
72
|
+
provider: "anthropic",
|
|
73
|
+
model: "claude-opus-4-7",
|
|
74
|
+
label: "Quality",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
80
|
+
const profile = readProfiles()["quality-optimized"]!;
|
|
81
|
+
expect(profile.model).toBe("claude-fable-5");
|
|
82
|
+
expect(profile.label).toBe("Quality");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("upgrades the managed profile but not the user-owned custom copy", () => {
|
|
86
|
+
writeConfig({
|
|
87
|
+
llm: {
|
|
88
|
+
profiles: {
|
|
89
|
+
"quality-optimized": {
|
|
90
|
+
provider: "anthropic",
|
|
91
|
+
model: "claude-opus-4-8",
|
|
92
|
+
},
|
|
93
|
+
"custom-quality-optimized": {
|
|
94
|
+
provider: "anthropic",
|
|
95
|
+
model: "claude-opus-4-8",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
101
|
+
const profiles = readProfiles();
|
|
102
|
+
expect(profiles["quality-optimized"]!.model).toBe("claude-fable-5");
|
|
103
|
+
expect(profiles["custom-quality-optimized"]!.model).toBe("claude-opus-4-8");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("upgrades OpenRouter-prefixed model ids", () => {
|
|
107
|
+
writeConfig({
|
|
108
|
+
llm: {
|
|
109
|
+
profiles: {
|
|
110
|
+
"quality-optimized": {
|
|
111
|
+
provider: "openrouter",
|
|
112
|
+
model: "anthropic/claude-opus-4.8",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
118
|
+
expect(readProfiles()["quality-optimized"]!.model).toBe(
|
|
119
|
+
"anthropic/claude-fable-5",
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("leaves user-customized models untouched", () => {
|
|
124
|
+
const original = {
|
|
125
|
+
llm: {
|
|
126
|
+
profiles: {
|
|
127
|
+
"quality-optimized": { provider: "openai", model: "gpt-5.4" },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
writeConfig(original);
|
|
132
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
133
|
+
expect(readConfig()).toEqual(original);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("leaves non-quality profiles untouched", () => {
|
|
137
|
+
writeConfig({
|
|
138
|
+
llm: {
|
|
139
|
+
profiles: {
|
|
140
|
+
balanced: { provider: "anthropic", model: "claude-opus-4-8" },
|
|
141
|
+
"quality-optimized": {
|
|
142
|
+
provider: "anthropic",
|
|
143
|
+
model: "claude-opus-4-8",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
149
|
+
const profiles = readProfiles();
|
|
150
|
+
expect(profiles["balanced"]!.model).toBe("claude-opus-4-8");
|
|
151
|
+
expect(profiles["quality-optimized"]!.model).toBe("claude-fable-5");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("idempotency: no-op on already-upgraded config (no writes)", () => {
|
|
155
|
+
writeConfig({
|
|
156
|
+
llm: {
|
|
157
|
+
profiles: {
|
|
158
|
+
"quality-optimized": {
|
|
159
|
+
provider: "anthropic",
|
|
160
|
+
model: "claude-fable-5",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
const beforeContent = readFileSync(
|
|
166
|
+
join(workspaceDir, "config.json"),
|
|
167
|
+
"utf-8",
|
|
168
|
+
);
|
|
169
|
+
upgradeQualityProfileToFable5Migration.run(workspaceDir);
|
|
170
|
+
expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
|
|
171
|
+
beforeContent,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { upgradeBalancedEconomyToMinimaxM3Migration } from "../workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.js";
|
|
13
|
+
|
|
14
|
+
let workspaceDir: string;
|
|
15
|
+
|
|
16
|
+
function writeConfig(data: Record<string, unknown>): void {
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(workspaceDir, "config.json"),
|
|
19
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readConfig(): Record<string, unknown> {
|
|
24
|
+
return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readProfiles(): Record<string, Record<string, unknown>> {
|
|
28
|
+
const llm = readConfig().llm as Record<string, unknown>;
|
|
29
|
+
return llm.profiles as Record<string, Record<string, unknown>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
workspaceDir = join(
|
|
34
|
+
tmpdir(),
|
|
35
|
+
`vellum-migration-101-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
36
|
+
);
|
|
37
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
if (existsSync(workspaceDir)) {
|
|
42
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("101-upgrade-balanced-economy-to-minimax-m3 migration", () => {
|
|
47
|
+
test("no-op when config.json does not exist", () => {
|
|
48
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
49
|
+
expect(existsSync(join(workspaceDir, "config.json"))).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("no-op when config has no llm.profiles", () => {
|
|
53
|
+
const original = { llm: { default: { provider: "fireworks" } } };
|
|
54
|
+
writeConfig(original);
|
|
55
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
56
|
+
expect(readConfig()).toEqual(original);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("gracefully handles invalid JSON", () => {
|
|
60
|
+
writeFileSync(join(workspaceDir, "config.json"), "not-valid-json");
|
|
61
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
62
|
+
expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
|
|
63
|
+
"not-valid-json",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("upgrades balanced-economy from Kimi K2.6 and drops the logit-bias preset", () => {
|
|
68
|
+
writeConfig({
|
|
69
|
+
llm: {
|
|
70
|
+
profiles: {
|
|
71
|
+
"balanced-economy": {
|
|
72
|
+
provider: "fireworks",
|
|
73
|
+
provider_connection: "fireworks-managed",
|
|
74
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
75
|
+
label: "Balanced Economy",
|
|
76
|
+
description: "Strong open model (Kimi K2.6) at a lower price point",
|
|
77
|
+
maxTokens: 16000,
|
|
78
|
+
effort: "high",
|
|
79
|
+
thinking: { enabled: true, streamThinking: true },
|
|
80
|
+
contextWindow: { maxInputTokens: 200000 },
|
|
81
|
+
logitBias: "suppress-cjk",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
87
|
+
const profile = readProfiles()["balanced-economy"]!;
|
|
88
|
+
expect(profile.model).toBe("accounts/fireworks/models/minimax-m3");
|
|
89
|
+
expect(profile.description).toBe(
|
|
90
|
+
"Strong open model (MiniMax M3) at a lower price point",
|
|
91
|
+
);
|
|
92
|
+
expect(profile.maxTokens).toBe(32000);
|
|
93
|
+
expect(profile.contextWindow).toEqual({ maxInputTokens: 200000 });
|
|
94
|
+
expect("logitBias" in profile).toBe(false);
|
|
95
|
+
expect(profile.label).toBe("Balanced Economy");
|
|
96
|
+
expect(profile.effort).toBe("high");
|
|
97
|
+
expect(profile.thinking).toEqual({ enabled: true, streamThinking: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("leaves user-customized models untouched", () => {
|
|
101
|
+
const original = {
|
|
102
|
+
llm: {
|
|
103
|
+
profiles: {
|
|
104
|
+
"balanced-economy": {
|
|
105
|
+
provider: "fireworks",
|
|
106
|
+
model: "accounts/fireworks/models/deepseek-v4-pro",
|
|
107
|
+
logitBias: "suppress-cjk",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
writeConfig(original);
|
|
113
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
114
|
+
expect(readConfig()).toEqual(original);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("leaves other profiles untouched", () => {
|
|
118
|
+
writeConfig({
|
|
119
|
+
llm: {
|
|
120
|
+
profiles: {
|
|
121
|
+
"my-kimi": {
|
|
122
|
+
provider: "fireworks",
|
|
123
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
124
|
+
},
|
|
125
|
+
"balanced-economy": {
|
|
126
|
+
provider: "fireworks",
|
|
127
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
133
|
+
const profiles = readProfiles();
|
|
134
|
+
expect(profiles["my-kimi"]!.model).toBe(
|
|
135
|
+
"accounts/fireworks/models/kimi-k2p6",
|
|
136
|
+
);
|
|
137
|
+
expect(profiles["balanced-economy"]!.model).toBe(
|
|
138
|
+
"accounts/fireworks/models/minimax-m3",
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("idempotency: no-op on already-upgraded config (no writes)", () => {
|
|
143
|
+
writeConfig({
|
|
144
|
+
llm: {
|
|
145
|
+
profiles: {
|
|
146
|
+
"balanced-economy": {
|
|
147
|
+
provider: "fireworks",
|
|
148
|
+
model: "accounts/fireworks/models/minimax-m3",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const beforeContent = readFileSync(
|
|
154
|
+
join(workspaceDir, "config.json"),
|
|
155
|
+
"utf-8",
|
|
156
|
+
);
|
|
157
|
+
upgradeBalancedEconomyToMinimaxM3Migration.run(workspaceDir);
|
|
158
|
+
expect(readFileSync(join(workspaceDir, "config.json"), "utf-8")).toBe(
|
|
159
|
+
beforeContent,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|