mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
refactor(custom-model): move the real-home link into the registry recipe
- linkFromRealHome/realHomeFallback on the configDir recipe; the apply consumer is generic and names no CLI - lstat guard so boot-recovery re-applies survive a dangling link; symlink is best-effort - resolve the real home from the session's own dirEnvVar override before the server's, threaded through both routes and boot recovery (Session.envOverrideValue) - deepseek links profiles/sessions/settings.yaml; boot-recovery test covers the session lookup
This commit is contained in:
1 parent
aece02da87
commit
c646613c56
13 files changed
+248
-54
No files matched your search
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @fileoverview Boot recovery re-derives a saved custom-model selection through
|
||||
* `_rebuildCustomModelEnv`, and the `linkFromRealHome` recipe must resolve the CLI's
|
||||
* real home from the SESSION's own env overrides first: the user who relocated dsh
|
||||
* (allowlisted `DSH_HOME`) runs their CLI against THAT home, and recovery must link the
|
||||
* regenerated config dir back to it, not to whatever the server's environment says.
|
||||
*
|
||||
* `_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 { mkdirSync, realpathSync, rmSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getDataDir } from '../src/config/instance.js';
|
||||
import { writeCustomModelHosts, type CustomModelHost } from '../src/custom-model-hosts.js';
|
||||
import { customModelConfigDir, removeConfigDir } 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<Record<string, string> | 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(): CustomModelHost {
|
||||
return {
|
||||
id: 'ep',
|
||||
label: 'Ep',
|
||||
baseUrl: 'http://localhost:8080',
|
||||
apiKey: 'recovery-probe-key',
|
||||
models: ['qwen3'],
|
||||
};
|
||||
}
|
||||
|
||||
describe('_rebuildCustomModelEnv: real-home resolution for linkFromRealHome', () => {
|
||||
const configs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const id of configs.splice(0)) removeConfigDir(customModelConfigDir(id));
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('links an isolated deepseek home from the SESSION’s own DSH_HOME, not the server’s', async () => {
|
||||
// The user who relocated dsh runs their CLI against THAT home; recovery must re-link
|
||||
// the session's generated config dir back to it even though the value never persists
|
||||
// into the server's environment. The server's DSH_HOME is emptied deliberately: with
|
||||
// the session lookup working, the fallback home is never consulted, and with it
|
||||
// broken this test gets a link into ~/.dsh rather than a pass.
|
||||
const sessionHome = join(homedir(), 'codeman-recovery-dsh-home');
|
||||
mkdirSync(join(sessionHome, 'profiles'), { recursive: true });
|
||||
vi.stubEnv('DSH_HOME', '');
|
||||
await writeCustomModelHosts(getDataDir(), [endpoint()]);
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'deepseek', envOverrides: { DSH_HOME: sessionHome } });
|
||||
configs.push(session.id);
|
||||
|
||||
const env = await rebuildCustomModelEnv(session, { endpointId: 'ep', modelId: 'qwen3', envKeys: [] });
|
||||
|
||||
expect(env?.DSH_HOME).toBe(customModelConfigDir(session.id));
|
||||
expect(realpathSync(join(env!.DSH_HOME, 'profiles'))).toBe(join(sessionHome, 'profiles'));
|
||||
rmSync(sessionHome, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -20,6 +20,8 @@ describe('DeepSeek custom endpoint isolation', () => {
|
||||
const realHome = join(homedir(), '.dsh');
|
||||
const profile = join(realHome, 'profiles', 'dsh-tui');
|
||||
mkdirSync(profile, { recursive: true });
|
||||
mkdirSync(join(realHome, 'sessions'), { recursive: true });
|
||||
writeFileSync(join(realHome, 'settings.yaml'), 'theme: dark');
|
||||
const original = join(profile, 'cordis.patch.yml');
|
||||
writeFileSync(original, 'original profile');
|
||||
vi.stubEnv('DSH_HOME', realHome);
|
||||
@@ -43,22 +45,20 @@ describe('DeepSeek custom endpoint isolation', () => {
|
||||
model: 'chosen-model',
|
||||
});
|
||||
expect(realpathSync(join(applied.configDir, 'profiles'))).toBe(join(realHome, 'profiles'));
|
||||
// The rest of the linkFromRealHome list, consumed generically: `sessions` is the
|
||||
// transcript root removeConfigDir would otherwise delete, and the user's settings.yaml
|
||||
// must keep applying through the redirect.
|
||||
expect(realpathSync(join(applied.configDir, 'sessions'))).toBe(join(realHome, 'sessions'));
|
||||
expect(realpathSync(join(applied.configDir, 'settings.yaml'))).toBe(join(realHome, 'settings.yaml'));
|
||||
// Re-apply onto the same config dir (boot recovery re-runs this every restart) with one
|
||||
// link dangling: the lstat guard must skip every existing link, broken ones included,
|
||||
// which the old existsSync check missed — recreating them would raise EEXIST.
|
||||
rmSync(join(realHome, 'sessions'), { recursive: true, force: true });
|
||||
const reapplied = applyCustomModelInjection(entry, endpoint, 'chosen-model', 'deepseek-probe', 131072);
|
||||
expect(reapplied?.envOverrides.DSH_HOME).toBe(applied.configDir);
|
||||
expect(readlinkSync(join(applied.configDir, 'sessions'))).toBe(join(realHome, 'sessions'));
|
||||
expect(readFileSync(original, 'utf8')).toBe('original profile');
|
||||
removeConfigDir(applied.configDir);
|
||||
expect(readFileSync(original, 'utf8')).toBe('original profile');
|
||||
});
|
||||
|
||||
it('gives Codex its known context and compaction budget', () => {
|
||||
const entry = getCli('codex');
|
||||
if (!entry) throw new Error('Missing Codex entry');
|
||||
const injection = buildCustomModelInjection(
|
||||
entry,
|
||||
{ id: 'local', label: 'Local', baseUrl: 'http://box' },
|
||||
'chosen-model',
|
||||
131072
|
||||
);
|
||||
if (injection.kind !== 'configDir') throw new Error('Expected isolated Codex config');
|
||||
expect(injection.files[0].content).toContain('model_context_window = 131072');
|
||||
expect(injection.files[0].content).toContain('model_auto_compact_token_limit = 98304');
|
||||
expect(injection.launchModel).toBe('chosen-model');
|
||||
});
|
||||
});
|
||||
@@ -454,6 +454,9 @@ export class MockSession extends EventEmitter {
|
||||
}
|
||||
);
|
||||
restartCli = vi.fn(async () => true);
|
||||
/** Mirrors Session.envOverrideValue; no override map here, so the custom-model
|
||||
* real-home lookup defaults to "session says nothing" (overridable per test). */
|
||||
envOverrideValue = vi.fn((_key: string): string | undefined => undefined);
|
||||
getCustomModelForPersist = vi.fn(() => this._mockCustomModel);
|
||||
/** Mirrors Session.noteReportedModel: the statusline route records the CLI's model. */
|
||||
reportedModel: { model: string; source: 'statusline' | 'screen' } | null = null;
|
||||
|
||||
Reference in new issue
Block a user