Files
Codeman/test/custom-model-recovery-context.test.ts
T
Codeman maintainer 4960bee779 chore: one changeset for the release, and no new em-dashes
Consolidates the four contributor changesets, and rewords the em-dashes this release
added in comments and docs. The five in test/settings-save-failure-ui.test.ts stay:
they assert existing toast text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:31:48 +01:00

92 lines
4.2 KiB
TypeScript

/**
* @fileoverview Boot recovery rebuilds a saved custom-model selection through the SAME
* context-length resolution the routes use: the model's discovered figure, falling back to
* the endpoint's `defaultContextLength` (`resolveCustomModelContextLength`). Before the
* shared helper, `_rebuildCustomModelEnv` read `modelContextLengths` alone, so a session
* on a model that only the endpoint default covers came back from a Codeman restart with
* `CLAUDE_CODE_MAX_CONTEXT_TOKENS` (or a config file's context figure) silently gone,
* re-arming exactly the window overflow the var exists to prevent.
*
* `_rebuildCustomModelEnv` reads no `this` state (it re-derives everything from the endpoint
* store and the Session it is handed), so it is driven straight off the prototype with an
* empty receiver: `restoreMuxSessions()` itself cannot be reached under vitest, the same
* constraint recorded in test/session-model-recovery.test.ts.
*/
import { afterEach, describe, expect, it } from 'vitest';
import { getDataDir } from '../src/config/instance.js';
import { writeCustomModelHosts, type CustomModelHost } from '../src/custom-model-hosts.js';
import { customModelConfigDir, removeConfigDir, type AppliedCustomModel } from '../src/custom-model-injection-apply.js';
import { Session } from '../src/session.js';
import { WebServer } from '../src/web/server.js';
import type { CustomModelBookkeeping } from '../src/types/session.js';
type RebuildFn = (session: Session, saved: CustomModelBookkeeping) => Promise<AppliedCustomModel | undefined>;
/** The real method, with an empty receiver, see the file header for why that is sound. */
function rebuildCustomModelEnv(session: Session, saved: CustomModelBookkeeping) {
const fn = (WebServer.prototype as unknown as { _rebuildCustomModelEnv: RebuildFn })._rebuildCustomModelEnv;
expect(typeof fn, '_rebuildCustomModelEnv vanished from WebServer.prototype').toBe('function');
return fn.call({}, session, saved);
}
function endpoint(overrides: Partial<CustomModelHost> = {}): CustomModelHost {
return {
id: 'ep',
label: 'Ep',
baseUrl: 'http://localhost:8080',
apiKey: 'recovery-probe-key',
models: ['qwen3'],
...overrides,
};
}
describe('_rebuildCustomModelEnv: context-length fallback on boot recovery', () => {
const configs: string[] = [];
afterEach(() => {
for (const id of configs.splice(0)) removeConfigDir(customModelConfigDir(id));
});
it('falls back to the endpoint defaultContextLength when discovery recorded no per-model figure', async () => {
await writeCustomModelHosts(getDataDir(), [endpoint({ defaultContextLength: 32768 })]);
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
configs.push(session.id);
const env = await rebuildCustomModelEnv(session, { endpointId: 'ep', modelId: 'qwen3', envKeys: [] });
expect(env?.envOverrides.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toBe('32768');
});
it('a discovered per-model figure still wins over the endpoint default', async () => {
await writeCustomModelHosts(getDataDir(), [
endpoint({ modelContextLengths: { qwen3: 131072 }, defaultContextLength: 32768 }),
]);
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
configs.push(session.id);
const env = await rebuildCustomModelEnv(session, { endpointId: 'ep', modelId: 'qwen3', envKeys: [] });
expect(env?.envOverrides.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toBe('131072');
});
it('states nothing rather than guessing when neither source knows a window', async () => {
await writeCustomModelHosts(getDataDir(), [endpoint()]);
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
configs.push(session.id);
const env = await rebuildCustomModelEnv(session, { endpointId: 'ep', modelId: 'qwen3', envKeys: [] });
expect(env?.envOverrides.CLAUDE_CODE_MAX_CONTEXT_TOKENS).toBeUndefined();
});
it('drops the selection to undefined (kept for clearing) when the endpoint no longer exists', async () => {
await writeCustomModelHosts(getDataDir(), []);
const session = new Session({ workingDir: '/tmp', mode: 'claude' });
configs.push(session.id);
await expect(rebuildCustomModelEnv(session, { endpointId: 'gone', modelId: 'qwen3', envKeys: [] })).resolves.toBe(
undefined
);
});
});