mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-27 09:34:43 +00:00
The daemon lock now records the host CLI version and entry path. When kimi web / kimi daemon finds a healthy daemon started by a different build (e.g. an older pkg.pr.new install), it warns that the running daemon lags the invoked CLI instead of silently reusing it; the new --restart flag stops the managed daemon and starts a fresh one with the current build. A daemon whose lock does not match the requested port (foreign process on the port) is never stopped.
169 lines
5.6 KiB
TypeScript
169 lines
5.6 KiB
TypeScript
import { Command } from 'commander';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
DEFAULT_DAEMON_HOST,
|
|
DEFAULT_DAEMON_ORIGIN,
|
|
DEFAULT_DAEMON_PORT,
|
|
} from '#/cli/sub/daemon';
|
|
import {
|
|
handleWebCommand,
|
|
registerWebCommand,
|
|
type WebCommandDeps,
|
|
} from '#/cli/sub/web';
|
|
|
|
function makeDeps(overrides: Partial<WebCommandDeps> = {}): {
|
|
deps: WebCommandDeps;
|
|
stdout: string[];
|
|
stderr: string[];
|
|
} {
|
|
const stdout: string[] = [];
|
|
const stderr: string[] = [];
|
|
return {
|
|
deps: {
|
|
ensureDaemonRunning: vi.fn(async () => ({
|
|
status: 'already-running' as const,
|
|
origin: DEFAULT_DAEMON_ORIGIN,
|
|
pid: 1234,
|
|
})),
|
|
ensureDaemonWebReady: vi.fn(async () => undefined),
|
|
openUrl: vi.fn(),
|
|
stdout: { write: (chunk) => stdout.push(String(chunk)) > 0 },
|
|
stderr: { write: (chunk) => stderr.push(String(chunk)) > 0 },
|
|
...overrides,
|
|
},
|
|
stdout,
|
|
stderr,
|
|
};
|
|
}
|
|
|
|
describe('kimi web', () => {
|
|
it('registers web as a subcommand', () => {
|
|
const program = new Command('kimi');
|
|
registerWebCommand(program);
|
|
|
|
const web = program.commands.find((cmd) => cmd.name() === 'web');
|
|
expect(web).toBeDefined();
|
|
expect(web?.options.some((option) => option.long === '--daemon-host')).toBe(true);
|
|
});
|
|
|
|
it('ensures the local daemon and opens the daemon-hosted web UI by default', async () => {
|
|
const { deps, stdout, stderr } = makeDeps();
|
|
|
|
await handleWebCommand({}, deps);
|
|
|
|
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
|
|
{
|
|
host: DEFAULT_DAEMON_HOST,
|
|
port: DEFAULT_DAEMON_PORT,
|
|
logLevel: 'info',
|
|
debugEndpoints: false,
|
|
restart: false,
|
|
},
|
|
expect.any(Function),
|
|
);
|
|
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith(DEFAULT_DAEMON_ORIGIN);
|
|
expect(deps.openUrl).toHaveBeenCalledWith(DEFAULT_DAEMON_ORIGIN);
|
|
expect(stderr.join('')).toBe('');
|
|
expect(stdout.join('')).toContain(`Kimi web: ${DEFAULT_DAEMON_ORIGIN}`);
|
|
});
|
|
|
|
it('opens the provided daemon host directly without starting local services', async () => {
|
|
const { deps } = makeDeps();
|
|
|
|
await handleWebCommand({ daemonHost: 'http://192.168.97.91:7878' }, deps);
|
|
|
|
expect(deps.ensureDaemonRunning).not.toHaveBeenCalled();
|
|
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith('http://192.168.97.91:7878');
|
|
expect(deps.openUrl).toHaveBeenCalledWith('http://192.168.97.91:7878');
|
|
});
|
|
|
|
it('uses --port as the local daemon start port when --daemon-host is absent', async () => {
|
|
const { deps } = makeDeps({
|
|
ensureDaemonRunning: vi.fn(async () => ({
|
|
status: 'already-running' as const,
|
|
origin: 'http://127.0.0.1:8899',
|
|
pid: 1234,
|
|
})),
|
|
});
|
|
|
|
await handleWebCommand({ host: '0.0.0.0', port: '8899' }, deps);
|
|
|
|
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
|
|
{
|
|
host: '0.0.0.0',
|
|
port: 8899,
|
|
logLevel: 'info',
|
|
debugEndpoints: false,
|
|
restart: false,
|
|
},
|
|
expect.any(Function),
|
|
);
|
|
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith('http://127.0.0.1:8899');
|
|
expect(deps.openUrl).toHaveBeenCalledWith('http://127.0.0.1:8899');
|
|
});
|
|
|
|
it('forwards --restart to the daemon resolver', async () => {
|
|
const { deps } = makeDeps();
|
|
|
|
await handleWebCommand({ restart: true }, deps);
|
|
|
|
expect(deps.ensureDaemonRunning).toHaveBeenCalledWith(
|
|
expect.objectContaining({ restart: true }),
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it('prints daemon startup trace from the local daemon resolver', async () => {
|
|
const { deps, stdout } = makeDeps({
|
|
ensureDaemonRunning: vi.fn(async (_options, report) => {
|
|
report?.('checking requested daemon at http://127.0.0.1:7999');
|
|
report?.('found live daemon lock (pid 4321, port 7880, started 2026-06-10T00:00:00.000Z)');
|
|
report?.('locked daemon is healthy; reusing http://127.0.0.1:7880');
|
|
return {
|
|
status: 'already-running' as const,
|
|
origin: 'http://127.0.0.1:7880',
|
|
pid: 4321,
|
|
};
|
|
}),
|
|
});
|
|
|
|
await handleWebCommand({ port: '7999', open: false }, deps);
|
|
|
|
expect(stdout.join('')).toContain(
|
|
'Daemon startup: checking requested daemon at http://127.0.0.1:7999',
|
|
);
|
|
expect(stdout.join('')).toContain(
|
|
'Daemon startup: found live daemon lock (pid 4321, port 7880, started 2026-06-10T00:00:00.000Z)',
|
|
);
|
|
expect(stdout.join('')).toContain(
|
|
'Daemon startup: locked daemon is healthy; reusing http://127.0.0.1:7880',
|
|
);
|
|
expect(stdout.join('')).toContain('Kimi web: http://127.0.0.1:7880');
|
|
});
|
|
|
|
it('respects --no-open while still printing the daemon-hosted web URL', async () => {
|
|
const { deps, stdout } = makeDeps();
|
|
|
|
await handleWebCommand({ daemonHost: 'http://example.test:9000/api/v1/', open: false }, deps);
|
|
|
|
expect(deps.ensureDaemonRunning).not.toHaveBeenCalled();
|
|
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith('http://example.test:9000');
|
|
expect(deps.openUrl).not.toHaveBeenCalled();
|
|
expect(stdout.join('')).toContain('Kimi web: http://example.test:9000');
|
|
});
|
|
|
|
it('does not open a daemon that does not serve the web UI', async () => {
|
|
const { deps, stdout } = makeDeps({
|
|
ensureDaemonWebReady: vi.fn(async () => {
|
|
throw new Error('Daemon at http://127.0.0.1:7878 does not serve the Kimi web UI.');
|
|
}),
|
|
});
|
|
|
|
await expect(handleWebCommand({}, deps)).rejects.toThrow('does not serve the Kimi web UI');
|
|
|
|
expect(deps.ensureDaemonWebReady).toHaveBeenCalledWith(DEFAULT_DAEMON_ORIGIN);
|
|
expect(deps.openUrl).not.toHaveBeenCalled();
|
|
expect(stdout.join('')).toBe('');
|
|
});
|
|
});
|