feat: add Pi conversation worker pool
This commit is contained in:
531
tests/unit/pi-worker-pool.test.ts
Normal file
531
tests/unit/pi-worker-pool.test.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { PrepareConversationInput } from '../../electron/coding-runtime/contracts';
|
||||
import {
|
||||
PiProcessBudget,
|
||||
PiWorkerPool,
|
||||
type PiConversationWorker,
|
||||
type PiWorkerOpenResult,
|
||||
} from '../../electron/coding-runtime/pi/worker-pool';
|
||||
import type { PiProcessError } from '../../electron/coding-runtime/pi/process-errors';
|
||||
import { PiProcessError as PiProcessFailure } from '../../electron/coding-runtime/pi/process-errors';
|
||||
import type { PiRpcCommand, PiRpcEvent } from '../../electron/coding-runtime/pi/rpc-client';
|
||||
import type { PiRuntimeTelemetryEvent } from '../../electron/coding-runtime/pi/telemetry';
|
||||
|
||||
const MODEL = {
|
||||
model: {
|
||||
accountId: 'account-a',
|
||||
modelId: 'model-a',
|
||||
thinkingLevel: 'medium' as const,
|
||||
},
|
||||
modelResolution: 'resolved' as const,
|
||||
};
|
||||
|
||||
function conversation(conversationId: string): PrepareConversationInput {
|
||||
return {
|
||||
conversationId,
|
||||
projectId: 'project-a',
|
||||
agentId: 'agent-a',
|
||||
title: conversationId,
|
||||
model: MODEL,
|
||||
};
|
||||
}
|
||||
|
||||
function deferred(): { promise: Promise<void>; resolve(): void } {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((done) => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
class FakeWorker implements PiConversationWorker {
|
||||
readonly generation = 1;
|
||||
readonly requests: PiRpcCommand[] = [];
|
||||
stopped = false;
|
||||
private readonly eventListeners = new Set<(event: PiRpcEvent) => void>();
|
||||
private readonly invalidationListeners = new Set<(error: PiProcessError) => void>();
|
||||
|
||||
constructor(readonly id: string) {}
|
||||
|
||||
async request() {
|
||||
this.requests.push(arguments[0] as PiRpcCommand);
|
||||
return { type: 'response' as const, id: 'fake', success: true };
|
||||
}
|
||||
|
||||
subscribe(listener: (event: PiRpcEvent) => void): () => void {
|
||||
this.eventListeners.add(listener);
|
||||
return () => this.eventListeners.delete(listener);
|
||||
}
|
||||
|
||||
subscribeInvalidation(listener: (error: PiProcessError) => void): () => void {
|
||||
this.invalidationListeners.add(listener);
|
||||
return () => this.invalidationListeners.delete(listener);
|
||||
}
|
||||
|
||||
emit(event: PiRpcEvent): void {
|
||||
for (const listener of this.eventListeners) listener(event);
|
||||
}
|
||||
|
||||
invalidate(error = new PiProcessFailure('PI_RPC_EXITED', 'fake worker crashed')): void {
|
||||
for (const listener of this.invalidationListeners) listener(error);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.stopped = true;
|
||||
return { mode: 'stdin-close' as const, code: 0, signal: null };
|
||||
}
|
||||
}
|
||||
|
||||
describe('Pi worker pool', () => {
|
||||
it('single-flights prepare per Conversation and never shares its worker with another Conversation', async () => {
|
||||
const gate = deferred();
|
||||
const opened: string[] = [];
|
||||
const pool = new PiWorkerPool({
|
||||
openWorker: async ({ conversation: input }): Promise<PiWorkerOpenResult> => {
|
||||
opened.push(input.conversationId);
|
||||
await gate.promise;
|
||||
return {
|
||||
worker: new FakeWorker(`worker-${input.conversationId}`),
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
maxIdle: 6,
|
||||
});
|
||||
|
||||
const first = pool.prepare(conversation('conversation-a'));
|
||||
const duplicate = pool.prepare(conversation('conversation-a'));
|
||||
const other = pool.prepare(conversation('conversation-b'));
|
||||
await expect.poll(() => opened).toEqual(['conversation-a', 'conversation-b']);
|
||||
gate.resolve();
|
||||
|
||||
const [firstState, duplicateState, otherState] = await Promise.all([first, duplicate, other]);
|
||||
expect(firstState).toEqual(duplicateState);
|
||||
expect(firstState).toMatchObject({
|
||||
conversationId: 'conversation-a',
|
||||
workerId: 'worker-conversation-a',
|
||||
state: 'ready',
|
||||
generation: 1,
|
||||
});
|
||||
expect(otherState).toMatchObject({
|
||||
conversationId: 'conversation-b',
|
||||
workerId: 'worker-conversation-b',
|
||||
state: 'ready',
|
||||
generation: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('starts only four top-level runs and advances the remaining queue fairly on agent_settled', async () => {
|
||||
const workers = new Map<string, FakeWorker>();
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 6,
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}`);
|
||||
workers.set(input.conversationId, worker);
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
const ids = ['a', 'b', 'c', 'd', 'e', 'f'].map((id) => `conversation-${id}`);
|
||||
await Promise.all(ids.map((id) => pool.prepare(conversation(id))));
|
||||
|
||||
const runs = ids.map((conversationId, index) => pool.startTopLevel({
|
||||
conversationId,
|
||||
runId: `run-${index + 1}`,
|
||||
command: { type: 'prompt', message: conversationId },
|
||||
}));
|
||||
|
||||
expect(runs.map((run) => run.queuePosition)).toEqual([undefined, undefined, undefined, undefined, 1, 2]);
|
||||
await expect.poll(() => ids.map((id) => workers.get(id)!.requests.length))
|
||||
.toEqual([1, 1, 1, 1, 0, 0]);
|
||||
workers.get('conversation-a')!.emit({ type: 'agent_end' });
|
||||
expect(workers.get('conversation-e')!.requests).toHaveLength(0);
|
||||
|
||||
workers.get('conversation-a')!.emit({ type: 'agent_settled' });
|
||||
await expect.poll(() => workers.get('conversation-e')!.requests.length).toBe(1);
|
||||
expect(workers.get('conversation-f')!.requests).toHaveLength(0);
|
||||
workers.get('conversation-b')!.emit({ type: 'agent_settled' });
|
||||
await expect.poll(() => workers.get('conversation-f')!.requests.length).toBe(1);
|
||||
});
|
||||
|
||||
it('evicts only the least-recent idle worker and reopens it on demand', async () => {
|
||||
const workers = new Map<string, FakeWorker[]>();
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 2,
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}-${(workers.get(input.conversationId)?.length ?? 0) + 1}`);
|
||||
workers.set(input.conversationId, [...(workers.get(input.conversationId) ?? []), worker]);
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await pool.prepare(conversation('conversation-a'));
|
||||
await pool.prepare(conversation('conversation-b'));
|
||||
await pool.prepare(conversation('conversation-c'));
|
||||
await expect.poll(() => workers.get('conversation-a')![0]!.stopped).toBe(true);
|
||||
expect(pool.getState('conversation-a')).toBeNull();
|
||||
expect(pool.getState('conversation-b')?.state).toBe('ready');
|
||||
expect(pool.getState('conversation-c')?.state).toBe('ready');
|
||||
|
||||
const reopened = await pool.prepare(conversation('conversation-a'));
|
||||
expect(reopened).toMatchObject({
|
||||
workerId: 'worker-conversation-a-2',
|
||||
generation: 2,
|
||||
state: 'ready',
|
||||
});
|
||||
});
|
||||
|
||||
it('shares a fair total-process budget and starts the next worker only after a lease is released', async () => {
|
||||
const processBudget = new PiProcessBudget(2);
|
||||
const opened: string[] = [];
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 3,
|
||||
processBudget,
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
opened.push(input.conversationId);
|
||||
return {
|
||||
worker: new FakeWorker(`worker-${input.conversationId}`),
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
pool.prepare(conversation('conversation-a')),
|
||||
pool.prepare(conversation('conversation-b')),
|
||||
]);
|
||||
const third = pool.prepare(conversation('conversation-c'));
|
||||
await expect.poll(() => processBudget.waitingCount).toBe(1);
|
||||
expect(opened).toEqual(['conversation-a', 'conversation-b']);
|
||||
|
||||
await pool.dispose('conversation-a');
|
||||
await expect(third).resolves.toMatchObject({ conversationId: 'conversation-c', state: 'ready' });
|
||||
expect(opened).toEqual(['conversation-a', 'conversation-b', 'conversation-c']);
|
||||
expect(processBudget.activeCount).toBe(2);
|
||||
|
||||
await pool.shutdown();
|
||||
expect(processBudget.activeCount).toBe(0);
|
||||
});
|
||||
|
||||
it('never evicts a running worker when the warm-idle LRU exceeds its cap', async () => {
|
||||
const workers = new Map<string, FakeWorker>();
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 1,
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}`);
|
||||
workers.set(input.conversationId, worker);
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await pool.prepare(conversation('conversation-running'));
|
||||
const running = pool.startTopLevel({
|
||||
conversationId: 'conversation-running',
|
||||
runId: 'run-running',
|
||||
command: { type: 'prompt', message: 'keep alive' },
|
||||
});
|
||||
await running.accepted;
|
||||
await pool.prepare(conversation('conversation-old-idle'));
|
||||
await pool.prepare(conversation('conversation-new-idle'));
|
||||
|
||||
expect(workers.get('conversation-running')!.stopped).toBe(false);
|
||||
expect(pool.getState('conversation-running')).toMatchObject({ state: 'running' });
|
||||
expect(workers.get('conversation-old-idle')!.stopped).toBe(true);
|
||||
expect(pool.getState('conversation-new-idle')).toMatchObject({ state: 'ready' });
|
||||
});
|
||||
|
||||
it('cleans only the crashed generation and releases its permit for the next Conversation', async () => {
|
||||
const workers = new Map<string, FakeWorker>();
|
||||
const pool = new PiWorkerPool({
|
||||
maxRunning: 2,
|
||||
maxIdle: 3,
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}`);
|
||||
workers.set(input.conversationId, worker);
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await Promise.all(['a', 'b', 'c'].map((id) => pool.prepare(conversation(`conversation-${id}`))));
|
||||
pool.startTopLevel({ conversationId: 'conversation-a', runId: 'run-a', command: { type: 'prompt', message: 'a' } });
|
||||
pool.startTopLevel({ conversationId: 'conversation-b', runId: 'run-b', command: { type: 'prompt', message: 'b' } });
|
||||
pool.startTopLevel({ conversationId: 'conversation-c', runId: 'run-c', command: { type: 'prompt', message: 'c' } });
|
||||
await expect.poll(() => workers.get('conversation-a')!.requests.length).toBe(1);
|
||||
|
||||
const cancelled: string[] = [];
|
||||
for (const kind of ['command', 'interaction', 'child'] as const) {
|
||||
pool.trackGenerationResource({
|
||||
conversationId: 'conversation-a',
|
||||
kind,
|
||||
id: `${kind}-a`,
|
||||
cancel: () => { cancelled.push(kind); },
|
||||
});
|
||||
}
|
||||
pool.trackGenerationResource({
|
||||
conversationId: 'conversation-b',
|
||||
kind: 'child',
|
||||
id: 'child-b',
|
||||
cancel: () => { cancelled.push('other'); },
|
||||
});
|
||||
|
||||
workers.get('conversation-a')!.invalidate();
|
||||
|
||||
expect(cancelled.sort()).toEqual(['child', 'command', 'interaction']);
|
||||
expect(pool.getState('conversation-a')).toMatchObject({ state: 'crashed', generation: 1 });
|
||||
expect(pool.getState('conversation-b')).toMatchObject({ state: 'running', generation: 1 });
|
||||
await expect.poll(() => workers.get('conversation-c')!.requests.length).toBe(1);
|
||||
expect(cancelled).not.toContain('other');
|
||||
});
|
||||
|
||||
it('rebuilds stale idle workers before prompt and lets running workers settle first', async () => {
|
||||
const workers = new Map<string, FakeWorker[]>();
|
||||
const revisions: Array<{ conversationId: string; provider: number; resources: number }> = [];
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 4,
|
||||
openWorker: async ({ conversation: input, revision }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}-${(workers.get(input.conversationId)?.length ?? 0) + 1}`);
|
||||
workers.set(input.conversationId, [...(workers.get(input.conversationId) ?? []), worker]);
|
||||
revisions.push({ conversationId: input.conversationId, ...revision });
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await Promise.all([
|
||||
pool.prepare(conversation('conversation-running')),
|
||||
pool.prepare(conversation('conversation-idle')),
|
||||
]);
|
||||
pool.startTopLevel({
|
||||
conversationId: 'conversation-running',
|
||||
runId: 'run-running',
|
||||
command: { type: 'prompt', message: 'running' },
|
||||
});
|
||||
await expect.poll(() => workers.get('conversation-running')![0]!.requests.length).toBe(1);
|
||||
|
||||
pool.markProviderStale();
|
||||
pool.startTopLevel({
|
||||
conversationId: 'conversation-idle',
|
||||
runId: 'run-idle',
|
||||
command: { type: 'prompt', message: 'idle' },
|
||||
});
|
||||
|
||||
await expect.poll(() => workers.get('conversation-idle')?.length).toBe(2);
|
||||
expect(workers.get('conversation-idle')![0]!.stopped).toBe(true);
|
||||
await expect.poll(() => workers.get('conversation-idle')![1]!.requests.length).toBe(1);
|
||||
expect(workers.get('conversation-running')).toHaveLength(1);
|
||||
expect(workers.get('conversation-running')![0]!.stopped).toBe(false);
|
||||
|
||||
workers.get('conversation-running')![0]!.emit({ type: 'agent_settled' });
|
||||
await expect.poll(() => workers.get('conversation-running')?.length).toBe(2);
|
||||
expect(workers.get('conversation-running')![0]!.stopped).toBe(true);
|
||||
expect(revisions).toEqual(expect.arrayContaining([
|
||||
{ conversationId: 'conversation-idle', provider: 2, resources: 1 },
|
||||
{ conversationId: 'conversation-running', provider: 2, resources: 1 },
|
||||
]));
|
||||
});
|
||||
|
||||
it('rejects queued work and stops every parent worker during app shutdown', async () => {
|
||||
const workers: FakeWorker[] = [];
|
||||
const pool = new PiWorkerPool({
|
||||
maxRunning: 1,
|
||||
maxIdle: 3,
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}`);
|
||||
workers.push(worker);
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await Promise.all(['a', 'b', 'c'].map((id) => pool.prepare(conversation(`conversation-${id}`))));
|
||||
pool.startTopLevel({ conversationId: 'conversation-a', runId: 'run-a', command: { type: 'prompt', message: 'a' } });
|
||||
const queued = pool.startTopLevel({
|
||||
conversationId: 'conversation-b',
|
||||
runId: 'run-b',
|
||||
command: { type: 'prompt', message: 'b' },
|
||||
});
|
||||
let childCancelled = false;
|
||||
pool.trackGenerationResource({
|
||||
conversationId: 'conversation-a',
|
||||
kind: 'child',
|
||||
id: 'child-a',
|
||||
cancel: () => { childCancelled = true; },
|
||||
});
|
||||
|
||||
await pool.shutdown();
|
||||
|
||||
await expect(queued.accepted).rejects.toThrow('shutting down');
|
||||
expect(childCancelled).toBe(true);
|
||||
expect(workers.every((worker) => worker.stopped)).toBe(true);
|
||||
expect(pool.getState('conversation-a')).toBeNull();
|
||||
await expect(pool.prepare(conversation('conversation-after-quit')))
|
||||
.rejects.toThrow('shutting down');
|
||||
});
|
||||
|
||||
it('waits for an in-flight fork open and stops that process before shutdown completes', async () => {
|
||||
const forkGate = deferred();
|
||||
const workers: FakeWorker[] = [];
|
||||
let forkOpenStarted = false;
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 3,
|
||||
openWorker: async ({ conversation: input, existingSession }) => {
|
||||
if (input.conversationId === 'conversation-fork') {
|
||||
forkOpenStarted = true;
|
||||
await forkGate.promise;
|
||||
}
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}`);
|
||||
workers.push(worker);
|
||||
return {
|
||||
worker,
|
||||
session: existingSession ?? {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await pool.prepare(conversation('conversation-source'));
|
||||
const fork = pool.fork('conversation-source', conversation('conversation-fork'));
|
||||
await expect.poll(() => forkOpenStarted).toBe(true);
|
||||
const forkOutcome = fork.then(
|
||||
() => 'resolved',
|
||||
(error: unknown) => error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
let shutdownCompleted = false;
|
||||
const shutdown = pool.shutdown().then(() => { shutdownCompleted = true; });
|
||||
await Promise.resolve();
|
||||
expect(shutdownCompleted).toBe(false);
|
||||
|
||||
forkGate.resolve();
|
||||
await shutdown;
|
||||
expect(await forkOutcome).toContain('shutting down');
|
||||
expect(workers).toHaveLength(2);
|
||||
expect(workers.every((worker) => worker.stopped)).toBe(true);
|
||||
});
|
||||
|
||||
it('recovers the target session with a new generation and disposes no sibling worker', async () => {
|
||||
const workers = new Map<string, FakeWorker[]>();
|
||||
const pool = new PiWorkerPool({
|
||||
maxIdle: 4,
|
||||
openWorker: async ({ conversation: input, existingSession }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}-${(workers.get(input.conversationId)?.length ?? 0) + 1}`);
|
||||
workers.set(input.conversationId, [...(workers.get(input.conversationId) ?? []), worker]);
|
||||
return {
|
||||
worker,
|
||||
session: existingSession ?? {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await Promise.all([
|
||||
pool.prepare(conversation('conversation-a')),
|
||||
pool.prepare(conversation('conversation-b')),
|
||||
]);
|
||||
workers.get('conversation-a')![0]!.invalidate();
|
||||
|
||||
const recovered = await pool.recover('conversation-a');
|
||||
expect(recovered).toMatchObject({
|
||||
workerId: 'worker-conversation-a-2',
|
||||
generation: 2,
|
||||
state: 'ready',
|
||||
session: { piSessionId: 'session-conversation-a', sessionKey: 'key-conversation-a' },
|
||||
});
|
||||
expect(workers.get('conversation-a')![0]!.stopped).toBe(true);
|
||||
expect(workers.get('conversation-b')![0]!.stopped).toBe(false);
|
||||
|
||||
await pool.dispose('conversation-a');
|
||||
expect(workers.get('conversation-a')![1]!.stopped).toBe(true);
|
||||
expect(pool.getState('conversation-a')).toBeNull();
|
||||
expect(pool.getState('conversation-b')).toMatchObject({ state: 'ready', generation: 1 });
|
||||
});
|
||||
|
||||
it('records privacy-safe queue wait and RPC prompt acceptance spans', async () => {
|
||||
let now = 0;
|
||||
const telemetry: PiRuntimeTelemetryEvent[] = [];
|
||||
const workers = new Map<string, FakeWorker>();
|
||||
const pool = new PiWorkerPool({
|
||||
maxRunning: 1,
|
||||
maxIdle: 2,
|
||||
now: () => now,
|
||||
onTelemetry: (event) => telemetry.push(event),
|
||||
openWorker: async ({ conversation: input }) => {
|
||||
const worker = new FakeWorker(`worker-${input.conversationId}`);
|
||||
workers.set(input.conversationId, worker);
|
||||
return {
|
||||
worker,
|
||||
session: {
|
||||
piSessionId: `session-${input.conversationId}`,
|
||||
sessionKey: `key-${input.conversationId}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
await Promise.all([
|
||||
pool.prepare(conversation('f47ac10b-58cc-4372-a567-0e02b2c3d479')),
|
||||
pool.prepare(conversation('8b1a9953-c461-4d88-9c3e-7e1f8f3f2c11')),
|
||||
]);
|
||||
const first = pool.startTopLevel({
|
||||
conversationId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
runId: 'run-first-1234567890',
|
||||
command: { type: 'prompt', message: 'private first prompt' },
|
||||
});
|
||||
now = 5;
|
||||
const second = pool.startTopLevel({
|
||||
conversationId: '8b1a9953-c461-4d88-9c3e-7e1f8f3f2c11',
|
||||
runId: 'run-second-1234567890',
|
||||
command: { type: 'prompt', message: 'private second prompt' },
|
||||
});
|
||||
await first.accepted;
|
||||
now = 25;
|
||||
workers.get('f47ac10b-58cc-4372-a567-0e02b2c3d479')!.emit({ type: 'agent_settled' });
|
||||
await second.accepted;
|
||||
|
||||
expect(telemetry.map(({ milestone }) => milestone)).toEqual([
|
||||
'prompt.accepted',
|
||||
'worker.queue_wait',
|
||||
'prompt.accepted',
|
||||
]);
|
||||
expect(telemetry[1]).toMatchObject({ durationMs: 20, workerGeneration: 1, cold: true });
|
||||
const serialized = JSON.stringify(telemetry);
|
||||
expect(serialized).not.toContain('private first prompt');
|
||||
expect(serialized).not.toContain('private second prompt');
|
||||
expect(serialized).not.toContain('f47ac10b-58cc-4372-a567-0e02b2c3d479');
|
||||
expect(serialized).not.toContain('8b1a9953-c461-4d88-9c3e-7e1f8f3f2c11');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user