@turingfocus/chat-testing 0.1.0

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/dist/contract-core.d.ts +12 -0
  3. package/dist/contract-core.d.ts.map +1 -0
  4. package/dist/contract-core.js +34 -0
  5. package/dist/contract-core.js.map +1 -0
  6. package/dist/contracts.d.ts +4 -0
  7. package/dist/contracts.d.ts.map +1 -0
  8. package/dist/contracts.js +4 -0
  9. package/dist/contracts.js.map +1 -0
  10. package/dist/fixtures.d.ts +37 -0
  11. package/dist/fixtures.d.ts.map +1 -0
  12. package/dist/fixtures.js +365 -0
  13. package/dist/fixtures.js.map +1 -0
  14. package/dist/gateway-contracts.d.ts +58 -0
  15. package/dist/gateway-contracts.d.ts.map +1 -0
  16. package/dist/gateway-contracts.js +524 -0
  17. package/dist/gateway-contracts.js.map +1 -0
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +4 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/memory-gateway-clock.d.ts +13 -0
  23. package/dist/memory-gateway-clock.d.ts.map +1 -0
  24. package/dist/memory-gateway-clock.js +52 -0
  25. package/dist/memory-gateway-clock.js.map +1 -0
  26. package/dist/memory-gateway-holds.d.ts +26 -0
  27. package/dist/memory-gateway-holds.d.ts.map +1 -0
  28. package/dist/memory-gateway-holds.js +67 -0
  29. package/dist/memory-gateway-holds.js.map +1 -0
  30. package/dist/memory-gateway.d.ts +55 -0
  31. package/dist/memory-gateway.d.ts.map +1 -0
  32. package/dist/memory-gateway.js +461 -0
  33. package/dist/memory-gateway.js.map +1 -0
  34. package/dist/runtime-contracts.d.ts +26 -0
  35. package/dist/runtime-contracts.d.ts.map +1 -0
  36. package/dist/runtime-contracts.js +425 -0
  37. package/dist/runtime-contracts.js.map +1 -0
  38. package/package.json +37 -0
