@hachej/boring-agent 0.1.90 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4PKGVPJA.js → chunk-2GBHYJVC.js} +27 -5
- package/dist/{chunk-5CYVBVP3.js → chunk-BHH5NH7P.js} +4766 -2153
- package/dist/{chunk-BEFJNMKQ.js → chunk-CJ2BW2W2.js} +23 -8
- package/dist/{chunk-F7ZS27HE.js → chunk-IWBC6SF7.js} +93 -19
- package/dist/chunk-NITBKFG6.js +1246 -0
- package/dist/{chunk-JEDRQAR5.js → chunk-PZC76CIA.js} +7 -1
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +2 -2
- package/dist/{createHarness-UCCGMVQJ.js → createHarness-PMYRKDE4.js} +2 -2
- package/dist/errors-BEmw9HMp.d.ts +200 -0
- package/dist/front/index.d.ts +20 -4
- package/dist/front/index.js +172 -44
- package/dist/{harness-Rbkdpvse.d.ts → harness-Bt_GMNYR.d.ts} +2 -2
- package/dist/{workspaceAgentDispatcher-50Uzuxhs.d.ts → lease-2aXepSBL.d.ts} +266 -57
- package/dist/{protocol-CDA4bcP9.d.ts → middleware-CzW8n7B7.d.ts} +20 -2
- package/dist/piChatEvent-DBO3M5lu.d.ts +367 -0
- package/dist/{piChatSessionService-DDKPOj4x.d.ts → piChatSessionService-BaaVWke7.d.ts} +5 -1
- package/dist/{sandbox-OEG_l9GE.d.ts → sandbox-DAD8cWQR.d.ts} +14 -1
- package/dist/server/agent-host/testing/gatewayConformance.d.ts +41 -0
- package/dist/server/agent-host/testing/gatewayConformance.js +465 -0
- package/dist/server/index.d.ts +451 -528
- package/dist/server/index.js +43 -9
- package/dist/server/worker/index.d.ts +2 -2
- package/dist/server/worker/index.js +7 -7
- package/dist/shared/index.d.ts +109 -41
- package/dist/shared/index.js +65 -282
- package/dist/types-jcMapYr9.d.ts +759 -0
- package/docs/ACCESSIBILITY.md +4 -5
- package/docs/API.md +3 -0
- package/docs/AUTHORING.md +68 -0
- package/docs/ERROR_CODES.md +27 -6
- package/docs/README.md +1 -0
- package/package.json +17 -8
- package/dist/chunk-CFQQXJKK.js +0 -385
- package/dist/chunk-DGRKMMXO.js +0 -8
- package/dist/piChatEvent-CHgln6yx.d.ts +0 -367
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_GATEWAY_ERROR_CODES,
|
|
3
|
+
AgentGatewayError,
|
|
4
|
+
AgentGatewayErrorCode
|
|
5
|
+
} from "../../../chunk-NITBKFG6.js";
|
|
6
|
+
import "../../../chunk-AQBXNPMD.js";
|
|
7
|
+
import "../../../chunk-WSQ5QNIY.js";
|
|
8
|
+
import "../../../chunk-PZC76CIA.js";
|
|
9
|
+
import "../../../chunk-2GBHYJVC.js";
|
|
10
|
+
|
|
11
|
+
// src/server/agent-host/testing/gatewayConformance.ts
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
async function expectCode(operation, code) {
|
|
14
|
+
await expect(operation).rejects.toMatchObject({ code });
|
|
15
|
+
}
|
|
16
|
+
async function createSession(fixture, scope, requestId, agentTypeId = "alpha") {
|
|
17
|
+
return fixture.gateway.createSession({
|
|
18
|
+
scope,
|
|
19
|
+
agentTypeId,
|
|
20
|
+
requestId,
|
|
21
|
+
title: requestId
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function gatewayConformance(options) {
|
|
25
|
+
describe(`AgentGateway conformance (replay ${options.replayLevel}, pagination ${options.paginationLevel})`, () => {
|
|
26
|
+
it("requires issuer provenance and current membership on every operation", async () => {
|
|
27
|
+
const fixture = await options.createFixture();
|
|
28
|
+
const scope = fixture.issueScope();
|
|
29
|
+
const ref = await createSession(fixture, scope, "create-valid");
|
|
30
|
+
const foreign = fixture.issueScope({ issuer: "foreign" });
|
|
31
|
+
const spread = { ...scope };
|
|
32
|
+
const jsonCopy = JSON.parse(JSON.stringify(scope));
|
|
33
|
+
const forged = {
|
|
34
|
+
workspaceScopeId: scope.workspaceScopeId,
|
|
35
|
+
authSubjectId: scope.authSubjectId
|
|
36
|
+
};
|
|
37
|
+
fixture.revoke(scope);
|
|
38
|
+
for (const [index, denied] of [foreign, spread, jsonCopy, forged, scope].entries()) {
|
|
39
|
+
await expectCode(fixture.gateway.listAgents({ scope: denied }), "AGENT_SCOPE_DENIED");
|
|
40
|
+
await expectCode(fixture.gateway.listSessions({ scope: denied }), "AGENT_SCOPE_DENIED");
|
|
41
|
+
await expectCode(fixture.gateway.createSession({ scope: denied, agentTypeId: "alpha", requestId: `denied-create-${index}` }), "AGENT_SCOPE_DENIED");
|
|
42
|
+
await expectCode(fixture.gateway.connectSession({ scope: denied, ref }), "AGENT_SCOPE_DENIED");
|
|
43
|
+
await expectCode(fixture.gateway.readSessionState({ scope: denied, ref }), "AGENT_SCOPE_DENIED");
|
|
44
|
+
await expectCode(fixture.gateway.renameSession({ scope: denied, ref, requestId: `denied-rename-${index}`, title: "x" }), "AGENT_SCOPE_DENIED");
|
|
45
|
+
await expectCode(fixture.gateway.deleteSession({ scope: denied, ref, requestId: `denied-delete-${index}` }), "AGENT_SCOPE_DENIED");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
it("re-verifies membership for every command on an open connection", async () => {
|
|
49
|
+
const fixture = await options.createFixture();
|
|
50
|
+
const scope = fixture.issueScope();
|
|
51
|
+
const ref = await createSession(fixture, scope, "open-command-session");
|
|
52
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
53
|
+
fixture.revoke(scope);
|
|
54
|
+
await expectCode(connection.send({ kind: "prompt", requestId: "p", clientNonce: "n", content: "hello" }), "AGENT_SCOPE_DENIED");
|
|
55
|
+
await expectCode(connection.interrupt({ requestId: "i" }), "AGENT_SCOPE_DENIED");
|
|
56
|
+
await expectCode(connection.stop({ requestId: "s" }), "AGENT_SCOPE_DENIED");
|
|
57
|
+
await expectCode(connection.clearQueue({ requestId: "q" }), "AGENT_SCOPE_DENIED");
|
|
58
|
+
await connection.close();
|
|
59
|
+
});
|
|
60
|
+
it("exposes the complete closed v0 stable error mapping", () => {
|
|
61
|
+
expect(AGENT_GATEWAY_ERROR_CODES).toEqual([
|
|
62
|
+
"AGENT_TYPE_UNKNOWN",
|
|
63
|
+
"AGENT_SESSION_NOT_FOUND",
|
|
64
|
+
"AGENT_SCOPE_DENIED",
|
|
65
|
+
"AGENT_SESSION_REPLAY_GAP",
|
|
66
|
+
"AGENT_SESSION_CURSOR_AHEAD",
|
|
67
|
+
"AGENT_SESSION_CURSOR_EXPIRED",
|
|
68
|
+
"AGENT_SESSION_CURSOR_INVALID",
|
|
69
|
+
"AGENT_REQUEST_CONFLICT",
|
|
70
|
+
"AGENT_REQUEST_OUTCOME_UNKNOWN",
|
|
71
|
+
"AGENT_COMMAND_INVALID_STATE",
|
|
72
|
+
"AGENT_SESSION_RUNTIME_SCOPE_MISMATCH",
|
|
73
|
+
"AGENT_SHARED_ENVIRONMENT_UNAVAILABLE",
|
|
74
|
+
"AGENT_GATEWAY_CLOSED"
|
|
75
|
+
]);
|
|
76
|
+
expect(new AgentGatewayError(AgentGatewayErrorCode.AGENT_TYPE_UNKNOWN, "unknown").toJSON()).toEqual({
|
|
77
|
+
code: AgentGatewayErrorCode.AGENT_TYPE_UNKNOWN,
|
|
78
|
+
message: "unknown"
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it("applies admission before mutation with strong denial for every effect", async () => {
|
|
82
|
+
const cases = [
|
|
83
|
+
{
|
|
84
|
+
effect: "session.create",
|
|
85
|
+
run: async (fixture, scope, requestId) => {
|
|
86
|
+
await expectCode(fixture.gateway.createSession({ scope, agentTypeId: "alpha", requestId, title: "denied" }), "AGENT_SCOPE_DENIED");
|
|
87
|
+
expect(await fixture.gateway.listSessions({ scope })).toEqual({ sessions: [] });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
effect: "session.rename",
|
|
92
|
+
run: async (fixture, scope, requestId) => {
|
|
93
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
94
|
+
await expectCode(fixture.gateway.renameSession({ scope, ref, requestId, title: "mutated" }), "AGENT_SCOPE_DENIED");
|
|
95
|
+
expect((await fixture.gateway.readSessionState({ scope, ref })).summary.title).toBe(`${requestId}-session`);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
effect: "session.delete",
|
|
100
|
+
run: async (fixture, scope, requestId) => {
|
|
101
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
102
|
+
await expectCode(fixture.gateway.deleteSession({ scope, ref, requestId }), "AGENT_SCOPE_DENIED");
|
|
103
|
+
await expect(fixture.gateway.readSessionState({ scope, ref })).resolves.toMatchObject({ ref });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
effect: "session.prompt",
|
|
108
|
+
run: async (fixture, scope, requestId) => {
|
|
109
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
110
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
111
|
+
await expectCode(connection.send({ kind: "prompt", requestId, clientNonce: requestId, content: "prompt" }), "AGENT_SCOPE_DENIED");
|
|
112
|
+
const state = await fixture.gateway.readSessionState({ scope, ref });
|
|
113
|
+
expect(state.summary.status).toBe("idle");
|
|
114
|
+
expect(state.state.messages).toEqual([]);
|
|
115
|
+
await connection.close();
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
effect: "session.followup",
|
|
120
|
+
run: async (fixture, scope, requestId) => {
|
|
121
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
122
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
123
|
+
fixture.setActivity(ref, "running");
|
|
124
|
+
await expectCode(connection.send({ kind: "followup", requestId, clientNonce: requestId, clientSeq: 1, content: "followup" }), "AGENT_SCOPE_DENIED");
|
|
125
|
+
const state = await fixture.gateway.readSessionState({ scope, ref });
|
|
126
|
+
expect(state.summary.status).toBe("running");
|
|
127
|
+
expect(state.state.queue.followUps).toEqual([]);
|
|
128
|
+
await connection.close();
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
effect: "session.interrupt",
|
|
133
|
+
run: async (fixture, scope, requestId) => {
|
|
134
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
135
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
136
|
+
fixture.setActivity(ref, "running");
|
|
137
|
+
await expectCode(connection.interrupt({ requestId }), "AGENT_SCOPE_DENIED");
|
|
138
|
+
expect((await fixture.gateway.readSessionState({ scope, ref })).summary.status).toBe("running");
|
|
139
|
+
await connection.close();
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
effect: "session.stop",
|
|
144
|
+
run: async (fixture, scope, requestId) => {
|
|
145
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
146
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
147
|
+
fixture.setActivity(ref, "running");
|
|
148
|
+
await connection.send({ kind: "followup", requestId: `${requestId}-queued`, clientNonce: `${requestId}-queued`, clientSeq: 1, content: "queued" });
|
|
149
|
+
await expectCode(connection.stop({ requestId }), "AGENT_SCOPE_DENIED");
|
|
150
|
+
const state = await fixture.gateway.readSessionState({ scope, ref });
|
|
151
|
+
expect(state.summary.status).toBe("running");
|
|
152
|
+
expect(state.state.queue.followUps).toHaveLength(1);
|
|
153
|
+
await connection.close();
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
effect: "session.queue.clear",
|
|
158
|
+
run: async (fixture, scope, requestId) => {
|
|
159
|
+
const ref = await createSession(fixture, scope, `${requestId}-session`);
|
|
160
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
161
|
+
fixture.setActivity(ref, "running");
|
|
162
|
+
await connection.send({ kind: "followup", requestId: `${requestId}-queued`, clientNonce: `${requestId}-queued`, clientSeq: 1, content: "queued" });
|
|
163
|
+
await expectCode(connection.clearQueue({ requestId }), "AGENT_SCOPE_DENIED");
|
|
164
|
+
const state = await fixture.gateway.readSessionState({ scope, ref });
|
|
165
|
+
expect(state.summary.status).toBe("running");
|
|
166
|
+
expect(state.state.queue.followUps).toHaveLength(1);
|
|
167
|
+
await connection.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
];
|
|
171
|
+
for (const [index, testCase] of cases.entries()) {
|
|
172
|
+
const fixture = await options.createFixture();
|
|
173
|
+
const scope = fixture.issueScope();
|
|
174
|
+
const requestId = `strong-reject-${index}`;
|
|
175
|
+
fixture.queueAdmission(testCase.effect, "strong-reject");
|
|
176
|
+
await testCase.run(fixture, scope, requestId);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
it("applies admission before mutation with retryable retry and digest conflict", async () => {
|
|
180
|
+
const fixture = await options.createFixture();
|
|
181
|
+
const scope = fixture.issueScope();
|
|
182
|
+
fixture.queueAdmission("session.create", "strong-reject");
|
|
183
|
+
await expectCode(fixture.gateway.createSession({
|
|
184
|
+
scope,
|
|
185
|
+
agentTypeId: "alpha",
|
|
186
|
+
requestId: "strong-reject",
|
|
187
|
+
title: "denied"
|
|
188
|
+
}), "AGENT_SCOPE_DENIED");
|
|
189
|
+
await expectCode(fixture.gateway.createSession({
|
|
190
|
+
scope,
|
|
191
|
+
agentTypeId: "alpha",
|
|
192
|
+
requestId: "strong-reject",
|
|
193
|
+
title: "denied"
|
|
194
|
+
}), "AGENT_SCOPE_DENIED");
|
|
195
|
+
await expectCode(fixture.gateway.createSession({
|
|
196
|
+
scope,
|
|
197
|
+
agentTypeId: "alpha",
|
|
198
|
+
requestId: "strong-reject",
|
|
199
|
+
title: "conflicting digest"
|
|
200
|
+
}), "AGENT_REQUEST_CONFLICT");
|
|
201
|
+
fixture.queueAdmission("session.create", "retryable");
|
|
202
|
+
await expectCode(fixture.gateway.createSession({
|
|
203
|
+
scope,
|
|
204
|
+
agentTypeId: "alpha",
|
|
205
|
+
requestId: "retryable"
|
|
206
|
+
}), "AGENT_GATEWAY_CLOSED");
|
|
207
|
+
await expect(fixture.gateway.listSessions({ scope })).resolves.toEqual({ sessions: [] });
|
|
208
|
+
await expect(fixture.gateway.createSession({
|
|
209
|
+
scope,
|
|
210
|
+
agentTypeId: "alpha",
|
|
211
|
+
requestId: "retryable"
|
|
212
|
+
})).resolves.toMatchObject({ agentTypeId: "alpha" });
|
|
213
|
+
});
|
|
214
|
+
it("fails unknown agents and hidden cross-scope sessions with stable errors", async () => {
|
|
215
|
+
const fixture = await options.createFixture();
|
|
216
|
+
const scopeA = fixture.issueScope({ workspaceScopeId: "workspace-a" });
|
|
217
|
+
const scopeB = fixture.issueScope({ workspaceScopeId: "workspace-b" });
|
|
218
|
+
const ref = await createSession(fixture, scopeA, "scope-a-session");
|
|
219
|
+
await expectCode(createSession(fixture, scopeA, "unknown-agent", "missing"), "AGENT_TYPE_UNKNOWN");
|
|
220
|
+
await expectCode(fixture.gateway.readSessionState({ scope: scopeB, ref }), "AGENT_SESSION_NOT_FOUND");
|
|
221
|
+
await expectCode(fixture.gateway.readSessionState({ scope: scopeA, ref: { agentTypeId: ref.agentTypeId, sessionId: "missing" } }), "AGENT_SESSION_NOT_FOUND");
|
|
222
|
+
});
|
|
223
|
+
it("keys idempotency by scope, effect, full target, request id, and digest", async () => {
|
|
224
|
+
const fixture = await options.createFixture();
|
|
225
|
+
const scopeA = fixture.issueScope({ workspaceScopeId: "workspace-a" });
|
|
226
|
+
const scopeB = fixture.issueScope({ workspaceScopeId: "workspace-b" });
|
|
227
|
+
const [first, concurrentRetry] = await Promise.all([
|
|
228
|
+
createSession(fixture, scopeA, "same-id"),
|
|
229
|
+
createSession(fixture, scopeA, "same-id")
|
|
230
|
+
]);
|
|
231
|
+
expect(concurrentRetry).toEqual(first);
|
|
232
|
+
expect(await createSession(fixture, scopeA, "same-id")).toEqual(first);
|
|
233
|
+
await expectCode(
|
|
234
|
+
fixture.gateway.createSession({ scope: scopeA, agentTypeId: "alpha", requestId: "same-id", title: "different" }),
|
|
235
|
+
"AGENT_REQUEST_CONFLICT"
|
|
236
|
+
);
|
|
237
|
+
const otherScope = await createSession(fixture, scopeB, "same-id");
|
|
238
|
+
const otherAgent = await createSession(fixture, scopeA, "same-id", "beta");
|
|
239
|
+
expect(otherScope).not.toEqual(first);
|
|
240
|
+
expect(otherAgent.agentTypeId).toBe("beta");
|
|
241
|
+
const second = await createSession(fixture, scopeA, "second-target");
|
|
242
|
+
const renamedFirst = await fixture.gateway.renameSession({ scope: scopeA, ref: first, requestId: "shared-target-id", title: "one" });
|
|
243
|
+
expect(await fixture.gateway.renameSession({ scope: scopeA, ref: first, requestId: "shared-target-id", title: "one" })).toEqual(renamedFirst);
|
|
244
|
+
await expectCode(
|
|
245
|
+
fixture.gateway.renameSession({ scope: scopeA, ref: first, requestId: "shared-target-id", title: "changed" }),
|
|
246
|
+
"AGENT_REQUEST_CONFLICT"
|
|
247
|
+
);
|
|
248
|
+
await expect(fixture.gateway.renameSession({ scope: scopeA, ref: second, requestId: "shared-target-id", title: "two" })).resolves.toMatchObject({ title: "two" });
|
|
249
|
+
await fixture.gateway.deleteSession({ scope: scopeA, ref: second, requestId: "delete-id" });
|
|
250
|
+
await expect(fixture.gateway.deleteSession({ scope: scopeA, ref: second, requestId: "delete-id" })).resolves.toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
it("enforces command states, queue controls, typed receipts, and command idempotency", async () => {
|
|
253
|
+
const fixture = await options.createFixture();
|
|
254
|
+
const scope = fixture.issueScope();
|
|
255
|
+
const ref = await createSession(fixture, scope, "commands");
|
|
256
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
257
|
+
expect(await connection.interrupt({ requestId: "idle-interrupt" })).toMatchObject({ accepted: true });
|
|
258
|
+
expect((await fixture.gateway.readSessionState({ scope, ref })).summary.status).toBe("idle");
|
|
259
|
+
const prompt = await connection.send({ kind: "prompt", requestId: "prompt", clientNonce: "nonce-p", content: "start" });
|
|
260
|
+
expect(prompt).toMatchObject({ accepted: true, disposition: "prompt", clientNonce: "nonce-p" });
|
|
261
|
+
expect(await connection.send({ kind: "prompt", requestId: "prompt", clientNonce: "nonce-p", content: "start" })).toMatchObject({ duplicate: true });
|
|
262
|
+
await expectCode(
|
|
263
|
+
connection.send({ kind: "prompt", requestId: "prompt", clientNonce: "nonce-p", content: "different" }),
|
|
264
|
+
"AGENT_REQUEST_CONFLICT"
|
|
265
|
+
);
|
|
266
|
+
await expectCode(
|
|
267
|
+
connection.send({ kind: "prompt", requestId: "second-prompt", clientNonce: "nonce-p2", content: "invalid while running" }),
|
|
268
|
+
"AGENT_COMMAND_INVALID_STATE"
|
|
269
|
+
);
|
|
270
|
+
const followup = await connection.send({ kind: "followup", requestId: "followup", clientNonce: "nonce-f", clientSeq: 4, content: "next" });
|
|
271
|
+
expect(followup).toMatchObject({ accepted: true, disposition: "followup", clientNonce: "nonce-f", clientSeq: 4 });
|
|
272
|
+
expect(await connection.interrupt({ requestId: "interrupt" })).toMatchObject({ accepted: true });
|
|
273
|
+
expect(await connection.interrupt({ requestId: "interrupt" })).toMatchObject({ accepted: true });
|
|
274
|
+
expect(await connection.interrupt({ requestId: "interrupt-aborting" })).toMatchObject({ accepted: true });
|
|
275
|
+
await expectCode(
|
|
276
|
+
connection.send({ kind: "prompt", requestId: "aborting-prompt", clientNonce: "nonce-a", content: "invalid while aborting" }),
|
|
277
|
+
"AGENT_COMMAND_INVALID_STATE"
|
|
278
|
+
);
|
|
279
|
+
await expect(connection.send({ kind: "followup", requestId: "aborting-followup", clientNonce: "nonce-af", clientSeq: 5, content: "queued while aborting" })).resolves.toMatchObject({ disposition: "followup" });
|
|
280
|
+
expect(await connection.clearQueue({ requestId: "clear" })).toMatchObject({ accepted: true, cleared: 2 });
|
|
281
|
+
expect(await connection.clearQueue({ requestId: "clear" })).toMatchObject({ accepted: true, cleared: 2 });
|
|
282
|
+
fixture.setActivity(ref, "error");
|
|
283
|
+
await expect(connection.send({ kind: "prompt", requestId: "error-recovery", clientNonce: "nonce-e", content: "retry" })).resolves.toMatchObject({ disposition: "prompt" });
|
|
284
|
+
const stop = await connection.stop({ requestId: "stop" });
|
|
285
|
+
expect(stop).toMatchObject({ accepted: true, stopped: true, clearedQueue: [] });
|
|
286
|
+
expect(await connection.stop({ requestId: "stop" })).toEqual(stop);
|
|
287
|
+
await connection.close();
|
|
288
|
+
});
|
|
289
|
+
it("covers the complete command-state table and queue selector semantics", async () => {
|
|
290
|
+
const fixture = await options.createFixture();
|
|
291
|
+
const scope = fixture.issueScope();
|
|
292
|
+
const states = ["idle", "running", "aborting", "error"];
|
|
293
|
+
for (const [index, state] of states.entries()) {
|
|
294
|
+
const ref = await createSession(fixture, scope, `state-${state}`);
|
|
295
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
296
|
+
fixture.setActivity(ref, state);
|
|
297
|
+
const prompt = connection.send({
|
|
298
|
+
kind: "prompt",
|
|
299
|
+
requestId: `state-prompt-${index}`,
|
|
300
|
+
clientNonce: `state-prompt-${index}`,
|
|
301
|
+
content: "prompt"
|
|
302
|
+
});
|
|
303
|
+
if (state === "idle" || state === "error") {
|
|
304
|
+
await expect(prompt).resolves.toMatchObject({ disposition: "prompt" });
|
|
305
|
+
} else {
|
|
306
|
+
await expectCode(prompt, "AGENT_COMMAND_INVALID_STATE");
|
|
307
|
+
}
|
|
308
|
+
fixture.setActivity(ref, state);
|
|
309
|
+
const followupInput = {
|
|
310
|
+
kind: "followup",
|
|
311
|
+
requestId: `state-followup-${index}`,
|
|
312
|
+
clientNonce: `state-followup-${index}`,
|
|
313
|
+
clientSeq: index,
|
|
314
|
+
content: "follow-up"
|
|
315
|
+
};
|
|
316
|
+
await expect(connection.send(followupInput)).resolves.toMatchObject({ disposition: "followup" });
|
|
317
|
+
await expect(connection.send(followupInput)).resolves.toMatchObject({ duplicate: true });
|
|
318
|
+
await expectCode(connection.send({ ...followupInput, content: "conflict" }), "AGENT_REQUEST_CONFLICT");
|
|
319
|
+
await expectCode(connection.clearQueue({
|
|
320
|
+
requestId: `selector-mismatch-${index}`,
|
|
321
|
+
clientNonce: followupInput.clientNonce,
|
|
322
|
+
clientSeq: index + 100
|
|
323
|
+
}), "AGENT_REQUEST_CONFLICT");
|
|
324
|
+
const beforeClear = (await fixture.gateway.readSessionState({ scope, ref })).summary.status;
|
|
325
|
+
await expect(connection.clearQueue({ requestId: `state-clear-${index}` })).resolves.toMatchObject({ accepted: true });
|
|
326
|
+
expect((await fixture.gateway.readSessionState({ scope, ref })).summary.status).toBe(beforeClear);
|
|
327
|
+
fixture.setActivity(ref, state);
|
|
328
|
+
await expect(connection.interrupt({ requestId: `state-interrupt-${index}` })).resolves.toMatchObject({ accepted: true });
|
|
329
|
+
const afterInterrupt = (await fixture.gateway.readSessionState({ scope, ref })).summary.status;
|
|
330
|
+
expect(afterInterrupt).toBe(state === "running" ? "aborting" : state);
|
|
331
|
+
fixture.setActivity(ref, state);
|
|
332
|
+
await expect(connection.stop({ requestId: `state-stop-${index}` })).resolves.toMatchObject({
|
|
333
|
+
accepted: true,
|
|
334
|
+
stopped: state === "running" || state === "aborting"
|
|
335
|
+
});
|
|
336
|
+
expect((await fixture.gateway.readSessionState({ scope, ref })).summary.status).toBe("idle");
|
|
337
|
+
await connection.close();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
it("treats close as unsubscribe and keeps the session turn alive", async () => {
|
|
341
|
+
const fixture = await options.createFixture();
|
|
342
|
+
const scope = fixture.issueScope();
|
|
343
|
+
const ref = await createSession(fixture, scope, "close-unsubscribe");
|
|
344
|
+
const first = await fixture.gateway.connectSession({ scope, ref });
|
|
345
|
+
const pendingEvent = first.events[Symbol.asyncIterator]().next();
|
|
346
|
+
await first.close();
|
|
347
|
+
await expect(pendingEvent).resolves.toMatchObject({ done: true });
|
|
348
|
+
const second = await fixture.gateway.connectSession({ scope, ref });
|
|
349
|
+
await second.send({ kind: "prompt", requestId: "run", clientNonce: "run", content: "continue" });
|
|
350
|
+
await second.close();
|
|
351
|
+
const snapshot = await fixture.gateway.readSessionState({ scope, ref });
|
|
352
|
+
expect(snapshot.summary.status).toBe("running");
|
|
353
|
+
const third = await fixture.gateway.connectSession({ scope, ref });
|
|
354
|
+
await expect(third.interrupt({ requestId: "interrupt-after-close" })).resolves.toMatchObject({ accepted: true });
|
|
355
|
+
await third.close();
|
|
356
|
+
});
|
|
357
|
+
it("provides monotonic single-sequence envelopes and consistent snapshots", async () => {
|
|
358
|
+
const fixture = await options.createFixture();
|
|
359
|
+
const scope = fixture.issueScope();
|
|
360
|
+
const ref = await createSession(fixture, scope, "sequence");
|
|
361
|
+
const connection = await fixture.gateway.connectSession({ scope, ref, cursor: 0 });
|
|
362
|
+
await connection.send({ kind: "prompt", requestId: "event-1", clientNonce: "event-1", content: "one" });
|
|
363
|
+
await connection.send({ kind: "followup", requestId: "event-2", clientNonce: "event-2", clientSeq: 1, content: "two" });
|
|
364
|
+
const iterator = connection.events[Symbol.asyncIterator]();
|
|
365
|
+
const first = await iterator.next();
|
|
366
|
+
const second = await iterator.next();
|
|
367
|
+
expect(first.done).toBe(false);
|
|
368
|
+
expect(second.done).toBe(false);
|
|
369
|
+
if (!first.done && !second.done) {
|
|
370
|
+
expect(first.value.seq).toBe(first.value.event.seq);
|
|
371
|
+
expect(second.value.seq).toBe(second.value.event.seq);
|
|
372
|
+
expect(second.value.seq).toBeGreaterThan(first.value.seq);
|
|
373
|
+
}
|
|
374
|
+
const snapshot = await fixture.gateway.readSessionState({ scope, ref });
|
|
375
|
+
expect(snapshot.seq).toBe(snapshot.state.seq);
|
|
376
|
+
expect(snapshot.ref).toEqual(ref);
|
|
377
|
+
await connection.close();
|
|
378
|
+
});
|
|
379
|
+
it("recovers a bounded replay gap through snapshot then reconnect", async () => {
|
|
380
|
+
const fixture = await options.createFixture();
|
|
381
|
+
const scope = fixture.issueScope();
|
|
382
|
+
const ref = await createSession(fixture, scope, "replay");
|
|
383
|
+
const connection = await fixture.gateway.connectSession({ scope, ref });
|
|
384
|
+
for (let index = 0; index < 6; index += 1) {
|
|
385
|
+
await connection.send({ kind: "followup", requestId: `replay-${index}`, clientNonce: `replay-${index}`, clientSeq: index, content: "event" });
|
|
386
|
+
}
|
|
387
|
+
await connection.close();
|
|
388
|
+
await expectCode(fixture.gateway.connectSession({ scope, ref, cursor: 0 }), "AGENT_SESSION_REPLAY_GAP");
|
|
389
|
+
const snapshot = await fixture.gateway.readSessionState({ scope, ref });
|
|
390
|
+
await expect(fixture.gateway.connectSession({ scope, ref, cursor: snapshot.seq })).resolves.toMatchObject({ ref });
|
|
391
|
+
await expectCode(fixture.gateway.connectSession({ scope, ref, cursor: snapshot.seq + 1 }), "AGENT_SESSION_CURSOR_AHEAD");
|
|
392
|
+
});
|
|
393
|
+
it("orders keyset pages and denies tampered or rebound cursors", async () => {
|
|
394
|
+
const fixture = await options.createFixture();
|
|
395
|
+
const scopeA = fixture.issueScope({ workspaceScopeId: "workspace-a" });
|
|
396
|
+
const scopeB = fixture.issueScope({ workspaceScopeId: "workspace-b" });
|
|
397
|
+
const alphaA = await createSession(fixture, scopeA, "a", "alpha");
|
|
398
|
+
const beta = await createSession(fixture, scopeA, "b", "beta");
|
|
399
|
+
const alphaC = await createSession(fixture, scopeA, "c", "alpha");
|
|
400
|
+
fixture.moveSession(alphaA, 5e3);
|
|
401
|
+
fixture.moveSession(beta, 5e3);
|
|
402
|
+
fixture.moveSession(alphaC, 5e3);
|
|
403
|
+
const page = await fixture.gateway.listSessions({ scope: scopeA, limit: 1 });
|
|
404
|
+
expect(page.sessions).toHaveLength(1);
|
|
405
|
+
expect(page.nextCursor).toBeDefined();
|
|
406
|
+
const cursor = page.nextCursor;
|
|
407
|
+
await expectCode(fixture.gateway.listSessions({ scope: scopeA, cursor: `${cursor}tampered`, limit: 1 }), "AGENT_SESSION_CURSOR_INVALID");
|
|
408
|
+
await expectCode(fixture.gateway.listSessions({ scope: scopeB, cursor, limit: 1 }), "AGENT_SESSION_CURSOR_INVALID");
|
|
409
|
+
await expectCode(fixture.gateway.listSessions({ scope: scopeA, cursor, limit: 2 }), "AGENT_SESSION_CURSOR_INVALID");
|
|
410
|
+
await expectCode(fixture.gateway.listSessions({ scope: scopeA, cursor, agentTypeId: "alpha", limit: 1 }), "AGENT_SESSION_CURSOR_INVALID");
|
|
411
|
+
const all = await fixture.gateway.listSessions({ scope: scopeA, limit: 20 });
|
|
412
|
+
expect(all.sessions).toEqual([...all.sessions].sort(
|
|
413
|
+
(left, right) => right.updatedAt - left.updatedAt || left.ref.agentTypeId.localeCompare(right.ref.agentTypeId) || left.ref.sessionId.localeCompare(right.ref.sessionId)
|
|
414
|
+
));
|
|
415
|
+
expect(all.sessions.map((session) => session.ref)).toEqual([alphaA, alphaC, beta]);
|
|
416
|
+
});
|
|
417
|
+
it("documents keyset mutation behavior: moved rows may shift and deleted rows disappear", async () => {
|
|
418
|
+
const fixture = await options.createFixture();
|
|
419
|
+
const scope = fixture.issueScope();
|
|
420
|
+
const first = await createSession(fixture, scope, "mutation-a");
|
|
421
|
+
const moved = await createSession(fixture, scope, "mutation-b");
|
|
422
|
+
const deleted = await createSession(fixture, scope, "mutation-c");
|
|
423
|
+
const page = await fixture.gateway.listSessions({ scope, limit: 1 });
|
|
424
|
+
expect(page.nextCursor).toBeDefined();
|
|
425
|
+
fixture.moveSession(moved, Date.now() + 1e5);
|
|
426
|
+
await fixture.gateway.deleteSession({ scope, ref: deleted, requestId: "mutation-delete" });
|
|
427
|
+
const continuation = await fixture.gateway.listSessions({ scope, limit: 1, cursor: page.nextCursor });
|
|
428
|
+
expect(continuation.sessions).not.toContainEqual(expect.objectContaining({ ref: deleted }));
|
|
429
|
+
expect(continuation.sessions).not.toContainEqual(expect.objectContaining({ ref: moved }));
|
|
430
|
+
expect([first, moved, deleted]).toHaveLength(3);
|
|
431
|
+
});
|
|
432
|
+
it("allows a still-valid cursor to yield an empty page after mutation", async () => {
|
|
433
|
+
const fixture = await options.createFixture();
|
|
434
|
+
const scope = fixture.issueScope();
|
|
435
|
+
const tail = await createSession(fixture, scope, "empty-a");
|
|
436
|
+
await createSession(fixture, scope, "empty-b");
|
|
437
|
+
const page = await fixture.gateway.listSessions({ scope, limit: 1 });
|
|
438
|
+
expect(page.nextCursor).toBeDefined();
|
|
439
|
+
await fixture.gateway.deleteSession({ scope, ref: tail, requestId: "empty-delete" });
|
|
440
|
+
await expect(fixture.gateway.listSessions({ scope, limit: 1, cursor: page.nextCursor })).resolves.toEqual({ sessions: [] });
|
|
441
|
+
});
|
|
442
|
+
it("closes the facade idempotently and rejects subsequent operations", async () => {
|
|
443
|
+
const fixture = await options.createFixture();
|
|
444
|
+
const scope = fixture.issueScope();
|
|
445
|
+
await fixture.gateway.close();
|
|
446
|
+
await fixture.gateway.close();
|
|
447
|
+
await expectCode(fixture.gateway.listAgents({ scope }), "AGENT_GATEWAY_CLOSED");
|
|
448
|
+
});
|
|
449
|
+
it.skip("Level D restart preserves sequence continuity [owner: streaming lane]", () => {
|
|
450
|
+
});
|
|
451
|
+
it.skip("Level D durable request ledger replays receipts and create tombstones across restart [owner: streaming lane]", () => {
|
|
452
|
+
});
|
|
453
|
+
it.skip("Level D durable admission/effect crash matrix resolves acknowledged and outcome-unknown work [owner: streaming lane]", () => {
|
|
454
|
+
});
|
|
455
|
+
it.skip("Level D durable activity index reconciles non-quiescent states at startup [owner: streaming lane]", () => {
|
|
456
|
+
});
|
|
457
|
+
it.skip("Level D/v2 immutable snapshot pagination is mutation-stable and expires [owner: #905 pool cursor]", () => {
|
|
458
|
+
});
|
|
459
|
+
it.skip("v2 remote wire validates JSON event leaves, paths, depth, and size [owner: #905 remote wire]", () => {
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
export {
|
|
464
|
+
gatewayConformance
|
|
465
|
+
};
|