@baize-ai/core 0.3.15 → 0.3.17
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/.dockerignore +1 -0
- package/CHANGELOG.md +29 -0
- package/Dockerfile +10 -1
- package/cli/commands/doctor.js +60 -0
- package/cli/commands/init.js +104 -42
- package/cli/commands/runtime.js +9 -1
- package/cli/lib/__tests__/init-base-url.test.js +55 -13
- package/cli/lib/__tests__/runtime-base-url.test.js +3 -1
- package/cli/lib/__tests__/runtime-launch.test.js +4 -2
- package/cli/lib/__tests__/runtime-setup.test.js +53 -6
- package/cli/lib/__tests__/tmux-env.test.js +19 -5
- package/cli/lib/claude-eval.js +2 -1
- package/cli/lib/codex-hooks.js +12 -0
- package/cli/lib/path-bins.js +54 -0
- package/cli/lib/runtime/claude.js +4 -2
- package/cli/lib/runtime/codex.js +19 -0
- package/cli/lib/runtime/tmux-env.js +5 -4
- package/cli/lib/runtime-setup.js +157 -13
- package/docker/entrypoint.sh +4 -2
- package/docs/ops-runbook.md +161 -0
- package/package.json +2 -2
- package/scripts/docker-publish.sh +11 -5
- package/scripts/pack-release.sh +34 -26
- package/skills/activity-monitor/scripts/__tests__/guardian.test.js +51 -0
- package/skills/activity-monitor/scripts/adapters/runtime-components.js +27 -0
- package/skills/activity-monitor/scripts/guardian.js +45 -1
- package/skills/activity-monitor/scripts/monitor-orchestrator.js +8 -1
- package/skills/activity-monitor/scripts/upgrade-check.js +3 -1
- package/skills/web-console/public/app.js +20 -75
- package/skills/web-console/scripts/model-provider.js +97 -59
- package/skills/web-console/scripts/server.js +6 -24
- package/templates/pm2/ecosystem.config.cjs +5 -4
- package/test/model-provider.test.js +150 -40
- package/test/web-console-routes.test.js +11 -13
|
@@ -41,6 +41,10 @@ function readToml() {
|
|
|
41
41
|
const p = path.join(homeDir, '.codex', 'config.toml');
|
|
42
42
|
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
43
43
|
}
|
|
44
|
+
function readProjectToml() {
|
|
45
|
+
const p = path.join(baizeDir, '.codex', 'config.toml');
|
|
46
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
47
|
+
}
|
|
44
48
|
function readStore() {
|
|
45
49
|
const p = path.join(baizeDir, '.baize', 'providers.json');
|
|
46
50
|
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {};
|
|
@@ -52,10 +56,11 @@ describe('provider store', () => {
|
|
|
52
56
|
expect(mp.getProviders()).toEqual({ active: 'official', providers: [] });
|
|
53
57
|
});
|
|
54
58
|
|
|
55
|
-
test('saveProvider validates name and
|
|
59
|
+
test('saveProvider validates name, url, and kind', async () => {
|
|
56
60
|
expect((await mp.saveProvider({})).success).toBe(false);
|
|
57
61
|
expect((await mp.saveProvider({ name: 'x', cc: { baseUrl: 'ftp://bad' } })).success).toBe(false);
|
|
58
62
|
expect((await mp.saveProvider({ name: 'x', codex: { baseUrl: 'nope' } })).success).toBe(false);
|
|
63
|
+
expect((await mp.saveProvider({ name: 'x', codex: { kind: 'bogus-kind' } })).success).toBe(false);
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
test('saveProvider persists sanitized entry (no secrets echoed)', async () => {
|
|
@@ -70,6 +75,64 @@ describe('provider store', () => {
|
|
|
70
75
|
expect(stored.providers[0].cc.authToken).toBe('sk-ds-token'); // persisted, just not echoed
|
|
71
76
|
});
|
|
72
77
|
|
|
78
|
+
test('saveProvider assigns codex kind + providerKey (D51)', async () => {
|
|
79
|
+
const r = await mp.saveProvider(DEEPSEEK);
|
|
80
|
+
expect(r.provider.codex.kind).toBe('responses-compatible');
|
|
81
|
+
expect(r.provider.codex.providerKey).toBe('deepseek');
|
|
82
|
+
// Stored (not just sanitized) so the slug is stable across restarts.
|
|
83
|
+
const stored = readStore();
|
|
84
|
+
expect(stored.providers[0].codex.kind).toBe('responses-compatible');
|
|
85
|
+
expect(stored.providers[0].codex.providerKey).toBe('deepseek');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('provider without codex.baseUrl defaults to openai-official kind', async () => {
|
|
89
|
+
const r = await mp.saveProvider({ name: 'Claude Only', cc: { baseUrl: 'https://a', authToken: 'sk', model: 'm' } });
|
|
90
|
+
expect(r.success).toBe(true);
|
|
91
|
+
expect(r.provider.codex.kind).toBe('openai-official');
|
|
92
|
+
expect(r.provider.codex.providerKey).toBe('claude-only');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('codex kind can be set explicitly, chat-only included', async () => {
|
|
96
|
+
const r = await mp.saveProvider({ name: 'Old Gateway', codex: { baseUrl: 'https://legacy.example.com', kind: 'chat-only' } });
|
|
97
|
+
expect(r.success).toBe(true);
|
|
98
|
+
expect(r.provider.codex.kind).toBe('chat-only');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('providerKey falls back to codex-<hash8> for reserved slugs (D51)', async () => {
|
|
102
|
+
// Raw reserved key 'openai' → codex-<hash8>; the id itself is sanitized to
|
|
103
|
+
// 'openai-custom' by genId, which is a safe non-reserved slug.
|
|
104
|
+
const r = await mp.saveProvider({ name: 'OpenAI', codex: { baseUrl: 'https://api.deepseek.com', apiKey: 'sk', model: 'm' } });
|
|
105
|
+
expect(r.success).toBe(true);
|
|
106
|
+
expect(r.provider.id).toBe('openai-custom'); // genId reservation (D51-extended)
|
|
107
|
+
expect(r.provider.codex.providerKey).toBe('openai-custom');
|
|
108
|
+
expect(r.provider.codex.providerKey).not.toBe('openai');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('explicit kind survives an update; slug stays stable (D51)', async () => {
|
|
112
|
+
const created = await mp.saveProvider(DEEPSEEK);
|
|
113
|
+
const updated = await mp.saveProvider({ id: created.provider.id, name: 'DeepSeek 主', codex: { baseUrl: 'https://api.deepseek.com/v2', apiKey: 'sk2', model: 'm2' } });
|
|
114
|
+
expect(updated.success).toBe(true);
|
|
115
|
+
expect(updated.provider.codex.kind).toBe('responses-compatible'); // unchanged
|
|
116
|
+
expect(updated.provider.codex.providerKey).toBe('deepseek'); // slug never regenerated
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('lazy migration backfills kind/providerKey for legacy store entries (D51)', async () => {
|
|
120
|
+
// Simulate a pre-D51 providers.json: codex config but no kind/providerKey.
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
path.join(baizeDir, '.baize', 'providers.json'),
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
active: 'legacy',
|
|
125
|
+
providers: [{ id: 'legacy', name: 'Legacy', codex: { baseUrl: 'https://api.deepseek.com', apiKey: 'sk', model: 'm' } }],
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
const list = mp.getProviders();
|
|
129
|
+
expect(list.providers[0].codex.kind).toBe('responses-compatible');
|
|
130
|
+
expect(list.providers[0].codex.providerKey).toBe('legacy');
|
|
131
|
+
// Backfill is persisted by the lazy migration.
|
|
132
|
+
expect(readStore().providers[0].codex.kind).toBe('responses-compatible');
|
|
133
|
+
expect(readStore().providers[0].codex.providerKey).toBe('legacy');
|
|
134
|
+
});
|
|
135
|
+
|
|
73
136
|
test('deleteProvider falls back to official when active', async () => {
|
|
74
137
|
await mp.saveProvider(DEEPSEEK);
|
|
75
138
|
const store = readStore();
|
|
@@ -82,7 +145,7 @@ describe('provider store', () => {
|
|
|
82
145
|
});
|
|
83
146
|
|
|
84
147
|
describe('activateProvider — external (cc + codex)', () => {
|
|
85
|
-
test('applies env/settings/auth
|
|
148
|
+
test('applies env/settings/auth + provider block, stashes official key, restarts', async () => {
|
|
86
149
|
// Pre-configure an official API key via D5-style storage
|
|
87
150
|
fs.writeFileSync(path.join(baizeDir, '.env'), 'ANTHROPIC_API_KEY=sk-ant-official-key\n');
|
|
88
151
|
fs.mkdirSync(path.join(homeDir, '.claude'), { recursive: true });
|
|
@@ -104,7 +167,7 @@ describe('activateProvider — external (cc + codex)', () => {
|
|
|
104
167
|
expect(env).toMatch(/^ANTHROPIC_BASE_URL=https:\/\/api\.deepseek\.com\/anthropic$/m);
|
|
105
168
|
expect(env).toMatch(/^ANTHROPIC_MODEL=deepseek-chat$/m);
|
|
106
169
|
expect(env).toMatch(/^ANTHROPIC_SMALL_FAST_MODEL=deepseek-chat$/m);
|
|
107
|
-
expect(env).toMatch(/^OPENAI_BASE_URL=https:\/\/api\.deepseek\.com$/m);
|
|
170
|
+
expect(env).toMatch(/^OPENAI_BASE_URL=https:\/\/api\.deepseek\.com$/m); // legacy health-probe surface, kept
|
|
108
171
|
expect(env).not.toMatch(/ANTHROPIC_API_KEY/);
|
|
109
172
|
|
|
110
173
|
// settings.json env: same
|
|
@@ -115,12 +178,29 @@ describe('activateProvider — external (cc + codex)', () => {
|
|
|
115
178
|
// stash preserved for revert
|
|
116
179
|
expect(readStore().stash.apiKey).toBe('sk-ant-official-key');
|
|
117
180
|
|
|
118
|
-
// codex auth.json + config
|
|
181
|
+
// codex auth.json + provider block config (D51)
|
|
119
182
|
const auth = JSON.parse(fs.readFileSync(path.join(homeDir, '.codex', 'auth.json'), 'utf8'));
|
|
120
183
|
expect(auth.OPENAI_API_KEY).toBe('sk-ds-key');
|
|
121
184
|
const toml = readToml();
|
|
122
|
-
expect(toml).toMatch(/^
|
|
123
|
-
expect(toml).toMatch(
|
|
185
|
+
expect(toml).toMatch(/^model_provider = "deepseek"$/m);
|
|
186
|
+
expect(toml).toMatch(/^\[model_providers\.deepseek\]$/m);
|
|
187
|
+
expect(toml).toMatch(/^name = "deepseek"$/m);
|
|
188
|
+
expect(toml).toMatch(/^base_url = "https:\/\/api\.deepseek\.com"$/m);
|
|
189
|
+
expect(toml).toMatch(/^wire_api = "responses"$/m);
|
|
190
|
+
expect(toml).toMatch(/^experimental_bearer_token = "sk-ds-key"$/m);
|
|
191
|
+
// the legacy override must never coexist with the provider block
|
|
192
|
+
expect(toml).not.toMatch(/openai_base_url/);
|
|
193
|
+
|
|
194
|
+
// project-level: model ensured, openai_base_url cleaned, no provider block
|
|
195
|
+
const projectToml = readProjectToml();
|
|
196
|
+
expect(projectToml).toMatch(/^model = "deepseek-chat"$/m);
|
|
197
|
+
expect(projectToml).not.toMatch(/openai_base_url/);
|
|
198
|
+
expect(projectToml).not.toMatch(/model_provider/);
|
|
199
|
+
expect(projectToml).not.toMatch(/\[model_providers\./);
|
|
200
|
+
|
|
201
|
+
// token-bearing global config is user-only
|
|
202
|
+
const mode = fs.statSync(path.join(homeDir, '.codex', 'config.toml')).mode & 0o777;
|
|
203
|
+
expect(mode).toBe(0o600);
|
|
124
204
|
});
|
|
125
205
|
|
|
126
206
|
test('probe failure surfaces a warning but still applies', async () => {
|
|
@@ -139,6 +219,22 @@ describe('activateProvider — external (cc + codex)', () => {
|
|
|
139
219
|
});
|
|
140
220
|
});
|
|
141
221
|
|
|
222
|
+
describe('activateProvider — chat-only interception (D51)', () => {
|
|
223
|
+
test('activating a chat-only provider is rejected before anything is written', async () => {
|
|
224
|
+
const saved = await mp.saveProvider({
|
|
225
|
+
name: '旧网关',
|
|
226
|
+
codex: { baseUrl: 'https://legacy.example.com', apiKey: 'sk-legacy', model: 'gpt-3.5', kind: 'chat-only' },
|
|
227
|
+
});
|
|
228
|
+
const before = readToml();
|
|
229
|
+
const r = await mp.activateProvider(saved.provider.id, { probe: async () => true, restart: noopRestart });
|
|
230
|
+
expect(r.success).toBe(false);
|
|
231
|
+
expect(r.error).toMatch(/仅支持 Chat 接口/);
|
|
232
|
+
expect(r.error).toMatch(/Claude 运行时/);
|
|
233
|
+
expect(readToml()).toBe(before); // nothing written
|
|
234
|
+
expect(readEnv()).not.toMatch(/OPENAI_BASE_URL=https:\/\/legacy\.example\.com/);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
142
238
|
describe('activateProvider — official restores', () => {
|
|
143
239
|
test('official clears external vars and restores stashed key', async () => {
|
|
144
240
|
fs.writeFileSync(path.join(baizeDir, '.env'), 'ANTHROPIC_API_KEY=sk-ant-official-key\n');
|
|
@@ -160,58 +256,57 @@ describe('activateProvider — official restores', () => {
|
|
|
160
256
|
expect(readStore().stash).toBeUndefined(); // stash cleared from file
|
|
161
257
|
});
|
|
162
258
|
|
|
163
|
-
test('official-openai
|
|
259
|
+
test('official-openai drops the provider block and resets model to gpt-5.5 (D51)', async () => {
|
|
164
260
|
await mp.saveProvider(DEEPSEEK);
|
|
165
261
|
await mp.activateProvider('deepseek', { probe: async () => true, restart: noopRestart });
|
|
166
262
|
|
|
167
263
|
const r = await mp.activateProvider('official-openai', { restart: noopRestart });
|
|
168
264
|
expect(r.success).toBe(true);
|
|
265
|
+
expect(r.codexProviderKey).toBeNull();
|
|
169
266
|
const env = readEnv();
|
|
170
|
-
expect(env).not.toMatch(/OPENAI_BASE_URL/);
|
|
171
267
|
const toml = readToml();
|
|
268
|
+
expect(env).not.toMatch(/OPENAI_BASE_URL/);
|
|
172
269
|
expect(toml).toMatch(/^model = "gpt-5.5"$/m);
|
|
270
|
+
expect(toml).not.toMatch(/^model_provider/m);
|
|
271
|
+
expect(toml).not.toMatch(/\[model_providers\.deepseek\]/);
|
|
173
272
|
expect(toml).not.toMatch(/openai_base_url/);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
describe('editCodexConfigToml (TOML scope aware)', () => {
|
|
178
|
-
test('top-level keys land BEFORE the first section header; section content byte-preserved', () => {
|
|
179
|
-
fs.mkdirSync(path.join(homeDir, '.codex'), { recursive: true });
|
|
180
|
-
fs.writeFileSync(path.join(homeDir, '.codex', 'config.toml'), '# comment\n\n[model_providers.my]\nbase_url = "https://x"\nmodel = "other"\n');
|
|
181
|
-
mp.editCodexConfigToml({ model: 'deepseek-chat', openaiBaseUrl: 'https://api.deepseek.com' });
|
|
182
|
-
const toml = readToml();
|
|
183
|
-
const sectionIdx = toml.indexOf('[model_providers.my]');
|
|
184
|
-
// top-level keys must appear in the prefix BEFORE the section header
|
|
185
|
-
const topLevelIdx = toml.indexOf('model = "deepseek-chat"');
|
|
186
|
-
const baseUrlIdx = toml.indexOf('openai_base_url = "https://api.deepseek.com"');
|
|
187
|
-
expect(topLevelIdx).toBeGreaterThanOrEqual(0);
|
|
188
|
-
expect(baseUrlIdx).toBeGreaterThanOrEqual(0);
|
|
189
|
-
expect(topLevelIdx).toBeLessThan(sectionIdx);
|
|
190
|
-
expect(baseUrlIdx).toBeLessThan(sectionIdx);
|
|
191
|
-
// section content untouched
|
|
192
|
-
expect(toml).toMatch(/^\[model_providers\.my\]$/m);
|
|
193
|
-
expect(toml).toMatch(/^base_url = "https:\/\/x"$/m);
|
|
194
|
-
expect(toml).toMatch(/^model = "other"$/m);
|
|
273
|
+
const projectToml = readProjectToml();
|
|
274
|
+
expect(projectToml).not.toMatch(/openai_base_url/);
|
|
195
275
|
});
|
|
196
276
|
|
|
197
|
-
test('
|
|
277
|
+
test('official-openai removes baize-written tables but preserves hand-edited ones (D51 review P2)', async () => {
|
|
278
|
+
// Contract (review P2): a baize-written block always has name === key &&
|
|
279
|
+
// wire_api === 'responses'. Those are removed on restore. Hand-edited
|
|
280
|
+
// user providers (any other shape) survive untouched.
|
|
198
281
|
fs.mkdirSync(path.join(homeDir, '.codex'), { recursive: true });
|
|
199
|
-
fs.writeFileSync(
|
|
200
|
-
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
path.join(homeDir, '.codex', 'config.toml'),
|
|
284
|
+
'model_provider = "deepseek"\n\n[model_providers.deepseek]\nname = "deepseek"\nbase_url = "https://api.deepseek.com"\nwire_api = "responses"\n\n[model_providers.myrelay]\nname = "My Relay"\nbase_url = "https://r.example"\n',
|
|
285
|
+
);
|
|
286
|
+
const r = await mp.activateProvider('official-openai', { restart: noopRestart });
|
|
287
|
+
expect(r.success).toBe(true);
|
|
201
288
|
const toml = readToml();
|
|
202
|
-
expect(toml.
|
|
203
|
-
expect(toml.
|
|
289
|
+
expect(toml).toMatch(/^model = "gpt-5.5"$/m);
|
|
290
|
+
expect(toml).not.toMatch(/^model_provider/m);
|
|
291
|
+
expect(toml).not.toMatch(/\[model_providers\.deepseek\]/, 'baize-written block removed');
|
|
292
|
+
expect(toml).toMatch(/\[model_providers\.myrelay\]/, 'hand-edited provider survives');
|
|
293
|
+
expect(toml).toMatch(/name = "My Relay"/);
|
|
204
294
|
});
|
|
205
295
|
|
|
206
|
-
test('
|
|
296
|
+
test('activating an openai-official-kind provider keeps the built-in OpenAI provider (D51)', async () => {
|
|
297
|
+
fs.mkdirSync(path.join(baizeDir, '.baize'), { recursive: true });
|
|
298
|
+
fs.writeFileSync(path.join(baizeDir, '.baize', 'config.json'), JSON.stringify({ runtime: 'codex' }));
|
|
207
299
|
fs.mkdirSync(path.join(homeDir, '.codex'), { recursive: true });
|
|
208
|
-
|
|
209
|
-
|
|
300
|
+
// fixture: baize-written shape (name === key && wire_api === responses) so restore removes it
|
|
301
|
+
fs.writeFileSync(path.join(homeDir, '.codex', 'config.toml'), 'model_provider = "deepseek"\n\n[model_providers.deepseek]\nname = "deepseek"\nbase_url = "https://api.deepseek.com"\nwire_api = "responses"\n');
|
|
302
|
+
await mp.saveProvider({ name: '官方 OpenAI', codex: { apiKey: 'sk-official', model: 'gpt-5.5' } }); // no baseUrl → openai-official
|
|
303
|
+
const r = await mp.activateProvider('openai-custom', { probe: async () => true, restart: noopRestart });
|
|
304
|
+
expect(r.success).toBe(true);
|
|
305
|
+
expect(r.codexProviderKey).toBeNull();
|
|
210
306
|
const toml = readToml();
|
|
211
|
-
expect(toml).not.toMatch(/^
|
|
307
|
+
expect(toml).not.toMatch(/^model_provider/m); // built-in provider back in charge
|
|
308
|
+
expect(toml).not.toMatch(/\[model_providers\.deepseek\]/);
|
|
212
309
|
expect(toml).toMatch(/^model = "gpt-5.5"$/m);
|
|
213
|
-
expect(toml).toMatch(/^\[model_providers\.my\]$/m);
|
|
214
|
-
expect(toml).toMatch(/^model = "deepseek-chat"$/m); // section model untouched
|
|
215
310
|
});
|
|
216
311
|
});
|
|
217
312
|
|
|
@@ -268,3 +363,18 @@ describe('activateProvider — side-coverage validation (D13)', () => {
|
|
|
268
363
|
expect(r.success).toBe(true);
|
|
269
364
|
});
|
|
270
365
|
});
|
|
366
|
+
|
|
367
|
+
describe('D51 migration — legacy activation cleans stale overrides', () => {
|
|
368
|
+
test('activating a provider removes a pre-existing openai_base_url override', async () => {
|
|
369
|
+
fs.mkdirSync(path.join(homeDir, '.codex'), { recursive: true });
|
|
370
|
+
fs.writeFileSync(path.join(homeDir, '.codex', 'config.toml'), 'openai_base_url = "https://old.example.com/v1"\nmodel = "old"\n');
|
|
371
|
+
fs.mkdirSync(path.join(baizeDir, '.baize'), { recursive: true });
|
|
372
|
+
fs.writeFileSync(path.join(baizeDir, '.baize', 'config.json'), JSON.stringify({ runtime: 'codex' }));
|
|
373
|
+
await mp.saveProvider({ name: 'codex-only', codex: { baseUrl: 'https://api.deepseek.com', apiKey: 'sk', model: 'deepseek-chat' } });
|
|
374
|
+
const r = await mp.activateProvider('codex-only', { probe: async () => true, restart: noopRestart });
|
|
375
|
+
expect(r.success).toBe(true);
|
|
376
|
+
const toml = readToml();
|
|
377
|
+
expect(toml).not.toMatch(/openai_base_url/);
|
|
378
|
+
expect(toml).toMatch(/^model_provider = "codex-only"$/m);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -744,7 +744,7 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
744
744
|
expect(await res.json()).toEqual({ success: true, mode: 'open', allow: [], block: [] });
|
|
745
745
|
});
|
|
746
746
|
|
|
747
|
-
test('PUT authz validates mode enum and
|
|
747
|
+
test('PUT authz validates mode enum; allow/block ignored and forced empty (D52 governance)', async () => {
|
|
748
748
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
749
749
|
const put = async (body) => {
|
|
750
750
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/authz`, {
|
|
@@ -754,17 +754,15 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
754
754
|
});
|
|
755
755
|
return { status: res.status, body: await res.json() };
|
|
756
756
|
};
|
|
757
|
-
expect((await put({ mode: 'bogus' })).status).toBe(400);
|
|
758
|
-
expect((await put({
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
expect((await put({ mode: 'open', block: ['a', 'a'], allow: [] })).status).toBe(400); // duplicate
|
|
762
|
-
const ok = await put({ mode: 'allowlist', allow: ['peer_1', 'peer_2'], block: [] });
|
|
757
|
+
expect((await put({ mode: 'bogus' })).status).toBe(400); // invalid mode rejected
|
|
758
|
+
expect((await put({})).status).toBe(400); // missing mode rejected
|
|
759
|
+
// D52: lists are admin-managed — any allow/block payload is ignored → []
|
|
760
|
+
const ok = await put({ mode: 'allowlist', allow: ['peer_1', 'peer_2'], block: ['x'] });
|
|
763
761
|
expect(ok.status).toBe(200);
|
|
764
|
-
expect(ok.body).toEqual({ success: true, mode: 'allowlist', allow: [
|
|
762
|
+
expect(ok.body).toEqual({ success: true, mode: 'allowlist', allow: [], block: [] });
|
|
765
763
|
});
|
|
766
764
|
|
|
767
|
-
test('PUT authz writes config.json preserving other fields
|
|
765
|
+
test('PUT authz writes config.json preserving other fields; lists forced empty', async () => {
|
|
768
766
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
769
767
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/authz`, {
|
|
770
768
|
method: 'PUT',
|
|
@@ -772,16 +770,16 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
772
770
|
body: JSON.stringify({ mode: 'open', allow: [], block: [' peer_9 ', 'peer_10'] }),
|
|
773
771
|
});
|
|
774
772
|
expect(res.status).toBe(200);
|
|
775
|
-
expect(await res.json()).toEqual({ success: true, mode: 'open', allow: [], block: [
|
|
773
|
+
expect(await res.json()).toEqual({ success: true, mode: 'open', allow: [], block: [] });
|
|
776
774
|
const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
|
|
777
|
-
expect(cfg.authz).toEqual({ mode: 'open', allow: [], block: [
|
|
775
|
+
expect(cfg.authz).toEqual({ mode: 'open', allow: [], block: [] });
|
|
778
776
|
expect(cfg.enabled).toBe(true);
|
|
779
777
|
expect(cfg.agentId).toBe('agent_test');
|
|
780
778
|
expect(cfg.cert).toEqual({ keyPath: '', certPath: '' });
|
|
781
779
|
expect(cfg.name).toBeUndefined();
|
|
782
780
|
});
|
|
783
781
|
|
|
784
|
-
test('PUT authz creates config.json when missing', async () => {
|
|
782
|
+
test('PUT authz creates config.json when missing (mode only)', async () => {
|
|
785
783
|
fs.rmSync(configFile());
|
|
786
784
|
ctx = await startServer({ extraEnv: a2aEnv() });
|
|
787
785
|
const res = await fetch(`${ctx.baseUrl}/api/admin/a2a/authz`, {
|
|
@@ -791,7 +789,7 @@ describe('web-console a2a authz routes (D22 单元 C)', () => {
|
|
|
791
789
|
});
|
|
792
790
|
expect(res.status).toBe(200);
|
|
793
791
|
const cfg = JSON.parse(fs.readFileSync(configFile(), 'utf8'));
|
|
794
|
-
expect(cfg.authz).toEqual({ mode: 'allowlist', allow: [
|
|
792
|
+
expect(cfg.authz).toEqual({ mode: 'allowlist', allow: [], block: [] });
|
|
795
793
|
});
|
|
796
794
|
|
|
797
795
|
test('authz routes require session when a password is set', async () => {
|