@@ -0,0 +1,52 @@
1
+ /** Deterministic clock used to drive Memory Gateway deadlines without timers. */
2
+ export class MemoryGatewayClock {
3
+ #deadlineWaiters = new Set();
4
+ #time;
5
+ constructor(now) {
6
+ this.#time = now;
7
+ }
8
+ now() {
9
+ return this.#time;
10
+ }
11
+ signal(deadlineAt) {
12
+ if (this.#time >= deadlineAt) {
13
+ return { cancel: () => undefined, promise: Promise.resolve() };
14
+ }
15
+ let active = true;
16
+ let resolvePromise;
17
+ const promise = new Promise((resolve) => {
18
+ resolvePromise = resolve;
19
+ });
20
+ const waiter = {
21
+ deadlineAt,
22
+ resolve: () => {
23
+ if (!active)
24
+ return;
25
+ active = false;
26
+ this.#deadlineWaiters.delete(waiter);
27
+ resolvePromise();
28
+ },
29
+ };
30
+ this.#deadlineWaiters.add(waiter);
31
+ return {
32
+ cancel: () => {
33
+ if (!active)
34
+ return;
35
+ active = false;
36
+ this.#deadlineWaiters.delete(waiter);
37
+ },
38
+ promise,
39
+ };
40
+ }
41
+ advanceTo(timestamp) {
42
+ if (!Number.isSafeInteger(timestamp) || timestamp < this.#time) {
43
+ throw new TypeError("Memory Gateway time must advance to a safe integer");
44
+ }
45
+ this.#time = timestamp;
46
+ for (const waiter of [...this.#deadlineWaiters]) {
47
+ if (waiter.deadlineAt <= timestamp)
48
+ waiter.resolve();
49
+ }
50
+ }
51
+ }
52
+ //# sourceMappingURL=memory-gateway-clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-gateway-clock.js","sourceRoot":"","sources":["../src/memory-gateway-clock.ts"],"names":[],"mappings":"AAUA,iFAAiF;AACjF,MAAM,OAAO,kBAAkB;IACpB,gBAAgB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtD,KAAK,CAAS;IAEd,YAAY,GAAW;QACrB,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC;IACnB,CAAC;IAED,GAAG;QACD,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,MAAM,CAAC,UAAkB;QACvB,IAAI,IAAI,CAAC,KAAK,IAAI,UAAU,EAAE,CAAC;YAC7B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACjE,CAAC;QAED,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,IAAI,cAAwC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC5C,cAAc,GAAG,OAAO,CAAC;QAC3B,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAmB;YAC7B,UAAU;YACV,OAAO,EAAE,GAAG,EAAE;gBACZ,IAAI,CAAC,MAAM;oBAAE,OAAO;gBACpB,MAAM,GAAG,KAAK,CAAC;gBACf,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBACrC,cAAe,EAAE,CAAC;YACpB,CAAC;SACF,CAAC;QACF,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClC,OAAO;YACL,MAAM,EAAE,GAAG,EAAE;gBACX,IAAI,CAAC,MAAM;oBAAE,OAAO;gBACpB,MAAM,GAAG,KAAK,CAAC;gBACf,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACvC,CAAC;YACD,OAAO;SACR,CAAC;IACJ,CAAC;IAED,SAAS,CAAC,SAAiB;QACzB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAC/D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAChD,IAAI,MAAM,CAAC,UAAU,IAAI,SAAS;gBAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QACvD,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,26 @@
1
+ export type MemoryGatewayOperation = "answerInteraction" | "interrupt" | "listConversations" | "loadConversation" | "sendText" | "subscribe";
2
+ export type MemoryGatewayHoldPoint = MemoryGatewayOperation | "gateway.dispose" | "subscription.dispose";
3
+ export interface MemoryGatewayHold {
4
+ readonly completed: Promise<void>;
5
+ readonly resourceDisposeCount: number;
6
+ readonly started: Promise<void>;
7
+ release(): void;
8
+ }
9
+ export interface HeldTransportCompletion {
10
+ readonly subscription?: FakeTransportSubscription | undefined;
11
+ }
12
+ export declare class FakeTransportSubscription {
13
+ #private;
14
+ constructor(onDispose: () => void);
15
+ dispose(): void;
16
+ }
17
+ export declare class PendingHold implements MemoryGatewayHold {
18
+ #private;
19
+ constructor(point: MemoryGatewayHoldPoint);
20
+ get completed(): Promise<void>;
21
+ get resourceDisposeCount(): number;
22
+ get started(): Promise<void>;
23
+ start(): Promise<HeldTransportCompletion>;
24
+ release(): void;
25
+ }
26
+ //# sourceMappingURL=memory-gateway-holds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-gateway-holds.d.ts","sourceRoot":"","sources":["../src/memory-gateway-holds.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAC9B,mBAAmB,GACnB,WAAW,GACX,mBAAmB,GACnB,kBAAkB,GAClB,UAAU,GACV,WAAW,CAAC;AAEhB,MAAM,MAAM,sBAAsB,GAChC,sBAAsB,GAAG,iBAAiB,GAAG,sBAAsB,CAAC;AAEtE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAC;CAC/D;AAED,qBAAa,yBAAyB;;gBAIxB,SAAS,EAAE,MAAM,IAAI;IAIjC,OAAO,IAAI,IAAI;CAKhB;AAED,qBAAa,WAAY,YAAW,iBAAiB;;gBAWvC,KAAK,EAAE,sBAAsB;IAqBzC,IAAI,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAE7B;IAED,IAAI,oBAAoB,IAAI,MAAM,CAEjC;IAED,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAED,KAAK,IAAI,OAAO,CAAC,uBAAuB,CAAC;IASzC,OAAO,IAAI,IAAI;CAKhB"}
@@ -0,0 +1,67 @@
1
+ export class FakeTransportSubscription {
2
+ #onDispose;
3
+ #disposed = false;
4
+ constructor(onDispose) {
5
+ this.#onDispose = onDispose;
6
+ }
7
+ dispose() {
8
+ if (this.#disposed)
9
+ return;
10
+ this.#disposed = true;
11
+ this.#onDispose();
12
+ }
13
+ }
14
+ export class PendingHold {
15
+ #completedPromise;
16
+ #publicCompletedPromise;
17
+ #resolveCompleted;
18
+ #resolveStarted;
19
+ #startedPromise;
20
+ #released = false;
21
+ #resourceDisposeCount = 0;
22
+ #started = false;
23
+ #transportSubscription;
24
+ constructor(point) {
25
+ let resolveCompleted;
26
+ this.#completedPromise = new Promise((resolve) => {
27
+ resolveCompleted = resolve;
28
+ });
29
+ this.#publicCompletedPromise = this.#completedPromise.then(() => undefined);
30
+ this.#resolveCompleted = resolveCompleted;
31
+ let resolveStarted;
32
+ this.#startedPromise = new Promise((resolve) => {
33
+ resolveStarted = resolve;
34
+ });
35
+ this.#resolveStarted = resolveStarted;
36
+ this.#transportSubscription =
37
+ point === "subscribe"
38
+ ? new FakeTransportSubscription(() => {
39
+ this.#resourceDisposeCount += 1;
40
+ })
41
+ : undefined;
42
+ }
43
+ get completed() {
44
+ return this.#publicCompletedPromise;
45
+ }
46
+ get resourceDisposeCount() {
47
+ return this.#resourceDisposeCount;
48
+ }
49
+ get started() {
50
+ return this.#startedPromise;
51
+ }
52
+ start() {
53
+ if (this.#started) {
54
+ throw new Error("A Memory Gateway hold can only start once");
55
+ }
56
+ this.#started = true;
57
+ this.#resolveStarted();
58
+ return this.#completedPromise;
59
+ }
60
+ release() {
61
+ if (this.#released)
62
+ return;
63
+ this.#released = true;
64
+ this.#resolveCompleted({ subscription: this.#transportSubscription });
65
+ }
66
+ }
67
+ //# sourceMappingURL=memory-gateway-holds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-gateway-holds.js","sourceRoot":"","sources":["../src/memory-gateway-holds.ts"],"names":[],"mappings":"AAsBA,MAAM,OAAO,yBAAyB;IAC3B,UAAU,CAAa;IAChC,SAAS,GAAG,KAAK,CAAC;IAElB,YAAY,SAAqB;QAC/B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC9B,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,UAAU,EAAE,CAAC;IACpB,CAAC;CACF;AAED,MAAM,OAAO,WAAW;IACb,iBAAiB,CAAmC;IACpD,uBAAuB,CAAgB;IACvC,iBAAiB,CAAgD;IACjE,eAAe,CAAa;IAC5B,eAAe,CAAgB;IACxC,SAAS,GAAG,KAAK,CAAC;IAClB,qBAAqB,GAAG,CAAC,CAAC;IAC1B,QAAQ,GAAG,KAAK,CAAC;IACR,sBAAsB,CAAwC;IAEvE,YAAY,KAA6B;QACvC,IAAI,gBACyD,CAAC;QAC9D,IAAI,CAAC,iBAAiB,GAAG,IAAI,OAAO,CAA0B,CAAC,OAAO,EAAE,EAAE;YACxE,gBAAgB,GAAG,OAAO,CAAC;QAC7B,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5E,IAAI,CAAC,iBAAiB,GAAG,gBAAiB,CAAC;QAC3C,IAAI,cAAwC,CAAC;QAC7C,IAAI,CAAC,eAAe,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACnD,cAAc,GAAG,OAAO,CAAC;QAC3B,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,eAAe,GAAG,cAAe,CAAC;QACvC,IAAI,CAAC,sBAAsB;YACzB,KAAK,KAAK,WAAW;gBACnB,CAAC,CAAC,IAAI,yBAAyB,CAAC,GAAG,EAAE;oBACjC,IAAI,CAAC,qBAAqB,IAAI,CAAC,CAAC;gBAClC,CAAC,CAAC;gBACJ,CAAC,CAAC,SAAS,CAAC;IAClB,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,uBAAuB,CAAC;IACtC,CAAC;IAED,IAAI,oBAAoB;QACtB,OAAO,IAAI,CAAC,qBAAqB,CAAC;IACpC,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,eAAe,CAAC;IAC9B,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,iBAAiB,CAAC;IAChC,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,iBAAiB,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACxE,CAAC;CACF"}
@@ -0,0 +1,55 @@
1
+ import { type ChatError, type AnswerInteractionInput, type AnswerInteractionSuccess, type ChatGateway, type ChatSnapshot, type ChatUpdate, type ConversationPage, type GatewayResult, type InterruptRunInput, type InterruptRunSuccess, type ListConversationsInput, type LoadConversationInput, type SendTextInput, type SendTextSuccess, type SubscribeConversationInput } from "@turingfocus/chat-protocol";
2
+ import { type ChatContractFixtures } from "./fixtures.js";
3
+ import { type MemoryGatewayHold, type MemoryGatewayHoldPoint, type MemoryGatewayOperation } from "./memory-gateway-holds.js";
4
+ export type { MemoryGatewayHold, MemoryGatewayHoldPoint, MemoryGatewayOperation, } from "./memory-gateway-holds.js";
5
+ export type MemoryGatewayCall = {
6
+ readonly operation: "answerInteraction";
7
+ readonly input: AnswerInteractionInput;
8
+ } | {
9
+ readonly operation: "interrupt";
10
+ readonly input: InterruptRunInput;
11
+ } | {
12
+ readonly operation: "listConversations";
13
+ readonly input: ListConversationsInput;
14
+ } | {
15
+ readonly operation: "loadConversation";
16
+ readonly input: LoadConversationInput;
17
+ } | {
18
+ readonly operation: "sendText";
19
+ readonly input: SendTextInput;
20
+ } | {
21
+ readonly operation: "subscribe";
22
+ readonly input: SubscribeConversationInput;
23
+ };
24
+ export interface MemoryChatGatewayOptions {
25
+ readonly connected?: boolean | undefined;
26
+ readonly fixtures?: ChatContractFixtures | undefined;
27
+ readonly now?: (() => number) | undefined;
28
+ }
29
+ export interface MemoryGatewayController {
30
+ readonly calls: readonly MemoryGatewayCall[];
31
+ readonly connected: boolean;
32
+ readonly disposed: boolean;
33
+ disconnect(error?: ChatError): number;
34
+ emitError(error: ChatError): number;
35
+ emitUpdate(update: ChatUpdate): number;
36
+ emitUpdateToAll(update: ChatUpdate): number;
37
+ failNext(operation: MemoryGatewayOperation, error: ChatError): void;
38
+ holdNext(point: MemoryGatewayHoldPoint): MemoryGatewayHold;
39
+ now(): number;
40
+ advanceTimeTo(timestamp: number): void;
41
+ reconnect(): void;
42
+ setConversationPage(page: ConversationPage): void;
43
+ setAnswerInteractionResult(result: GatewayResult<AnswerInteractionSuccess>): void;
44
+ setInterruptResult(result: GatewayResult<InterruptRunSuccess>): void;
45
+ setSendTextResult(result: GatewayResult<SendTextSuccess>): void;
46
+ setSnapshot(snapshot: ChatSnapshot): void;
47
+ }
48
+ export interface MemoryGatewayHarness {
49
+ readonly controller: MemoryGatewayController;
50
+ readonly fixtures: ChatContractFixtures;
51
+ readonly gateway: ChatGateway;
52
+ }
53
+ /** Creates one isolated, scriptable, network-free ChatGateway test harness. */
54
+ export declare const createMemoryChatGateway: (options?: MemoryChatGatewayOptions) => MemoryGatewayHarness;
55
+ //# sourceMappingURL=memory-gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-gateway.d.ts","sourceRoot":"","sources":["../src/memory-gateway.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,461 @@
1
+ import { answerInteractionInputSchema, answerInteractionResultSchema, chatErrorSchema, chatSnapshotSchema, chatUpdateSchema, conversationPageSchema, createGatewayDeadlineExceededError, interruptRunInputSchema, interruptRunResultSchema, isGatewayDeadlineExceeded, isGatewayOperationSupported, listConversationsInputSchema, loadConversationInputSchema, sendTextInputSchema, sendTextResultSchema, subscribeConversationInputSchema, } from "@turingfocus/chat-protocol";
2
+ import { createChatContractFixtures, } from "./fixtures.js";
3
+ import { FakeTransportSubscription, PendingHold, } from "./memory-gateway-holds.js";
4
+ import { MemoryGatewayClock } from "./memory-gateway-clock.js";
5
+ const unsupported = (message) => ({
6
+ ok: false,
7
+ error: chatErrorSchema.parse({
8
+ code: "unsupported",
9
+ message,
10
+ retryable: false,
11
+ }),
12
+ });
13
+ const notFound = (conversationId) => ({
14
+ ok: false,
15
+ error: chatErrorSchema.parse({
16
+ code: "not-found",
17
+ message: "Conversation was not found",
18
+ retryable: false,
19
+ conversationId,
20
+ }),
21
+ });
22
+ const disposedError = () => ({
23
+ ok: false,
24
+ error: chatErrorSchema.parse({
25
+ code: "conflict",
26
+ message: "Gateway has been disposed",
27
+ retryable: false,
28
+ }),
29
+ });
30
+ const staleInterrupt = (conversationId, requestedRunId) => ({
31
+ ok: false,
32
+ error: chatErrorSchema.parse({
33
+ code: "conflict",
34
+ message: "The requested run is no longer active",
35
+ retryable: false,
36
+ conversationId,
37
+ details: { requestedRunId },
38
+ }),
39
+ });
40
+ const updateConversationId = (update) => {
41
+ switch (update.kind) {
42
+ case "snapshot.replace":
43
+ return update.snapshot.conversation.id;
44
+ case "conversation.upsert":
45
+ return update.conversation.id;
46
+ case "error.reported":
47
+ return update.conversationId;
48
+ default:
49
+ return update.conversationId;
50
+ }
51
+ };
52
+ class MemoryChatGateway {
53
+ #calls = [];
54
+ #clock;
55
+ #failures = new Map();
56
+ #holds = new Map();
57
+ #observers = new Set();
58
+ #connected;
59
+ #answerInteractionResult;
60
+ #conversationPage;
61
+ #disposed = false;
62
+ #disposePromise;
63
+ #interruptResult;
64
+ #sendTextResult;
65
+ #snapshot;
66
+ constructor(options, fixtures) {
67
+ this.#connected = options.connected ?? true;
68
+ this.#clock = new MemoryGatewayClock(options.now?.() ?? Date.now());
69
+ this.#conversationPage = conversationPageSchema.parse({
70
+ conversations: [fixtures.conversation],
71
+ });
72
+ this.#snapshot = fixtures.initialSnapshot;
73
+ this.#answerInteractionResult = answerInteractionResultSchema.parse({
74
+ ok: true,
75
+ value: fixtures.answerInteractionSuccess,
76
+ });
77
+ this.#sendTextResult = sendTextResultSchema.parse({
78
+ ok: true,
79
+ value: fixtures.sendTextSuccess,
80
+ });
81
+ this.#interruptResult = interruptRunResultSchema.parse({
82
+ ok: true,
83
+ value: fixtures.interruptSuccess,
84
+ });
85
+ }
86
+ get calls() {
87
+ return Object.freeze([...this.#calls]);
88
+ }
89
+ get connected() {
90
+ return this.#connected;
91
+ }
92
+ get disposed() {
93
+ return this.#disposed;
94
+ }
95
+ #record(call) {
96
+ this.#calls.push(Object.freeze(call));
97
+ }
98
+ #takeHold(point) {
99
+ return this.#holds.get(point)?.shift();
100
+ }
101
+ async #waitForOperation(operation, input) {
102
+ const hold = this.#takeHold(operation);
103
+ if (hold === undefined)
104
+ return undefined;
105
+ const transportCompletion = hold.start();
106
+ const deadline = this.#clock.signal(input.deadlineAt);
107
+ const outcome = await Promise.race([
108
+ transportCompletion.then(() => "transport"),
109
+ deadline.promise.then(() => "deadline"),
110
+ ]);
111
+ if (outcome === "transport") {
112
+ deadline.cancel();
113
+ const completion = await transportCompletion;
114
+ if (this.#disposed) {
115
+ completion.subscription?.dispose();
116
+ return { failure: disposedError() };
117
+ }
118
+ return { transportSubscription: completion.subscription };
119
+ }
120
+ if (operation === "subscribe") {
121
+ void transportCompletion.then((completion) => {
122
+ completion.subscription?.dispose();
123
+ });
124
+ }
125
+ return {
126
+ failure: {
127
+ ok: false,
128
+ error: createGatewayDeadlineExceededError("conversationId" in input && typeof input.conversationId === "string"
129
+ ? input.conversationId
130
+ : undefined),
131
+ },
132
+ };
133
+ }
134
+ async #waitForCleanup(point, options) {
135
+ const hold = this.#takeHold(point);
136
+ if (hold === undefined)
137
+ return;
138
+ const transportCompletion = hold.start();
139
+ const deadline = this.#clock.signal(options.deadlineAt);
140
+ const outcome = await Promise.race([
141
+ transportCompletion.then(() => "transport"),
142
+ deadline.promise.then(() => "deadline"),
143
+ ]);
144
+ if (outcome === "transport")
145
+ deadline.cancel();
146
+ }
147
+ #guard(operation, input) {
148
+ if (isGatewayDeadlineExceeded(input, this.#clock.now())) {
149
+ return {
150
+ ok: false,
151
+ error: createGatewayDeadlineExceededError("conversationId" in input && typeof input.conversationId === "string"
152
+ ? input.conversationId
153
+ : undefined),
154
+ };
155
+ }
156
+ if (this.#disposed)
157
+ return disposedError();
158
+ const failures = this.#failures.get(operation);
159
+ const failure = failures?.shift();
160
+ if (failure !== undefined)
161
+ return { ok: false, error: failure };
162
+ if (!this.#connected) {
163
+ return {
164
+ ok: false,
165
+ error: chatErrorSchema.parse({
166
+ code: "network",
167
+ message: "Memory Gateway is disconnected",
168
+ retryable: true,
169
+ ...("conversationId" in input &&
170
+ typeof input.conversationId === "string"
171
+ ? { conversationId: input.conversationId }
172
+ : {}),
173
+ }),
174
+ };
175
+ }
176
+ return undefined;
177
+ }
178
+ async listConversations(input) {
179
+ const parsedInput = listConversationsInputSchema.parse(input);
180
+ this.#record({ operation: "listConversations", input: parsedInput });
181
+ const guarded = this.#guard("listConversations", parsedInput);
182
+ if (guarded !== undefined)
183
+ return guarded;
184
+ const held = await this.#waitForOperation("listConversations", parsedInput);
185
+ if (held?.failure !== undefined)
186
+ return held.failure;
187
+ if (!isGatewayOperationSupported(this.#snapshot.capabilities, "listConversations")) {
188
+ return unsupported("Conversation listing is unavailable");
189
+ }
190
+ return { ok: true, value: this.#conversationPage };
191
+ }
192
+ async answerInteraction(input) {
193
+ const parsed = answerInteractionInputSchema.safeParse(input);
194
+ if (!parsed.success) {
195
+ return {
196
+ ok: false,
197
+ error: chatErrorSchema.parse({
198
+ code: "validation",
199
+ message: "Interaction answer input is invalid",
200
+ retryable: false,
201
+ ...(typeof input.conversationId === "string"
202
+ ? { conversationId: input.conversationId }
203
+ : {}),
204
+ }),
205
+ };
206
+ }
207
+ const parsedInput = parsed.data;
208
+ this.#record({ operation: "answerInteraction", input: parsedInput });
209
+ const guarded = this.#guard("answerInteraction", parsedInput);
210
+ if (guarded !== undefined)
211
+ return guarded;
212
+ const held = await this.#waitForOperation("answerInteraction", parsedInput);
213
+ if (held?.failure !== undefined)
214
+ return held.failure;
215
+ if (parsedInput.conversationId !== this.#snapshot.conversation.id) {
216
+ return notFound(parsedInput.conversationId);
217
+ }
218
+ if (!isGatewayOperationSupported(this.#snapshot.capabilities, "answerInteraction")) {
219
+ return unsupported("Interaction answers are unavailable");
220
+ }
221
+ if (this.#snapshot.pendingInteraction?.requestId !==
222
+ parsedInput.answer.requestId ||
223
+ this.#snapshot.pendingInteraction.revision !== parsedInput.answer.revision) {
224
+ return {
225
+ ok: false,
226
+ error: chatErrorSchema.parse({
227
+ code: "conflict",
228
+ message: "The interaction request is no longer pending",
229
+ retryable: false,
230
+ conversationId: parsedInput.conversationId,
231
+ }),
232
+ };
233
+ }
234
+ return this.#answerInteractionResult;
235
+ }
236
+ async loadConversation(input) {
237
+ const parsedInput = loadConversationInputSchema.parse(input);
238
+ this.#record({ operation: "loadConversation", input: parsedInput });
239
+ const guarded = this.#guard("loadConversation", parsedInput);
240
+ if (guarded !== undefined)
241
+ return guarded;
242
+ const held = await this.#waitForOperation("loadConversation", parsedInput);
243
+ if (held?.failure !== undefined)
244
+ return held.failure;
245
+ if (parsedInput.conversationId !== this.#snapshot.conversation.id) {
246
+ return notFound(parsedInput.conversationId);
247
+ }
248
+ if (parsedInput.previousCursor !== undefined &&
249
+ !isGatewayOperationSupported(this.#snapshot.capabilities, "loadHistory")) {
250
+ return unsupported("History loading is unavailable");
251
+ }
252
+ return { ok: true, value: this.#snapshot };
253
+ }
254
+ async subscribe(input, observer) {
255
+ const parsedInput = subscribeConversationInputSchema.parse(input);
256
+ this.#record({ operation: "subscribe", input: parsedInput });
257
+ const guarded = this.#guard("subscribe", parsedInput);
258
+ if (guarded !== undefined)
259
+ return guarded;
260
+ const held = await this.#waitForOperation("subscribe", parsedInput);
261
+ if (held?.failure !== undefined)
262
+ return held.failure;
263
+ if (parsedInput.conversationId !== this.#snapshot.conversation.id) {
264
+ held?.transportSubscription?.dispose();
265
+ return notFound(parsedInput.conversationId);
266
+ }
267
+ if (!isGatewayOperationSupported(this.#snapshot.capabilities, "subscribe")) {
268
+ held?.transportSubscription?.dispose();
269
+ return unsupported("Live updates are unavailable");
270
+ }
271
+ const registration = {
272
+ active: true,
273
+ conversationId: parsedInput.conversationId,
274
+ observer,
275
+ ...(held?.transportSubscription === undefined
276
+ ? {}
277
+ : { transportSubscription: held.transportSubscription }),
278
+ };
279
+ this.#observers.add(registration);
280
+ return {
281
+ ok: true,
282
+ value: Object.freeze({
283
+ dispose: async (options) => {
284
+ registration.active = false;
285
+ this.#observers.delete(registration);
286
+ registration.transportSubscription?.dispose();
287
+ await this.#waitForCleanup("subscription.dispose", options);
288
+ },
289
+ }),
290
+ };
291
+ }
292
+ async sendText(input) {
293
+ const parsedInput = sendTextInputSchema.parse(input);
294
+ this.#record({ operation: "sendText", input: parsedInput });
295
+ const guarded = this.#guard("sendText", parsedInput);
296
+ if (guarded !== undefined)
297
+ return guarded;
298
+ const held = await this.#waitForOperation("sendText", parsedInput);
299
+ if (held?.failure !== undefined)
300
+ return held.failure;
301
+ if (parsedInput.conversationId !== this.#snapshot.conversation.id) {
302
+ return notFound(parsedInput.conversationId);
303
+ }
304
+ if (!isGatewayOperationSupported(this.#snapshot.capabilities, "sendText")) {
305
+ return unsupported("Text sending is unavailable");
306
+ }
307
+ return this.#sendTextResult;
308
+ }
309
+ async interrupt(input) {
310
+ const parsedInput = interruptRunInputSchema.parse(input);
311
+ this.#record({ operation: "interrupt", input: parsedInput });
312
+ const guarded = this.#guard("interrupt", parsedInput);
313
+ if (guarded !== undefined)
314
+ return guarded;
315
+ const held = await this.#waitForOperation("interrupt", parsedInput);
316
+ if (held?.failure !== undefined)
317
+ return held.failure;
318
+ if (parsedInput.conversationId !== this.#snapshot.conversation.id) {
319
+ return notFound(parsedInput.conversationId);
320
+ }
321
+ if (parsedInput.runId !== undefined &&
322
+ parsedInput.runId !== this.#snapshot.run?.id) {
323
+ return staleInterrupt(parsedInput.conversationId, parsedInput.runId);
324
+ }
325
+ if (!isGatewayOperationSupported(this.#snapshot.capabilities, "interrupt", this.#snapshot.run)) {
326
+ return unsupported("Run interruption is unavailable");
327
+ }
328
+ return this.#interruptResult;
329
+ }
330
+ dispose(options) {
331
+ if (this.#disposed)
332
+ return this.#disposePromise;
333
+ this.#disposed = true;
334
+ for (const registration of this.#observers) {
335
+ registration.active = false;
336
+ registration.transportSubscription?.dispose();
337
+ }
338
+ this.#observers.clear();
339
+ this.#disposePromise = this.#waitForCleanup("gateway.dispose", options);
340
+ return this.#disposePromise;
341
+ }
342
+ emitUpdate(update) {
343
+ return this.#deliverUpdate(update, true);
344
+ }
345
+ emitUpdateToAll(update) {
346
+ return this.#deliverUpdate(update, false);
347
+ }
348
+ #deliverUpdate(update, filterConversation) {
349
+ if (this.#disposed || !this.#connected)
350
+ return 0;
351
+ const parsed = chatUpdateSchema.parse(update);
352
+ const conversationId = updateConversationId(parsed);
353
+ let delivered = 0;
354
+ for (const registration of this.#observers) {
355
+ if (registration.active &&
356
+ (!filterConversation ||
357
+ conversationId === undefined ||
358
+ conversationId === registration.conversationId)) {
359
+ registration.observer.next(parsed);
360
+ delivered += 1;
361
+ }
362
+ }
363
+ return delivered;
364
+ }
365
+ emitError(error) {
366
+ if (this.#disposed)
367
+ return 0;
368
+ const parsed = chatErrorSchema.parse(error);
369
+ let delivered = 0;
370
+ for (const registration of this.#observers) {
371
+ if (registration.active &&
372
+ (parsed.conversationId === undefined ||
373
+ parsed.conversationId === registration.conversationId)) {
374
+ registration.observer.error?.(parsed);
375
+ delivered += 1;
376
+ }
377
+ }
378
+ return delivered;
379
+ }
380
+ disconnect(error) {
381
+ if (this.#disposed || !this.#connected)
382
+ return 0;
383
+ this.#connected = false;
384
+ return this.emitError(error);
385
+ }
386
+ reconnect() {
387
+ if (this.#disposed) {
388
+ throw new Error("Cannot reconnect a disposed Memory Gateway");
389
+ }
390
+ this.#connected = true;
391
+ }
392
+ failNext(operation, error) {
393
+ const failures = this.#failures.get(operation) ?? [];
394
+ failures.push(chatErrorSchema.parse(error));
395
+ this.#failures.set(operation, failures);
396
+ }
397
+ holdNext(point) {
398
+ const hold = new PendingHold(point);
399
+ const holds = this.#holds.get(point) ?? [];
400
+ holds.push(hold);
401
+ this.#holds.set(point, holds);
402
+ return hold;
403
+ }
404
+ now() {
405
+ return this.#clock.now();
406
+ }
407
+ advanceTimeTo(timestamp) {
408
+ this.#clock.advanceTo(timestamp);
409
+ }
410
+ setConversationPage(page) {
411
+ this.#conversationPage = conversationPageSchema.parse(page);
412
+ }
413
+ setAnswerInteractionResult(result) {
414
+ this.#answerInteractionResult = answerInteractionResultSchema.parse(result);
415
+ }
416
+ setInterruptResult(result) {
417
+ this.#interruptResult = interruptRunResultSchema.parse(result);
418
+ }
419
+ setSendTextResult(result) {
420
+ this.#sendTextResult = sendTextResultSchema.parse(result);
421
+ }
422
+ setSnapshot(snapshot) {
423
+ this.#snapshot = chatSnapshotSchema.parse(snapshot);
424
+ }
425
+ }
426
+ /** Creates one isolated, scriptable, network-free ChatGateway test harness. */
427
+ export const createMemoryChatGateway = (options = {}) => {
428
+ const fixtures = options.fixtures ?? createChatContractFixtures();
429
+ const implementation = new MemoryChatGateway(options, fixtures);
430
+ const controller = {
431
+ get calls() {
432
+ return implementation.calls;
433
+ },
434
+ get connected() {
435
+ return implementation.connected;
436
+ },
437
+ get disposed() {
438
+ return implementation.disposed;
439
+ },
440
+ disconnect: (error = fixtures.disconnectError) => implementation.disconnect(error),
441
+ emitError: (error) => implementation.emitError(error),
442
+ emitUpdate: (update) => implementation.emitUpdate(update),
443
+ emitUpdateToAll: (update) => implementation.emitUpdateToAll(update),
444
+ failNext: (operation, error) => implementation.failNext(operation, error),
445
+ holdNext: (point) => implementation.holdNext(point),
446
+ now: () => implementation.now(),
447
+ advanceTimeTo: (timestamp) => implementation.advanceTimeTo(timestamp),
448
+ reconnect: () => implementation.reconnect(),
449
+ setConversationPage: (page) => implementation.setConversationPage(page),
450
+ setAnswerInteractionResult: (result) => implementation.setAnswerInteractionResult(result),
451
+ setInterruptResult: (result) => implementation.setInterruptResult(result),
452
+ setSendTextResult: (result) => implementation.setSendTextResult(result),
453
+ setSnapshot: (snapshot) => implementation.setSnapshot(snapshot),
454
+ };
455
+ return Object.freeze({
456
+ controller: Object.freeze(controller),
457
+ fixtures,
458
+ gateway: implementation,
459
+ });
460
+ };
461
+ //# sourceMappingURL=memory-gateway.js.map