File size: 3,899 Bytes
2df2e7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ObjectId } from "mongodb";
import type { Workspace } from "$lib/types/Workspace";

const mocks = vi.hoisted(() => {
	class Rpc {
		static instances: Rpc[] = [];
		commands: Array<Record<string, unknown>> = [];
		lastSequence = 0;

		constructor() {
			Rpc.instances.push(this);
		}
		async connect() {}
		subscribe() {
			return () => undefined;
		}
		subscribeReplayGap() {
			return () => undefined;
		}
		async command(command: Record<string, unknown>) {
			this.commands.push(command);
			if (command.type === "get_state") {
				return { type: "response", success: true, data: { isStreaming: false } };
			}
			return { type: "response", success: true, data: {} };
		}
		close() {}
	}
	return { Rpc };
});

vi.mock("./PiRpcClient", () => ({ PiRpcClient: mocks.Rpc }));
vi.mock("./SandboxClient", () => ({
	SandboxClient: {
		connect: () => ({
			proxyHeaders: {},
			proxyUrlFor: () => "ws://bridge.test/ws",
		}),
	},
}));

import { AgentSessionManager } from "./AgentSessionManager";

beforeEach(() => {
	mocks.Rpc.instances.length = 0;
});

describe("AgentSessionManager dispatch fencing", () => {
	it("never sends prompt or abort when a new-turn signal was already aborted", async () => {
		const manager = AgentSessionManager.connect({ workspace: workspace(), hfToken: "test" });
		const controller = new AbortController();
		controller.abort();

		await expect(
			manager
				.runBatches("do work", {
					signal: controller.signal,
					commandId: "prompt-a",
				})
				.next()
		).rejects.toMatchObject({ name: "AbortError" });
		expect(mocks.Rpc.instances[0]?.commands).toEqual([]);
	});

	it("runs the last ownership assertion before prompt and sends nothing when it fails", async () => {
		const manager = AgentSessionManager.connect({ workspace: workspace(), hfToken: "test" });
		const assertion = vi.fn(async () => {
			throw new Error("lease lost");
		});

		await expect(
			manager
				.runBatches("do work", {
					commandId: "prompt-a",
					beforePromptDispatch: assertion,
				})
				.next()
		).rejects.toThrow("lease lost");
		expect(assertion).toHaveBeenCalledOnce();
		expect(mocks.Rpc.instances[0]?.commands).toEqual([]);
	});

	it("fences a resume abort and carries the expected prompt id", async () => {
		const manager = AgentSessionManager.connect({ workspace: workspace(), hfToken: "test" });
		const controller = new AbortController();
		controller.abort();
		const assertion = vi.fn(async () => undefined);

		await manager
			.resumeBatches({
				signal: controller.signal,
				expectedPromptId: "prompt-a",
				beforeAbortDispatch: assertion,
			})
			.next();
		await vi.waitFor(() => expect(assertion).toHaveBeenCalledOnce());
		expect(mocks.Rpc.instances[0]?.commands).toContainEqual({
			type: "abort",
			_chatUiExpectedPromptId: "prompt-a",
		});
	});

	it("detaching a stale relay never aborts pi", async () => {
		const manager = AgentSessionManager.connect({ workspace: workspace(), hfToken: "test" });
		const detached = new AbortController();
		detached.abort();

		await expect(
			manager.resumeBatches({ detachSignal: detached.signal, expectedPromptId: "prompt-a" }).next()
		).rejects.toMatchObject({ name: "AgentSessionDetachedError" });
		expect(mocks.Rpc.instances[0]?.commands).toEqual([]);
	});
});

function workspace(): Workspace {
	const now = new Date();
	return {
		_id: new ObjectId(),
		conversationId: new ObjectId(),
		sessionId: "owner",
		repo: { kind: "blank" },
		flavor: "cpu-basic",
		model: "test/model",
		thinkingLevel: "medium",
		policy: "ask",
		sandbox: {
			status: "ready",
			flavor: "cpu-basic",
			id: "sandbox",
			generation: "incarnation",
			namespace: "owner",
			baseUrl: "https://sandbox.hf.jobs",
			nonce: "nonce",
			image: "node:22",
			createdAt: now,
		},
		spend: { accruedUsd: 0 },
		createdAt: now,
		updatedAt: now,
	};
}