mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-29 11:04:51 +00:00
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
* feat: polish vis * feat: add 'kimi vis' command for session visualization * fix(vis): drop metadata app_version/resumed removed upstream #786 stopped recording resume version metadata, so those fields no longer exist on the metadata wire record. The vis-into-typecheck wiring caught the stale field reads after merging main; drop them from the metadata headline. * fix(vis): drop unnecessary return-await in startVisServer oxlint typescript-eslint(return-await) flags returning an awaited promise outside try/catch; return the promise directly. * fix(vis): green CI — tolerate unbuilt embedded asset + bump nix pnpmDeps hash - handleVis: wrap the embedded-SPA dynamic import in try/catch. The value module is generated at build time (prebuild); in contexts without a build (tests run pnpm test, not build) only the .d.ts type stub exists, so the runtime import throws. Tolerate it and fall back to filesystem serving. - flake.nix: update the fetchPnpmDeps hash after adding the vis-web / vis-server / vite-plugin-singlefile dependencies. * refactor(vis): drop redundant alwaysBundle in tsdown config #775's single-entry build (codeSplitting: false) already bundles everything not declared in dependencies/peerDependencies. hono / @hono/node-server (transitive via vis-server) and @moonshot-ai/vis-server (a devDependency) are all undeclared there, so they bundle by default — the explicit alwaysBundle was redundant. Verified the emitted main.mjs is still fully self-contained and 'kimi vis' serves. * fix(vis): address review — context-token resets, IPv6 url, marker-safe indexing - contextTokens now mirrors agent-core on lifecycle records: 0 on context.clear, tokensAfter on context.apply_compaction (was only updated from step.end.usage, leaving a stale live fill after a clear/compaction). - start.ts brackets IPv6 hosts in the returned url (http://[::1]:port/); hostForUrl moved to config.ts and shared with the startup banner. - compaction slice + micro-compaction blanking now index over real history entries only, so synthetic undo/clear UI markers no longer offset agent-core's compactedCount / cutoff. * chore: add changeset for kimi vis command * fix(vis): cross-platform single-file build + history-count micro clamp - build-vis-asset.mjs sets VIS_SINGLEFILE via the spawn env and runs 'vite build' directly (cross-platform), instead of the POSIX-inline-env 'build:single' script that broke on Windows cmd; removed the now-unused build:single script. Fixes the win32 build path (the asset generator runs in the kimi-code prebuild + native bundle). - context.undo now clamps the micro-compaction cutoff by history-entry count (excluding synthetic undo/clear markers) instead of messages.length, mirroring agent-core undo() -> microCompaction.reset(_history.length); a surviving marker no longer leaves the cutoff one too high and wrongly blanks a later-appended tool result. * fix(vis): run the single-file build through a shell for Windows pnpm The win32 native binary is built on Windows runners (.github/workflows/_native-build.yml), which run this generator. pnpm's launcher there is pnpm.cmd, which a bare argv exec can't resolve without a shell. Use execSync with a single command string so the platform shell (cmd on Windows) resolves the shim; a command string (not an args array) avoids the args+shell deprecation. Args are static. * fix(vis): show model-facing tool result content in the context view agent-core normalizes tool results via toolResultOutputForModel before they enter history (error -> '<system>ERROR: ...' prefix, empty -> '<system>Tool output is empty.' sentinel). The projector was using the raw ev.result.output, so the Context tab's model view showed content the model never saw for failed/empty tool calls. Replicate that normalization (the upstream helper is module-private) so the projected tool message matches what the model received.
90 lines
3.8 KiB
TypeScript
90 lines
3.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { buildAgentTree, compareAgentIds } from '../../src/lib/agent-tree';
|
|
import type { AgentInfo } from '../../src/lib/agent-record-types';
|
|
|
|
function info(overrides: Partial<AgentInfo> & Pick<AgentInfo, 'agentId'>): AgentInfo {
|
|
return {
|
|
type: 'sub',
|
|
parentAgentId: null,
|
|
homedir: `/tmp/${overrides.agentId}`,
|
|
wireExists: true,
|
|
wireRecordCount: 0,
|
|
wireProtocolVersion: '1.1',
|
|
swarmItem: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('agent-tree', () => {
|
|
it('returns single main agent as the only root', () => {
|
|
const tree = buildAgentTree([info({ agentId: 'main', type: 'main' })]);
|
|
expect(tree).toHaveLength(1);
|
|
expect(tree[0]!.agentId).toBe('main');
|
|
expect(tree[0]!.children).toEqual([]);
|
|
});
|
|
|
|
it('attaches a sub agent to its main parent', () => {
|
|
const tree = buildAgentTree([
|
|
info({ agentId: 'main', type: 'main' }),
|
|
info({ agentId: 'agent-0', type: 'sub', parentAgentId: 'main' }),
|
|
]);
|
|
expect(tree).toHaveLength(1);
|
|
expect(tree[0]!.agentId).toBe('main');
|
|
expect(tree[0]!.children).toHaveLength(1);
|
|
expect(tree[0]!.children[0]!.agentId).toBe('agent-0');
|
|
expect(tree[0]!.children[0]!.parentAgentId).toBe('main');
|
|
});
|
|
|
|
it('treats orphan parentAgentId as a root node', () => {
|
|
const tree = buildAgentTree([
|
|
info({ agentId: 'main', type: 'main' }),
|
|
info({ agentId: 'agent-0', type: 'sub', parentAgentId: 'does-not-exist' }),
|
|
]);
|
|
expect(tree).toHaveLength(2);
|
|
const ids = tree.map((n) => n.agentId).sort();
|
|
expect(ids).toEqual(['agent-0', 'main']);
|
|
// orphan is still a root, no children attached anywhere
|
|
const orphan = tree.find((n) => n.agentId === 'agent-0')!;
|
|
expect(orphan.children).toEqual([]);
|
|
});
|
|
|
|
it('sorts main as the first root regardless of input order', () => {
|
|
const tree = buildAgentTree([
|
|
info({ agentId: 'agent-1', type: 'sub', parentAgentId: 'orphan' }),
|
|
info({ agentId: 'main', type: 'main' }),
|
|
info({ agentId: 'agent-2', type: 'sub', parentAgentId: 'orphan' }),
|
|
]);
|
|
expect(tree[0]!.agentId).toBe('main');
|
|
});
|
|
|
|
it('orders agents by numeric suffix, main first (agent-2 before agent-10)', () => {
|
|
const mk = (id: string): AgentInfo => ({
|
|
agentId: id, type: id === 'main' ? 'main' : 'sub', parentAgentId: id === 'main' ? null : 'main',
|
|
homedir: '', wireExists: true, wireRecordCount: 0, wireProtocolVersion: null, swarmItem: null,
|
|
});
|
|
const tree = buildAgentTree([mk('main'), mk('agent-10'), mk('agent-2')]);
|
|
const order = [tree[0]!.agentId, ...tree[0]!.children.map((c) => c.agentId)];
|
|
expect(order).toEqual(['main', 'agent-2', 'agent-10']);
|
|
});
|
|
|
|
it('orders orphan ROOTS by numeric suffix (agent-2 before agent-10)', () => {
|
|
const tree = buildAgentTree([
|
|
info({ agentId: 'agent-10', type: 'sub', parentAgentId: 'missing' }),
|
|
info({ agentId: 'agent-2', type: 'sub', parentAgentId: 'missing' }),
|
|
]);
|
|
expect(tree.map((n) => n.agentId)).toEqual(['agent-2', 'agent-10']);
|
|
});
|
|
|
|
it('compareAgentIds is a deterministic total order when agent-N and foreign ids mix', () => {
|
|
// 'agent-1a' is a foreign/hand-edited id reachable via state.json keys or
|
|
// discoverAgentsFromDisk directory names — it does not match agent-N.
|
|
// Under the new rule: all agent-N ids sort numerically first, then any
|
|
// non-agent-N id by localeCompare. Sorting any permutation must yield the
|
|
// same order; the OLD comparator was intransitive and order-dependent here.
|
|
const forward = ['agent-2', 'agent-1a', 'agent-10'];
|
|
const reverse = [...forward].reverse();
|
|
const expected = ['agent-2', 'agent-10', 'agent-1a'];
|
|
expect([...forward].sort(compareAgentIds)).toEqual(expected);
|
|
expect([...reverse].sort(compareAgentIds)).toEqual(expected);
|
|
});
|
|
});
|