@animalabs/connectome-host 0.3.8 → 0.3.10
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/.env.example +7 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +26 -0
- package/.github/workflows/changelog.yml +32 -0
- package/.github/workflows/publish.yml +46 -0
- package/CHANGELOG.md +59 -0
- package/CONTRIBUTING.md +126 -0
- package/README.md +52 -5
- package/bun.lock +6 -10
- package/package.json +5 -4
- package/scripts/release-changelog.ts +32 -0
- package/src/codex-subscription-adapter.ts +938 -0
- package/src/commands.ts +45 -0
- package/src/extensions.ts +201 -0
- package/src/framework-agent-config.ts +17 -9
- package/src/framework-strategy.ts +21 -0
- package/src/index.ts +102 -19
- package/src/logging-bedrock-adapter.ts +145 -0
- package/src/modules/web-ui-module.ts +45 -1
- package/src/recipe.ts +140 -43
- package/src/tui.ts +165 -34
- package/src/web/protocol.ts +35 -0
- package/test/codex-subscription-adapter.test.ts +226 -0
- package/test/fast-command.test.ts +39 -0
- package/test/framework-fkm-composition.test.ts +4 -13
- package/test/recipe-extensions.test.ts +295 -0
- package/test/recipe-primary-summary-fallback-rejection.test.ts +24 -0
- package/test/recipe-provider.test.ts +14 -1
- package/test/web-ui-observers.test.ts +14 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/web/src/App.tsx +235 -20
- package/web/src/Branches.tsx +150 -0
- package/web/src/Context.tsx +21 -13
- package/web/src/Health.tsx +274 -0
- package/web/src/Lessons.tsx +2 -1
- package/test/recipe-primary-summary-fallback.test.ts +0 -40
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { AgentFramework } from '@animalabs/agent-framework';
|
|
3
|
+
import { handleCommand } from '../src/commands.js';
|
|
4
|
+
|
|
5
|
+
function app(adapter?: { enabled: boolean }) : Parameters<typeof handleCommand>[1] {
|
|
6
|
+
return {
|
|
7
|
+
framework: {} as AgentFramework,
|
|
8
|
+
sessionManager: {} as never,
|
|
9
|
+
recipe: { name: 'test' } as never,
|
|
10
|
+
branchState: {} as never,
|
|
11
|
+
codexAdapter: adapter ? {
|
|
12
|
+
isFastMode: () => adapter.enabled,
|
|
13
|
+
setFastMode: (enabled) => { adapter.enabled = enabled; },
|
|
14
|
+
} : undefined,
|
|
15
|
+
switchSession: async () => {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('/fast', () => {
|
|
20
|
+
test('reports that the command requires the Codex subscription provider', () => {
|
|
21
|
+
expect(handleCommand('/fast on', app()).lines[0]?.text).toMatch(/openai-codex/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('toggles and reports Fast mode', () => {
|
|
25
|
+
const state = { enabled: false };
|
|
26
|
+
|
|
27
|
+
expect(handleCommand('/fast', app(state)).lines[0]?.text).toMatch(/OFF/);
|
|
28
|
+
expect(handleCommand('/fast on', app(state)).lines[0]?.text).toMatch(/requested/);
|
|
29
|
+
expect(state.enabled).toBe(true);
|
|
30
|
+
expect(handleCommand('/fast status', app(state)).lines[0]?.text).toMatch(/ON/);
|
|
31
|
+
expect(handleCommand('/fast off', app(state)).lines[0]?.text).toMatch(/disabled/);
|
|
32
|
+
expect(state.enabled).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('rejects unknown modes', () => {
|
|
36
|
+
expect(handleCommand('/fast turbo', app({ enabled: false })).lines[0]?.text)
|
|
37
|
+
.toBe('Usage: /fast [on|off|status]');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -98,11 +98,6 @@ describe('framework FKM composition', () => {
|
|
|
98
98
|
sameRoundThinkTextPolicy: 'private',
|
|
99
99
|
refusalHandling: {
|
|
100
100
|
autoRewind: true,
|
|
101
|
-
primarySummaryFallback: {
|
|
102
|
-
enabled: true,
|
|
103
|
-
maxNewSummaries: 4,
|
|
104
|
-
requestBudgetTokens: 216_000,
|
|
105
|
-
},
|
|
106
101
|
},
|
|
107
102
|
strategy: {
|
|
108
103
|
type: 'autobiographical',
|
|
@@ -118,10 +113,8 @@ describe('framework FKM composition', () => {
|
|
|
118
113
|
expect(reloaded).not.toBeNull();
|
|
119
114
|
|
|
120
115
|
expect(reloaded!.agent.sameRoundThinkTextPolicy).toBe('private');
|
|
121
|
-
expect(reloaded!.agent.refusalHandling
|
|
122
|
-
|
|
123
|
-
maxNewSummaries: 4,
|
|
124
|
-
requestBudgetTokens: 216_000,
|
|
116
|
+
expect(reloaded!.agent.refusalHandling).toEqual({
|
|
117
|
+
autoRewind: true,
|
|
125
118
|
});
|
|
126
119
|
expect(reloaded!.agent.strategy?.compressionRefusalCurveFallbacks).toBe(2);
|
|
127
120
|
expect(reloaded!.agent.strategy?.compressionContextBudgetTokens).toBe(19_000);
|
|
@@ -133,10 +126,8 @@ describe('framework FKM composition', () => {
|
|
|
133
126
|
|
|
134
127
|
const agentConfig = buildFrameworkAgentConfig(reloaded!, 'agent', 'model', runtimeStrategy);
|
|
135
128
|
expect(agentConfig.sameRoundThinkTextPolicy).toBe('private');
|
|
136
|
-
expect(agentConfig.refusalHandling
|
|
137
|
-
|
|
138
|
-
maxNewSummaries: 4,
|
|
139
|
-
requestBudgetTokens: 216_000,
|
|
129
|
+
expect(agentConfig.refusalHandling).toEqual({
|
|
130
|
+
autoRewind: true,
|
|
140
131
|
});
|
|
141
132
|
expect((agentConfig.providerParams as Record<string, unknown>).service_tier).toBe('priority');
|
|
142
133
|
expect(Object.keys(agentConfig.providerParams as Record<string, unknown>)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the recipe `extensions` seam: schema validation, load-time path
|
|
3
|
+
* resolution, the extension loader/registry, and custom strategy dispatch
|
|
4
|
+
* through buildFrameworkStrategy.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
7
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { loadRecipe, validateRecipe, type Recipe } from '../src/recipe.js';
|
|
11
|
+
import {
|
|
12
|
+
loadExtensions,
|
|
13
|
+
emptyExtensionRegistry,
|
|
14
|
+
isBuiltinStrategyType,
|
|
15
|
+
} from '../src/extensions.js';
|
|
16
|
+
import { buildFrameworkStrategy } from '../src/framework-strategy.js';
|
|
17
|
+
|
|
18
|
+
let dir: string;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
dir = mkdtempSync(join(tmpdir(), 'recipe-ext-'));
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function baseRecipe(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
27
|
+
return {
|
|
28
|
+
name: 'Ext Test',
|
|
29
|
+
agent: { systemPrompt: 'x' },
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Write an extension module file and return its absolute path. */
|
|
35
|
+
function writeExtension(filename: string, body: string): string {
|
|
36
|
+
const path = join(dir, filename);
|
|
37
|
+
writeFileSync(path, body, 'utf-8');
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// validateRecipe
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
describe('validateRecipe extensions block', () => {
|
|
46
|
+
test('accepts a well-formed extensions block', () => {
|
|
47
|
+
const recipe = validateRecipe(baseRecipe({
|
|
48
|
+
extensions: {
|
|
49
|
+
zk: { kind: 'strategy', path: './ext.ts', config: { a: 1 } },
|
|
50
|
+
feeder: { kind: 'module', path: '/abs/feeder.ts' },
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
expect(Object.keys(recipe.extensions!)).toEqual(['zk', 'feeder']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('rejects bad kind', () => {
|
|
57
|
+
expect(() => validateRecipe(baseRecipe({
|
|
58
|
+
extensions: { zk: { kind: 'plugin', path: './ext.ts' } },
|
|
59
|
+
}))).toThrow(/kind must be "strategy" or "module"/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('rejects missing path', () => {
|
|
63
|
+
expect(() => validateRecipe(baseRecipe({
|
|
64
|
+
extensions: { zk: { kind: 'strategy' } },
|
|
65
|
+
}))).toThrow(/path must be a non-empty string/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('rejects non-object config', () => {
|
|
69
|
+
expect(() => validateRecipe(baseRecipe({
|
|
70
|
+
extensions: { zk: { kind: 'strategy', path: './e.ts', config: [1] } },
|
|
71
|
+
}))).toThrow(/config must be an object/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('rejects array extensions block', () => {
|
|
75
|
+
expect(() => validateRecipe(baseRecipe({ extensions: [] })))
|
|
76
|
+
.toThrow(/must be an object mapping names/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('custom strategy type without a strategy-kind extension is rejected', () => {
|
|
80
|
+
expect(() => validateRecipe(baseRecipe({
|
|
81
|
+
agent: { systemPrompt: 'x', strategy: { type: 'zk' } },
|
|
82
|
+
}))).toThrow(/Invalid strategy type "zk"/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('custom strategy type with only module-kind extensions is rejected', () => {
|
|
86
|
+
expect(() => validateRecipe(baseRecipe({
|
|
87
|
+
agent: { systemPrompt: 'x', strategy: { type: 'zk' } },
|
|
88
|
+
extensions: { feeder: { kind: 'module', path: './f.ts' } },
|
|
89
|
+
}))).toThrow(/Invalid strategy type "zk"/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('custom strategy type with a strategy-kind extension is accepted', () => {
|
|
93
|
+
const recipe = validateRecipe(baseRecipe({
|
|
94
|
+
agent: { systemPrompt: 'x', strategy: { type: 'zk' } },
|
|
95
|
+
extensions: { zk: { kind: 'strategy', path: './e.ts' } },
|
|
96
|
+
}));
|
|
97
|
+
expect(recipe.agent.strategy?.type).toBe('zk');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('built-in strategy types still validate without extensions', () => {
|
|
101
|
+
const recipe = validateRecipe(baseRecipe({
|
|
102
|
+
agent: { systemPrompt: 'x', strategy: { type: 'frontdesk' } },
|
|
103
|
+
}));
|
|
104
|
+
expect(recipe.agent.strategy?.type).toBe('frontdesk');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// loadRecipe path resolution
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe('loadRecipe extension path resolution', () => {
|
|
113
|
+
test('relative extension paths resolve against the recipe dir', async () => {
|
|
114
|
+
const recipePath = join(dir, 'r.json');
|
|
115
|
+
writeFileSync(recipePath, JSON.stringify(baseRecipe({
|
|
116
|
+
extensions: { zk: { kind: 'module', path: './exts/zk.ts' } },
|
|
117
|
+
})), 'utf-8');
|
|
118
|
+
const recipe = await loadRecipe(recipePath);
|
|
119
|
+
expect(recipe.extensions!.zk!.path).toBe(join(dir, 'exts', 'zk.ts'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('absolute extension paths pass through', async () => {
|
|
123
|
+
const recipePath = join(dir, 'r.json');
|
|
124
|
+
writeFileSync(recipePath, JSON.stringify(baseRecipe({
|
|
125
|
+
extensions: { zk: { kind: 'module', path: '/opt/zk/index.ts' } },
|
|
126
|
+
})), 'utf-8');
|
|
127
|
+
const recipe = await loadRecipe(recipePath);
|
|
128
|
+
expect(recipe.extensions!.zk!.path).toBe('/opt/zk/index.ts');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// loadExtensions
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('loadExtensions', () => {
|
|
137
|
+
test('empty when no extensions declared', async () => {
|
|
138
|
+
const recipe = validateRecipe(baseRecipe());
|
|
139
|
+
const registry = await loadExtensions(recipe);
|
|
140
|
+
expect(registry.strategies.size).toBe(0);
|
|
141
|
+
expect(registry.modules.length).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('collects strategy and module registrations, passing config', async () => {
|
|
145
|
+
const path = writeExtension('combo.mjs', `
|
|
146
|
+
export function register(api, config) {
|
|
147
|
+
api.registerStrategy('zk', (ctx) => ({ kind: 'zk-strategy', opts: ctx }));
|
|
148
|
+
api.registerModule((ctx) => ({ name: 'zk-feeder', ctx }));
|
|
149
|
+
if (config.marker !== 'hello') throw new Error('config not passed');
|
|
150
|
+
}
|
|
151
|
+
`);
|
|
152
|
+
const recipe = validateRecipe(baseRecipe({
|
|
153
|
+
extensions: { zk: { kind: 'strategy', path, config: { marker: 'hello' } } },
|
|
154
|
+
}));
|
|
155
|
+
const registry = await loadExtensions(recipe);
|
|
156
|
+
expect(registry.strategies.has('zk')).toBe(true);
|
|
157
|
+
expect(registry.modules.length).toBe(1);
|
|
158
|
+
expect(registry.modules[0]!.extensionName).toBe('zk');
|
|
159
|
+
expect(registry.modules[0]!.config).toEqual({ marker: 'hello' });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('accepts a default-export register function', async () => {
|
|
163
|
+
const path = writeExtension('default.mjs', `
|
|
164
|
+
export default function (api) {
|
|
165
|
+
api.registerStrategy('dflt', () => ({ kind: 'dflt' }));
|
|
166
|
+
}
|
|
167
|
+
`);
|
|
168
|
+
const recipe = validateRecipe(baseRecipe({
|
|
169
|
+
extensions: { d: { kind: 'strategy', path } },
|
|
170
|
+
}));
|
|
171
|
+
const registry = await loadExtensions(recipe);
|
|
172
|
+
expect(registry.strategies.has('dflt')).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('supports async register', async () => {
|
|
176
|
+
const path = writeExtension('async.mjs', `
|
|
177
|
+
export async function register(api) {
|
|
178
|
+
await Promise.resolve();
|
|
179
|
+
api.registerModule(() => ({ name: 'late' }));
|
|
180
|
+
}
|
|
181
|
+
`);
|
|
182
|
+
const recipe = validateRecipe(baseRecipe({
|
|
183
|
+
extensions: { a: { kind: 'module', path } },
|
|
184
|
+
}));
|
|
185
|
+
const registry = await loadExtensions(recipe);
|
|
186
|
+
expect(registry.modules.length).toBe(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('rejects a module without a register function', async () => {
|
|
190
|
+
const path = writeExtension('bad.mjs', `export const nothing = 1;`);
|
|
191
|
+
const recipe = validateRecipe(baseRecipe({
|
|
192
|
+
extensions: { bad: { kind: 'module', path } },
|
|
193
|
+
}));
|
|
194
|
+
await expect(loadExtensions(recipe)).rejects.toThrow(/must export a "register" function/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('rejects relative paths at load time', async () => {
|
|
198
|
+
const recipe = validateRecipe(baseRecipe({
|
|
199
|
+
extensions: { rel: { kind: 'module', path: './never-resolved.ts' } },
|
|
200
|
+
}));
|
|
201
|
+
await expect(loadExtensions(recipe)).rejects.toThrow(/not absolute/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('rejects registering a built-in strategy name', async () => {
|
|
205
|
+
const path = writeExtension('builtin-clash.mjs', `
|
|
206
|
+
export function register(api) {
|
|
207
|
+
api.registerStrategy('autobiographical', () => ({}));
|
|
208
|
+
}
|
|
209
|
+
`);
|
|
210
|
+
const recipe = validateRecipe(baseRecipe({
|
|
211
|
+
extensions: { clash: { kind: 'strategy', path } },
|
|
212
|
+
}));
|
|
213
|
+
await expect(loadExtensions(recipe)).rejects.toThrow(/collides with a built-in/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('rejects duplicate strategy registrations across extensions', async () => {
|
|
217
|
+
const p1 = writeExtension('one.mjs', `
|
|
218
|
+
export function register(api) { api.registerStrategy('zk', () => ({})); }
|
|
219
|
+
`);
|
|
220
|
+
const p2 = writeExtension('two.mjs', `
|
|
221
|
+
export function register(api) { api.registerStrategy('zk', () => ({})); }
|
|
222
|
+
`);
|
|
223
|
+
const recipe = validateRecipe(baseRecipe({
|
|
224
|
+
extensions: {
|
|
225
|
+
one: { kind: 'strategy', path: p1 },
|
|
226
|
+
two: { kind: 'strategy', path: p2 },
|
|
227
|
+
},
|
|
228
|
+
}));
|
|
229
|
+
await expect(loadExtensions(recipe)).rejects.toThrow(/already registered/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('wraps import failures with the extension name', async () => {
|
|
233
|
+
const path = writeExtension('boom.mjs', `throw new Error('kaboom at import');`);
|
|
234
|
+
const recipe = validateRecipe(baseRecipe({
|
|
235
|
+
extensions: { boom: { kind: 'module', path } },
|
|
236
|
+
}));
|
|
237
|
+
await expect(loadExtensions(recipe)).rejects.toThrow(/extension "boom".*failed to import/);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// buildFrameworkStrategy custom dispatch
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
describe('buildFrameworkStrategy with extensions', () => {
|
|
246
|
+
test('dispatches a custom type through the registry with full config', async () => {
|
|
247
|
+
const path = writeExtension('strat.mjs', `
|
|
248
|
+
export function register(api) {
|
|
249
|
+
api.registerStrategy('zk', (ctx) => ({
|
|
250
|
+
kind: 'zk',
|
|
251
|
+
model: ctx.model,
|
|
252
|
+
timeZone: ctx.timeZone,
|
|
253
|
+
customKnob: ctx.config.customKnob,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
`);
|
|
257
|
+
const recipe = validateRecipe(baseRecipe({
|
|
258
|
+
agent: {
|
|
259
|
+
systemPrompt: 'x',
|
|
260
|
+
strategy: { type: 'zk', customKnob: 42 },
|
|
261
|
+
},
|
|
262
|
+
extensions: { zk: { kind: 'strategy', path } },
|
|
263
|
+
})) as Recipe;
|
|
264
|
+
const registry = await loadExtensions(recipe);
|
|
265
|
+
const strategy = buildFrameworkStrategy(recipe, 'test-model', 'UTC', registry) as unknown as Record<string, unknown>;
|
|
266
|
+
expect(strategy.kind).toBe('zk');
|
|
267
|
+
expect(strategy.model).toBe('test-model');
|
|
268
|
+
expect(strategy.timeZone).toBe('UTC');
|
|
269
|
+
expect(strategy.customKnob).toBe(42);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('throws a precise error when a declared type was never registered', () => {
|
|
273
|
+
const recipe = validateRecipe(baseRecipe({
|
|
274
|
+
agent: { systemPrompt: 'x', strategy: { type: 'ghost' } },
|
|
275
|
+
extensions: { zk: { kind: 'strategy', path: '/tmp/unused.ts' } },
|
|
276
|
+
})) as Recipe;
|
|
277
|
+
expect(() => buildFrameworkStrategy(recipe, 'm', 'UTC', emptyExtensionRegistry()))
|
|
278
|
+
.toThrow(/"ghost" is not built-in and no loaded extension registered it/);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('built-in dispatch is unaffected by an empty registry', () => {
|
|
282
|
+
const recipe = validateRecipe(baseRecipe()) as Recipe;
|
|
283
|
+
const strategy = buildFrameworkStrategy(recipe, 'm', 'UTC', emptyExtensionRegistry());
|
|
284
|
+
expect(strategy).toBeDefined();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('isBuiltinStrategyType', () => {
|
|
289
|
+
test('recognizes the three built-ins and nothing else', () => {
|
|
290
|
+
expect(isBuiltinStrategyType('autobiographical')).toBe(true);
|
|
291
|
+
expect(isBuiltinStrategyType('passthrough')).toBe(true);
|
|
292
|
+
expect(isBuiltinStrategyType('frontdesk')).toBe(true);
|
|
293
|
+
expect(isBuiltinStrategyType('zk')).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { validateRecipe } from '../src/recipe.js';
|
|
3
|
+
|
|
4
|
+
function recipe(refusalHandling?: Record<string, unknown>) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'primary-summary-fallback-rejection-test',
|
|
7
|
+
agent: {
|
|
8
|
+
systemPrompt: 'sys',
|
|
9
|
+
refusalHandling,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('recipe primarySummaryFallback removal', () => {
|
|
15
|
+
test('rejects the removed primarySummaryFallback setting', () => {
|
|
16
|
+
expect(() => validateRecipe(recipe({
|
|
17
|
+
primarySummaryFallback: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
maxNewSummaries: 4,
|
|
20
|
+
requestBudgetTokens: 216_000,
|
|
21
|
+
},
|
|
22
|
+
}))).toThrow(/primarySummaryFallback was removed and is unsupported/);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -6,10 +6,19 @@ function recipe(agent: Record<string, unknown> = {}) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
describe('recipe provider validation', () => {
|
|
9
|
-
test('preserves Anthropic as the omitted provider and accepts
|
|
9
|
+
test('preserves Anthropic as the omitted provider and accepts OpenAI providers', () => {
|
|
10
10
|
expect(validateRecipe(recipe()).agent.provider).toBeUndefined();
|
|
11
11
|
expect(validateRecipe(recipe({ provider: 'openai-responses' })).agent.provider)
|
|
12
12
|
.toBe('openai-responses');
|
|
13
|
+
expect(validateRecipe(recipe({ provider: 'openai-codex' })).agent.provider)
|
|
14
|
+
.toBe('openai-codex');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('accepts Codex subscription settings', () => {
|
|
18
|
+
expect(validateRecipe(recipe({
|
|
19
|
+
provider: 'openai-codex',
|
|
20
|
+
codex: { fastMode: true },
|
|
21
|
+
})).agent.codex).toEqual({ fastMode: true });
|
|
13
22
|
});
|
|
14
23
|
|
|
15
24
|
test('accepts Responses reasoning and compaction settings', () => {
|
|
@@ -19,11 +28,13 @@ describe('recipe provider validation', () => {
|
|
|
19
28
|
reasoningEffort: 'xhigh',
|
|
20
29
|
reasoningContext: 'all_turns',
|
|
21
30
|
compactThreshold: 100_000,
|
|
31
|
+
serviceTier: 'priority',
|
|
22
32
|
},
|
|
23
33
|
})).agent.responses).toEqual({
|
|
24
34
|
reasoningEffort: 'xhigh',
|
|
25
35
|
reasoningContext: 'all_turns',
|
|
26
36
|
compactThreshold: 100_000,
|
|
37
|
+
serviceTier: 'priority',
|
|
27
38
|
});
|
|
28
39
|
});
|
|
29
40
|
|
|
@@ -32,5 +43,7 @@ describe('recipe provider validation', () => {
|
|
|
32
43
|
expect(() => validateRecipe(recipe({ responses: { reasoningEffort: 'ultra' } }))).toThrow(/reasoningEffort/);
|
|
33
44
|
expect(() => validateRecipe(recipe({ responses: { reasoningContext: 'previous_turn' } }))).toThrow(/reasoningContext/);
|
|
34
45
|
expect(() => validateRecipe(recipe({ responses: { compactThreshold: 0 } }))).toThrow(/compactThreshold/);
|
|
46
|
+
expect(() => validateRecipe(recipe({ responses: { serviceTier: 'fast' } }))).toThrow(/serviceTier/);
|
|
47
|
+
expect(() => validateRecipe(recipe({ codex: { fastMode: 'yes' } }))).toThrow(/fastMode/);
|
|
35
48
|
});
|
|
36
49
|
});
|
|
@@ -326,6 +326,20 @@ describe('WebUiModule observer flow (e2e)', () => {
|
|
|
326
326
|
const errFrame = await got('error');
|
|
327
327
|
expect(String(errFrame.message)).toContain('forbidden');
|
|
328
328
|
|
|
329
|
+
// Branch listing rides the 'messages' scope, which this grant lacks.
|
|
330
|
+
ws.send(JSON.stringify({ type: 'request-branches' }));
|
|
331
|
+
const branchErr = await new Promise<Record<string, unknown>>((resolvePromise, reject) => {
|
|
332
|
+
const t = setTimeout(() => reject(new Error('timeout waiting for request-branches refusal')), 5000);
|
|
333
|
+
const check = () => {
|
|
334
|
+
const f = frames.find((m) => m.type === 'error' && String(m.message).includes('request-branches'));
|
|
335
|
+
if (f) { clearTimeout(t); resolvePromise(f); return true; }
|
|
336
|
+
return false;
|
|
337
|
+
};
|
|
338
|
+
if (check()) return;
|
|
339
|
+
ws.addEventListener('message', () => { check(); });
|
|
340
|
+
});
|
|
341
|
+
expect(String(branchErr.message)).toContain('forbidden');
|
|
342
|
+
|
|
329
343
|
// Session cookie: health allowed, debug denied (not in scopes).
|
|
330
344
|
const cookie = `fkm_obs=${ack.sessionToken}`;
|
|
331
345
|
// healthz returns 503 (app not bound in this harness) — auth passed.
|
|
Binary file
|