mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-30 04:22:51 +00:00
* refactor(agent-core-v2): replace defineOp/Model with Event2 dispatch and replayable states - replace defineOp/Op/OpDescriptor/toEvent and defineModel/defineCheckpointedModel with Event2 subclasses: durable classes declare static durable + schema, serialize() keeps the wire record shape byte-frozen, transient classes stay off the journal - define states via defineState(...).replayable(...).on(Event2, fold): immer produceWithPatches folds with atomic prepare/commit, .undoable() trait drives prompt-submit checkpoints and context.undo, ephemeral kv keys keep imperative set - degrade IWireService to a journal adapter; the agent event dispatcher owns the pipeline (fold -> set -> appendRecord -> publish) and silent restore - align downstream event surfaces: kap-server WS envelope timestamp from event.time, klient event schemas gain time, node-sdk/acp-server/print wiring updated - rewrite gen-wire-manifest/gen-state-manifest for the unified registry and replace the op-uniqueness lint with event-uniqueness * fix(ci): repair Event2 prompt and media projections - restore prompt admission and session media materialization - align transcript, WS, SDK, and replayable media state projections - update affected tests and generated state manifest * fix(ci): update prompt event and projection expectations - update snapshots for the durable prompt.accepted event - normalize prompt.steered media in transcript projections
330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { collection, type CollectionView } from '#/_base/di/collection';
|
|
import { SyncDescriptor } from '#/_base/di/descriptors';
|
|
import { createDecorator } from '#/_base/di/instantiation';
|
|
import { InstantiationService } from '#/_base/di/instantiationService';
|
|
import { Service } from '#/_base/di/service';
|
|
import { ServiceCollection } from '#/_base/di/serviceCollection';
|
|
import { Emitter } from '#/_base/event';
|
|
import { IEventService } from '#/app/event/event';
|
|
import type { Event2 } from '#/app/event/event2';
|
|
import { EventService } from '#/app/event/eventService';
|
|
import { IEventBus } from '#/app/event/eventBus';
|
|
import { EventBusService } from '#/app/event/eventBusService';
|
|
import { DI_UNIT_CHANGED_EVENT, DiUnitChanged } from '#/debug/debugCascade';
|
|
import { DebugCascadeService } from '#/debug/debugCascadeService';
|
|
import { DebugGraphService } from '#/debug/debugGraphService';
|
|
import { DebugLedgerService } from '#/debug/debugLedgerService';
|
|
import { DebugEventsService } from '#/features/debugEvents/debugEventsService';
|
|
|
|
|
|
interface IRoot {
|
|
label: string;
|
|
}
|
|
const IRoot = createDecorator<IRoot>('debug-root');
|
|
|
|
interface IMid {
|
|
root: IRoot;
|
|
}
|
|
const IMid = createDecorator<IMid>('debug-mid');
|
|
|
|
interface IBoom {
|
|
marker: string;
|
|
}
|
|
const IBoom = createDecorator<IBoom>('debug-boom');
|
|
|
|
interface IFold {
|
|
marker: string;
|
|
}
|
|
const IFold = createDecorator<IFold>('debug-fold');
|
|
|
|
const ToolContribution = collection<{ name: string }>('debug-tool-contribution');
|
|
|
|
class Root implements IRoot {
|
|
label = 'root';
|
|
dispose(): void {}
|
|
}
|
|
|
|
class Mid implements IMid {
|
|
constructor(@IRoot readonly root: IRoot) {}
|
|
dispose(): void {}
|
|
}
|
|
|
|
class Boom implements IBoom {
|
|
marker = 'boom';
|
|
constructor() {
|
|
throw new Error('boom construction failed');
|
|
}
|
|
}
|
|
|
|
class Fold extends Service implements IFold {
|
|
marker = 'fold';
|
|
constructor(@ToolContribution readonly view: CollectionView<{ name: string }>) {
|
|
super();
|
|
}
|
|
}
|
|
|
|
class FakeEventService implements IEventService {
|
|
declare readonly _serviceBrand: undefined;
|
|
private readonly emitter = new Emitter<Event2>();
|
|
readonly onDidPublish = this.emitter.event;
|
|
readonly published: Event2[] = [];
|
|
publish(event: Event2): void {
|
|
this.published.push(event);
|
|
this.emitter.fire(event);
|
|
}
|
|
subscribe(handler: (event: Event2) => void) {
|
|
return this.emitter.event(handler);
|
|
}
|
|
}
|
|
|
|
class BusSubscriber extends Service {
|
|
constructor(@IEventBus bus: IEventBus) {
|
|
super();
|
|
this._register(bus.subscribe('debug.test', () => undefined));
|
|
}
|
|
}
|
|
const IBusSubscriber = createDecorator<BusSubscriber>('debug-bus-subscriber');
|
|
|
|
function makeTree(): { app: InstantiationService; ws: InstantiationService } {
|
|
const app = new InstantiationService(new ServiceCollection(), true);
|
|
app.debugLabel = 'app';
|
|
const ws = app.createChild(new ServiceCollection()) as InstantiationService;
|
|
ws.debugLabel = 'workspace:ws1';
|
|
return { app, ws };
|
|
}
|
|
|
|
|
|
describe('debug domain — IDebugLedgerService', () => {
|
|
it('tree() exposes units, ledger entries, and children recursively', () => {
|
|
const { app, ws } = makeTree();
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
ws.provide(IMid, new SyncDescriptor(Mid));
|
|
|
|
const tree = new DebugLedgerService(app).tree();
|
|
|
|
expect(tree.path).toBe('app');
|
|
expect(tree.label).toBe('app');
|
|
const rootUnit = tree.units.find((unit) => unit.token === 'debug-root');
|
|
expect(rootUnit).toMatchObject({
|
|
uid: expect.any(Number),
|
|
state: 'Active',
|
|
everActive: true,
|
|
inFlight: false,
|
|
});
|
|
expect(tree.ledger.map((entry) => entry.label)).toContain('provide:debug-root');
|
|
expect(tree.ledger.map((entry) => entry.label)).toContain('service:debug-root');
|
|
|
|
expect(tree.children).toHaveLength(1);
|
|
const wsNode = tree.children[0]!;
|
|
expect(wsNode.path).toBe('app/workspace:ws1');
|
|
expect(wsNode.label).toBe('workspace:ws1');
|
|
expect(wsNode.units.find((unit) => unit.token === 'debug-mid')).toMatchObject({
|
|
state: 'Active',
|
|
});
|
|
expect(() => JSON.stringify(tree)).not.toThrow();
|
|
app.dispose();
|
|
});
|
|
});
|
|
|
|
describe('debug domain — IDebugGraphService', () => {
|
|
it('graph() renders instance edges (cross-tree) and collection edges', () => {
|
|
const { app, ws } = makeTree();
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
ws.provide(IMid, new SyncDescriptor(Mid));
|
|
app.provide(IFold, new SyncDescriptor(Fold));
|
|
ws.invokeFunction((a) => a.get(IMid));
|
|
app.invokeFunction((a) => a.get(IFold));
|
|
|
|
const graph = new DebugGraphService(app).graph();
|
|
const nodeIds = new Set(graph.nodes.map((node) => node.id));
|
|
expect(nodeIds.has('app::debug-root')).toBe(true);
|
|
expect(nodeIds.has('app/workspace:ws1::debug-mid')).toBe(true);
|
|
expect(graph.nodes.find((node) => node.id === 'app::debug-root')).toMatchObject({
|
|
token: 'debug-root',
|
|
scopePath: 'app',
|
|
state: 'Active',
|
|
});
|
|
|
|
const instanceEdge = graph.edges.find(
|
|
(edge) =>
|
|
edge.from === 'app/workspace:ws1::debug-mid' &&
|
|
edge.to === 'app::debug-root' &&
|
|
edge.kind === 'instance',
|
|
);
|
|
expect(instanceEdge).toBeDefined();
|
|
|
|
const collectionEdge = graph.edges.find((edge) => edge.kind === 'collection');
|
|
expect(collectionEdge).toMatchObject({
|
|
from: 'app::debug-fold',
|
|
to: 'app::collection:debug-tool-contribution',
|
|
});
|
|
expect(nodeIds.has('app::collection:debug-tool-contribution')).toBe(true);
|
|
expect(() => JSON.stringify(graph)).not.toThrow();
|
|
app.dispose();
|
|
});
|
|
});
|
|
|
|
describe('debug domain — IDebugCascadeService', () => {
|
|
it('history() folds every scope and pending() reports waiting + failed units', () => {
|
|
const { app, ws } = makeTree();
|
|
const events = new FakeEventService();
|
|
const service = new DebugCascadeService(app, events);
|
|
|
|
ws.provide(IMid, new SyncDescriptor(Mid));
|
|
app.provide(IBoom, new SyncDescriptor(Boom));
|
|
ws.provide(IRoot, new SyncDescriptor(Root));
|
|
|
|
const history = service.history();
|
|
const scopes = new Set(history.map((entry) => entry.scopePath));
|
|
expect(scopes.has('app')).toBe(true);
|
|
expect(scopes.has('app/workspace:ws1')).toBe(true);
|
|
expect(history.every((entry) => typeof entry.reason === 'string')).toBe(true);
|
|
|
|
const pending = service.pending();
|
|
const wsGroup = pending.find((group) => group.scopePath === 'app/workspace:ws1');
|
|
expect(wsGroup?.waiting ?? []).toEqual([]);
|
|
const appGroup = pending.find((group) => group.scopePath === 'app');
|
|
expect(appGroup?.failed).toEqual([
|
|
{ token: 'debug-boom', error: 'boom construction failed' },
|
|
]);
|
|
app.dispose();
|
|
});
|
|
|
|
it('pending() reports a waiting unit with its missing dependencies', () => {
|
|
const { app, ws } = makeTree();
|
|
const service = new DebugCascadeService(app, new FakeEventService());
|
|
ws.provide(IMid, new SyncDescriptor(Mid));
|
|
|
|
const wsGroup = service.pending().find((group) => group.scopePath === 'app/workspace:ws1');
|
|
expect(wsGroup?.waiting).toEqual([{ token: 'debug-mid', missing: ['debug-root'] }]);
|
|
app.dispose();
|
|
});
|
|
|
|
it('unprovide/update/dispose triggers drive the public cascade entries', async () => {
|
|
const { app, ws } = makeTree();
|
|
const service = new DebugCascadeService(app, new FakeEventService());
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
ws.provide(IMid, new SyncDescriptor(Mid));
|
|
const firstMid = ws.invokeFunction((a) => a.get(IMid));
|
|
|
|
await service.unprovide('app', 'debug-root');
|
|
expect(app.cascade.stateOf(IRoot)).toBeUndefined();
|
|
expect(ws.cascade.stateOf(IMid)).toBe('Pending');
|
|
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
expect(ws.cascade.stateOf(IMid)).toBe('Active');
|
|
await service.update('app', 'debug-root');
|
|
const secondMid = ws.invokeFunction((a) => a.get(IMid));
|
|
expect(secondMid).not.toBe(firstMid);
|
|
expect(ws.cascade.stateOf(IMid)).toBe('Active');
|
|
|
|
await service.dispose('app', 'debug-root');
|
|
expect(app.cascade.stateOf(IRoot)).toBeUndefined();
|
|
expect(ws.cascade.stateOf(IMid)).toBe('Pending');
|
|
app.dispose();
|
|
});
|
|
|
|
it('update with a config routes through the fiber host', async () => {
|
|
const { app } = makeTree();
|
|
const service = new DebugCascadeService(app, new FakeEventService());
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
await service.update('app', 'debug-root', { tag: 1 });
|
|
expect(app.cascade.stateOf(IRoot)).toBe('Active');
|
|
app.dispose();
|
|
});
|
|
|
|
it('rejects unknown scope paths and tokens with coded errors', async () => {
|
|
const { app } = makeTree();
|
|
const service = new DebugCascadeService(app, new FakeEventService());
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
|
|
await expect(service.unprovide('app/nope', 'debug-root')).rejects.toMatchObject({
|
|
code: 'debug.scope_not_found',
|
|
});
|
|
await expect(service.update('app', 'debug-nope')).rejects.toMatchObject({
|
|
code: 'debug.token_not_found',
|
|
});
|
|
await expect(
|
|
(service.dispose as (scopePath?: string) => Promise<void>)('app'),
|
|
).rejects.toMatchObject({
|
|
code: 'debug.token_not_found',
|
|
});
|
|
app.dispose();
|
|
});
|
|
|
|
it('publishes event.di.unit_changed for live and late-joined engines until teardown', () => {
|
|
const { app } = makeTree();
|
|
const events = new FakeEventService();
|
|
const service = new DebugCascadeService(app, events);
|
|
|
|
app.provide(IRoot, new SyncDescriptor(Root));
|
|
const rootEvents = events.published.filter(
|
|
(event) => event.type === DI_UNIT_CHANGED_EVENT,
|
|
);
|
|
expect(rootEvents).toContainEqual(
|
|
expect.objectContaining({
|
|
type: DI_UNIT_CHANGED_EVENT,
|
|
payload: { scope: 'app', token: 'debug-root', state: 'Active', error: undefined },
|
|
}),
|
|
);
|
|
|
|
const ws = app.createChild(new ServiceCollection()) as InstantiationService;
|
|
ws.debugLabel = 'workspace:late';
|
|
ws.provide(IMid, new SyncDescriptor(Mid));
|
|
const wsEvents = events.published.filter(
|
|
(event): event is DiUnitChanged =>
|
|
event.type === DI_UNIT_CHANGED_EVENT &&
|
|
(event as DiUnitChanged).payload.scope === 'app/workspace:late',
|
|
);
|
|
expect(wsEvents.length).toBeGreaterThan(0);
|
|
|
|
const publishedBefore = events.published.length;
|
|
service.dispose();
|
|
app.provide(IBoom, new SyncDescriptor(Boom));
|
|
expect(events.published.length).toBe(publishedBefore);
|
|
app.dispose();
|
|
});
|
|
});
|
|
|
|
describe('debug domain — IDebugEventsService', () => {
|
|
it('subscriptions() merges unit-book labels and bus listener counts, deduped across containers', () => {
|
|
const { app } = makeTree();
|
|
app.provide(IEventBus, new SyncDescriptor(EventBusService));
|
|
app.provide(IBusSubscriber, new SyncDescriptor(BusSubscriber));
|
|
app.invokeFunction((a) => a.get(IBusSubscriber));
|
|
const bus = app.invokeFunction((a) => a.get(IEventBus));
|
|
bus.subscribe('debug.test', () => undefined);
|
|
bus.subscribe(() => undefined);
|
|
|
|
const result = new DebugEventsService(app).subscriptions();
|
|
|
|
const entry = result.subscriptions.find((s) => s.unit === 'debug-bus-subscriber');
|
|
expect(entry).toMatchObject({
|
|
scopePath: 'app',
|
|
label: 'on:debug.test',
|
|
kind: 'disposer',
|
|
uid: expect.any(Number),
|
|
});
|
|
expect(result.buses).toEqual([
|
|
{ scopePath: 'app', all: 1, perType: { 'debug.test': 2 } },
|
|
]);
|
|
expect(() => JSON.stringify(result)).not.toThrow();
|
|
app.dispose();
|
|
});
|
|
|
|
it('skips unmaterialized units and reports the global event service listener count', () => {
|
|
const { app } = makeTree();
|
|
app.provide(IBusSubscriber, new SyncDescriptor(BusSubscriber));
|
|
app.provide(IEventService, new SyncDescriptor(EventService));
|
|
const events = app.invokeFunction((a) => a.get(IEventService));
|
|
events.subscribe(() => undefined);
|
|
|
|
const result = new DebugEventsService(app).subscriptions();
|
|
|
|
expect(result.subscriptions.find((s) => s.unit === 'debug-bus-subscriber')).toBeUndefined();
|
|
expect(result.globalListeners).toBe(1);
|
|
app.dispose();
|
|
});
|
|
});
|