ankka 0.6.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.
- package/README.md +30 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +350 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.js +90 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +254 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.js +47 -0
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +105 -0
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +28 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +598 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +175 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +224 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +44 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +212 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +47 -0
- package/dist/_proto/ankka/protocol/v1/key_value_pb.d.ts +154 -0
- package/dist/_proto/ankka/protocol/v1/key_value_pb.js +36 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +276 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.js +110 -0
- package/dist/_proto/ankka/protocol/v1/timed_action_pb.d.ts +75 -0
- package/dist/_proto/ankka/protocol/v1/timed_action_pb.js +23 -0
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +93 -0
- package/dist/_proto/ankka/protocol/v1/view_pb.js +23 -0
- package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +298 -0
- package/dist/_proto/ankka/protocol/v1/workflow_pb.js +67 -0
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +36 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +239 -0
- package/dist/codec.d.ts +44 -0
- package/dist/codec.js +215 -0
- package/dist/consumer.d.ts +31 -0
- package/dist/consumer.js +32 -0
- package/dist/context.d.ts +61 -0
- package/dist/context.js +75 -0
- package/dist/effects/agent.d.ts +37 -0
- package/dist/effects/agent.js +48 -0
- package/dist/effects/common.d.ts +68 -0
- package/dist/effects/common.js +55 -0
- package/dist/effects/eventSourced.d.ts +50 -0
- package/dist/effects/eventSourced.js +66 -0
- package/dist/effects/keyValue.d.ts +36 -0
- package/dist/effects/keyValue.js +47 -0
- package/dist/effects/stateless.d.ts +40 -0
- package/dist/effects/stateless.js +35 -0
- package/dist/effects/workflow.d.ts +99 -0
- package/dist/effects/workflow.js +83 -0
- package/dist/endpoint.d.ts +21 -0
- package/dist/endpoint.js +29 -0
- package/dist/eventSourcedEntity.d.ts +40 -0
- package/dist/eventSourcedEntity.js +66 -0
- package/dist/handlers.d.ts +55 -0
- package/dist/handlers.js +64 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +39 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +362 -0
- package/dist/keyValueEntity.d.ts +29 -0
- package/dist/keyValueEntity.js +52 -0
- package/dist/kinds.d.ts +8 -0
- package/dist/kinds.js +41 -0
- package/dist/materialise.d.ts +33 -0
- package/dist/materialise.js +55 -0
- package/dist/routes.d.ts +71 -0
- package/dist/routes.js +79 -0
- package/dist/schema.d.ts +112 -0
- package/dist/schema.js +200 -0
- package/dist/server/agent.d.ts +14 -0
- package/dist/server/agent.js +113 -0
- package/dist/server/discovery.d.ts +5 -0
- package/dist/server/discovery.js +21 -0
- package/dist/server/eventSourced.d.ts +12 -0
- package/dist/server/eventSourced.js +168 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.js +153 -0
- package/dist/server/keyValue.d.ts +5 -0
- package/dist/server/keyValue.js +122 -0
- package/dist/server/payloads.d.ts +6 -0
- package/dist/server/payloads.js +11 -0
- package/dist/server/queue.d.ts +10 -0
- package/dist/server/queue.js +67 -0
- package/dist/server/server.d.ts +34 -0
- package/dist/server/server.js +101 -0
- package/dist/server/stateless.d.ts +9 -0
- package/dist/server/stateless.js +108 -0
- package/dist/server/workflow.d.ts +5 -0
- package/dist/server/workflow.js +244 -0
- package/dist/service.d.ts +147 -0
- package/dist/service.js +384 -0
- package/dist/spec.d.ts +7 -0
- package/dist/spec.js +106 -0
- package/dist/testkit/index.d.ts +3 -0
- package/dist/testkit/index.js +6 -0
- package/dist/testkit/integration.d.ts +60 -0
- package/dist/testkit/integration.js +284 -0
- package/dist/testkit/kinds.d.ts +133 -0
- package/dist/testkit/kinds.js +398 -0
- package/dist/testkit/unit.d.ts +54 -0
- package/dist/testkit/unit.js +224 -0
- package/dist/time.d.ts +73 -0
- package/dist/time.js +275 -0
- package/dist/timedAction.d.ts +20 -0
- package/dist/timedAction.js +28 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/view.d.ts +37 -0
- package/dist/view.js +52 -0
- package/dist/workflow.d.ts +33 -0
- package/dist/workflow.js +61 -0
- package/package.json +75 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// The integration testkit: this process, a real sidecar, a real Postgres.
|
|
2
|
+
//
|
|
3
|
+
// Starts Postgres with the platform's schema (copied out of the sidecar image, so a test can never pass
|
|
4
|
+
// against a schema the platform does not have), the `ankka-sidecar` image pointed at this process through
|
|
5
|
+
// `host.docker.internal`, and this process's server. `http` talks to the sidecar's HTTP port, where the
|
|
6
|
+
// declared routes are served; `restart` replaces the sidecar container against the same database, which
|
|
7
|
+
// is how a test proves durability rather than caching.
|
|
8
|
+
//
|
|
9
|
+
// Needs Docker, and `testcontainers` with `@testcontainers/postgresql` installed (optional peers of this
|
|
10
|
+
// package). The image is `$ANKKA_SIDECAR_IMAGE` or `ankka-sidecar:latest`.
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { chmodSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
const POSTGRES_IMAGE = "postgres:17-alpine";
|
|
16
|
+
const HTTP_PORT = 9000;
|
|
17
|
+
const CALLBACK_PORT = 9011;
|
|
18
|
+
/** What `http` answers with. */
|
|
19
|
+
export class HttpResponse {
|
|
20
|
+
status;
|
|
21
|
+
headers;
|
|
22
|
+
body;
|
|
23
|
+
constructor(status, headers, body) {
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.headers = headers;
|
|
26
|
+
this.body = body;
|
|
27
|
+
Object.freeze(this);
|
|
28
|
+
}
|
|
29
|
+
text() {
|
|
30
|
+
return new TextDecoder().decode(this.body);
|
|
31
|
+
}
|
|
32
|
+
json() {
|
|
33
|
+
return JSON.parse(this.text());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** A small HTTP client over `fetch` for the sidecar's routes. */
|
|
37
|
+
export class Http {
|
|
38
|
+
baseUrl;
|
|
39
|
+
constructor(baseUrl) {
|
|
40
|
+
this.baseUrl = baseUrl;
|
|
41
|
+
Object.freeze(this);
|
|
42
|
+
}
|
|
43
|
+
async request(method, path, body, headers = {}) {
|
|
44
|
+
const init = { method, headers: { ...headers } };
|
|
45
|
+
if (body !== undefined) {
|
|
46
|
+
if (body instanceof Uint8Array)
|
|
47
|
+
init.body = body;
|
|
48
|
+
else if (typeof body === "string") {
|
|
49
|
+
init.body = body;
|
|
50
|
+
init.headers["content-type"] ??= "text/plain; charset=utf-8";
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
init.body = JSON.stringify(body);
|
|
54
|
+
init.headers["content-type"] ??= "application/json";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const r = await fetch(this.baseUrl + path, init);
|
|
58
|
+
return new HttpResponse(r.status, r.headers, new Uint8Array(await r.arrayBuffer()));
|
|
59
|
+
}
|
|
60
|
+
get(path, headers) {
|
|
61
|
+
return this.request("GET", path, undefined, headers);
|
|
62
|
+
}
|
|
63
|
+
post(path, body, headers) {
|
|
64
|
+
return this.request("POST", path, body, headers);
|
|
65
|
+
}
|
|
66
|
+
put(path, body, headers) {
|
|
67
|
+
return this.request("PUT", path, body, headers);
|
|
68
|
+
}
|
|
69
|
+
delete(path, headers) {
|
|
70
|
+
return this.request("DELETE", path, undefined, headers);
|
|
71
|
+
}
|
|
72
|
+
/** The frames of an SSE response, each `data:` line decoded from its JSON. */
|
|
73
|
+
async sse(path, headers) {
|
|
74
|
+
const r = await this.get(path, headers);
|
|
75
|
+
return r
|
|
76
|
+
.text()
|
|
77
|
+
.split("\n")
|
|
78
|
+
.filter((line) => line.startsWith("data:"))
|
|
79
|
+
.map((line) => JSON.parse(line.slice(5).trim()));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function sidecarImage() {
|
|
83
|
+
return process.env.ANKKA_SIDECAR_IMAGE ?? "ankka-sidecar:latest";
|
|
84
|
+
}
|
|
85
|
+
/** The DDL as files, from `/opt/docker/ddl` in the sidecar image, without running the image. */
|
|
86
|
+
function copyDdl(image, into) {
|
|
87
|
+
const id = execFileSync("docker", ["create", image], { encoding: "utf8" }).trim();
|
|
88
|
+
try {
|
|
89
|
+
execFileSync("docker", ["cp", `${id}:/opt/docker/ddl/.`, into], { stdio: "pipe" });
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
try {
|
|
93
|
+
execFileSync("docker", ["rm", "-f", id], { stdio: "ignore" });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// the container may already be gone
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Readable by the container's own `postgres` user: the entrypoint lists the directory under `set -e`
|
|
100
|
+
// before initdb, and a private directory fails that check on Linux (Docker Desktop hides it).
|
|
101
|
+
chmodSync(into, 0o755);
|
|
102
|
+
for (const f of readdirSync(into))
|
|
103
|
+
if (statSync(join(into, f)).isFile())
|
|
104
|
+
chmodSync(join(into, f), 0o644);
|
|
105
|
+
}
|
|
106
|
+
async function logsOf(container) {
|
|
107
|
+
if (!container)
|
|
108
|
+
return "";
|
|
109
|
+
try {
|
|
110
|
+
const stream = await container.logs();
|
|
111
|
+
return await new Promise((resolve) => {
|
|
112
|
+
let out = "";
|
|
113
|
+
stream.on("data", (line) => (out += line));
|
|
114
|
+
stream.on("end", () => resolve(out));
|
|
115
|
+
stream.on("error", () => resolve(out));
|
|
116
|
+
setTimeout(() => resolve(out), 2000);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return "(no logs could be read)";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export class AnkkaTestKit {
|
|
124
|
+
#service;
|
|
125
|
+
#image;
|
|
126
|
+
#env;
|
|
127
|
+
#postgresImage;
|
|
128
|
+
#readyTimeoutMs;
|
|
129
|
+
#log;
|
|
130
|
+
#ddlDir;
|
|
131
|
+
#network;
|
|
132
|
+
#postgres;
|
|
133
|
+
#sidecar;
|
|
134
|
+
#server;
|
|
135
|
+
#processPort = 0;
|
|
136
|
+
#http;
|
|
137
|
+
/** The component client every handler in `service` uses, pointed at the sidecar's mapped callback port. */
|
|
138
|
+
client;
|
|
139
|
+
constructor(service, options) {
|
|
140
|
+
this.#service = service;
|
|
141
|
+
this.#image = options.image ?? sidecarImage();
|
|
142
|
+
this.#env = options.env ?? {};
|
|
143
|
+
this.#postgresImage = options.postgresImage ?? POSTGRES_IMAGE;
|
|
144
|
+
this.#readyTimeoutMs = options.readyTimeoutMs ?? 120_000;
|
|
145
|
+
this.#log = options.log ?? (() => { });
|
|
146
|
+
this.client = service.client;
|
|
147
|
+
}
|
|
148
|
+
/** Starts Postgres, this process's server and the sidecar; a start that fails half-way stops what it started. */
|
|
149
|
+
static async start(service, options = {}) {
|
|
150
|
+
const kit = new AnkkaTestKit(service, options);
|
|
151
|
+
try {
|
|
152
|
+
await kit.#start();
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
const logs = await logsOf(kit.#sidecar);
|
|
156
|
+
await kit.stop();
|
|
157
|
+
if (logs)
|
|
158
|
+
throw new Error(`${e instanceof Error ? e.message : String(e)}\n--- sidecar logs ---\n${logs.slice(-4000)}`, { cause: e });
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
161
|
+
return kit;
|
|
162
|
+
}
|
|
163
|
+
/** The sidecar's HTTP surface: the routes this service declared. */
|
|
164
|
+
get http() {
|
|
165
|
+
if (!this.#http)
|
|
166
|
+
throw new Error("the testkit is not started");
|
|
167
|
+
return this.#http;
|
|
168
|
+
}
|
|
169
|
+
get baseUrl() {
|
|
170
|
+
return this.http.baseUrl;
|
|
171
|
+
}
|
|
172
|
+
/** The JDBC URL of the throwaway database. */
|
|
173
|
+
get jdbcUrl() {
|
|
174
|
+
if (!this.#postgres)
|
|
175
|
+
throw new Error("the testkit is not started");
|
|
176
|
+
return `jdbc:postgresql://${this.#postgres.getHost()}:${this.#postgres.getPort()}/${this.#postgres.getDatabase()}`;
|
|
177
|
+
}
|
|
178
|
+
async #start() {
|
|
179
|
+
const tc = await import("testcontainers");
|
|
180
|
+
const pg = await import("@testcontainers/postgresql");
|
|
181
|
+
this.#ddlDir = mkdtempSync(join(tmpdir(), "ankka-ddl-"));
|
|
182
|
+
copyDdl(this.#image, this.#ddlDir);
|
|
183
|
+
this.#network = await new tc.Network().start();
|
|
184
|
+
this.#postgres = await new pg.PostgreSqlContainer(this.#postgresImage)
|
|
185
|
+
.withDatabase("ankka")
|
|
186
|
+
.withUsername("ankka")
|
|
187
|
+
.withPassword("ankka")
|
|
188
|
+
.withNetwork(this.#network)
|
|
189
|
+
.withNetworkAliases("postgres")
|
|
190
|
+
.withCopyDirectoriesToContainer([{ source: this.#ddlDir, target: "/docker-entrypoint-initdb.d", mode: 0o755 }])
|
|
191
|
+
.start();
|
|
192
|
+
// This process's server, on all interfaces: the sidecar is in a container and dials in.
|
|
193
|
+
this.#server = this.#service.server({ host: "0.0.0.0", port: 0, log: this.#log });
|
|
194
|
+
this.#processPort = (await this.#server.start()).port;
|
|
195
|
+
await this.#startSidecar();
|
|
196
|
+
}
|
|
197
|
+
async #startSidecar() {
|
|
198
|
+
const tc = await import("testcontainers");
|
|
199
|
+
const container = await new tc.GenericContainer(this.#image)
|
|
200
|
+
.withNetwork(this.#network)
|
|
201
|
+
.withExtraHosts([{ host: "host.docker.internal", ipAddress: "host-gateway" }])
|
|
202
|
+
.withEnvironment({
|
|
203
|
+
ANKKA_PROCESS_ADDRESS: `host.docker.internal:${this.#processPort}`,
|
|
204
|
+
ANKKA_SIDECAR_BIND: "0.0.0.0",
|
|
205
|
+
ANKKA_HTTP_PORT: String(HTTP_PORT),
|
|
206
|
+
ANKKA_DB_HOST: "postgres",
|
|
207
|
+
ANKKA_DB_PORT: "5432",
|
|
208
|
+
ANKKA_DB_NAME: "ankka",
|
|
209
|
+
ANKKA_DB_USER: "ankka",
|
|
210
|
+
ANKKA_DB_PASSWORD: "ankka",
|
|
211
|
+
...this.#env,
|
|
212
|
+
})
|
|
213
|
+
.withExposedPorts(HTTP_PORT, CALLBACK_PORT)
|
|
214
|
+
.withWaitStrategy(tc.Wait.forHttp("/_ankka/health", HTTP_PORT).forStatusCode(200))
|
|
215
|
+
.withStartupTimeout(this.#readyTimeoutMs)
|
|
216
|
+
.start();
|
|
217
|
+
this.#sidecar = container;
|
|
218
|
+
this.client.reconnect(`${container.getHost()}:${container.getMappedPort(CALLBACK_PORT)}`);
|
|
219
|
+
this.#http = new Http(`http://${container.getHost()}:${container.getMappedPort(HTTP_PORT)}`);
|
|
220
|
+
}
|
|
221
|
+
/** Replaces the sidecar against the same database: every instance is gone from memory, so the next read rebuilds from the journal. */
|
|
222
|
+
async restart() {
|
|
223
|
+
if (this.#sidecar) {
|
|
224
|
+
await this.#sidecar.stop();
|
|
225
|
+
this.#sidecar = undefined;
|
|
226
|
+
}
|
|
227
|
+
await this.#startSidecar();
|
|
228
|
+
}
|
|
229
|
+
/** Starts another image against this kit's Postgres — the Scala cart, say, to prove the journal is shared. */
|
|
230
|
+
async startBeside(image, env = {}, httpPort = HTTP_PORT) {
|
|
231
|
+
const tc = await import("testcontainers");
|
|
232
|
+
const container = await new tc.GenericContainer(image)
|
|
233
|
+
.withNetwork(this.#network)
|
|
234
|
+
.withEnvironment({
|
|
235
|
+
ANKKA_HTTP_PORT: String(httpPort),
|
|
236
|
+
ANKKA_DB_HOST: "postgres",
|
|
237
|
+
ANKKA_DB_PORT: "5432",
|
|
238
|
+
ANKKA_DB_NAME: "ankka",
|
|
239
|
+
ANKKA_DB_USER: "ankka",
|
|
240
|
+
ANKKA_DB_PASSWORD: "ankka",
|
|
241
|
+
...env,
|
|
242
|
+
})
|
|
243
|
+
.withExposedPorts(httpPort)
|
|
244
|
+
.withWaitStrategy(tc.Wait.forHttp("/_ankka/health", httpPort).forStatusCode(200))
|
|
245
|
+
.withStartupTimeout(this.#readyTimeoutMs)
|
|
246
|
+
.start();
|
|
247
|
+
return {
|
|
248
|
+
http: new Http(`http://${container.getHost()}:${container.getMappedPort(httpPort)}`),
|
|
249
|
+
logs: () => logsOf(container),
|
|
250
|
+
stop: async () => {
|
|
251
|
+
await container.stop();
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** The sidecar container's log so far. */
|
|
256
|
+
sidecarLogs() {
|
|
257
|
+
return logsOf(this.#sidecar);
|
|
258
|
+
}
|
|
259
|
+
async stop() {
|
|
260
|
+
if (this.#server) {
|
|
261
|
+
await this.#server.stop().catch(() => undefined);
|
|
262
|
+
this.#server = undefined;
|
|
263
|
+
}
|
|
264
|
+
if (this.#sidecar) {
|
|
265
|
+
await this.#sidecar.stop().catch(() => undefined);
|
|
266
|
+
this.#sidecar = undefined;
|
|
267
|
+
}
|
|
268
|
+
if (this.#postgres) {
|
|
269
|
+
await this.#postgres.stop().catch(() => undefined);
|
|
270
|
+
this.#postgres = undefined;
|
|
271
|
+
}
|
|
272
|
+
if (this.#network) {
|
|
273
|
+
await this.#network.stop().catch(() => undefined);
|
|
274
|
+
this.#network = undefined;
|
|
275
|
+
}
|
|
276
|
+
if (this.#ddlDir) {
|
|
277
|
+
rmSync(this.#ddlDir, { recursive: true, force: true });
|
|
278
|
+
this.#ddlDir = undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async [Symbol.asyncDispose]() {
|
|
282
|
+
await this.stop();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ComponentClient } from "../client.ts";
|
|
2
|
+
import { type ErrorDetail, type Metadata } from "../effects/common.ts";
|
|
3
|
+
import type { StepRef } from "../effects/workflow.ts";
|
|
4
|
+
import type { ConsumerEffect, TimedActionEffect, ViewEffect } from "../effects/stateless.ts";
|
|
5
|
+
import type { KeyValueEntity, KeyValueEntityClass } from "../keyValueEntity.ts";
|
|
6
|
+
import type { Workflow, WorkflowClass } from "../workflow.ts";
|
|
7
|
+
import type { View, ViewClass } from "../view.ts";
|
|
8
|
+
import type { Consumer, ConsumerClass } from "../consumer.ts";
|
|
9
|
+
import type { TimedAction, TimedActionClass } from "../timedAction.ts";
|
|
10
|
+
import type { Agent, AgentClass } from "../agent.ts";
|
|
11
|
+
import type { HandlerRef } from "../handlers.ts";
|
|
12
|
+
import { type MaterialisedKeyValue, type MaterialisedStep, type MaterialisedWorkflowCommand } from "../materialise.ts";
|
|
13
|
+
/** Drives one key value entity instance through its handlers. */
|
|
14
|
+
export declare class KeyValueTestKit<S, C extends KeyValueEntity<S>> {
|
|
15
|
+
#private;
|
|
16
|
+
readonly entityId: string;
|
|
17
|
+
state: S;
|
|
18
|
+
private constructor();
|
|
19
|
+
static of<S, C extends KeyValueEntity<S>>(cls: KeyValueEntityClass<S, C>, entityId?: string, client?: ComponentClient): KeyValueTestKit<S, C>;
|
|
20
|
+
call<I, R>(handler: HandlerRef<C, I, R, any> | string, input?: I, metadata?: Metadata): Promise<MaterialisedKeyValue<S, R>>;
|
|
21
|
+
}
|
|
22
|
+
/** What a workflow is doing, as the kit tracks it between calls. */
|
|
23
|
+
export interface WorkflowProgress {
|
|
24
|
+
/** The step the last effect asked to run next, not yet run. */
|
|
25
|
+
readonly pending: StepRef | null;
|
|
26
|
+
/** Set while the workflow is paused; `resume()` runs `onTimeout` as the engine would after `after`. */
|
|
27
|
+
readonly paused: {
|
|
28
|
+
readonly after: number | null;
|
|
29
|
+
readonly onTimeout: StepRef | null;
|
|
30
|
+
} | null;
|
|
31
|
+
readonly ended: boolean;
|
|
32
|
+
readonly failed: ErrorDetail | null;
|
|
33
|
+
}
|
|
34
|
+
/** Drives one workflow instance: commands, steps, and the transitions between them, stopping at a pause. */
|
|
35
|
+
export declare class WorkflowTestKit<S, C extends Workflow<S>> {
|
|
36
|
+
#private;
|
|
37
|
+
readonly entityId: string;
|
|
38
|
+
state: S;
|
|
39
|
+
private constructor();
|
|
40
|
+
static of<S, C extends Workflow<S>>(cls: WorkflowClass<S, C>, entityId?: string, client?: ComponentClient): WorkflowTestKit<S, C>;
|
|
41
|
+
get progress(): WorkflowProgress;
|
|
42
|
+
call<I, R>(handler: HandlerRef<C, I, R, any> | string, input?: I, metadata?: Metadata): Promise<MaterialisedWorkflowCommand<S, R>>;
|
|
43
|
+
/** Runs one step, by name or declaration, with an input (or the pending transition's). */
|
|
44
|
+
runStep(step: HandlerRef<C, any, any, any> | string, input?: unknown): Promise<MaterialisedStep<S>>;
|
|
45
|
+
/** Follows transitions until the workflow ends or pauses. Returns the progress reached. */
|
|
46
|
+
runUntilEnd(limit?: number): Promise<WorkflowProgress>;
|
|
47
|
+
/** As the engine would when a pause's timeout passes: runs `onTimeout`, then follows transitions. */
|
|
48
|
+
resume(): Promise<WorkflowProgress>;
|
|
49
|
+
}
|
|
50
|
+
/** Feeds a view events by source key and keeps the rows as the sidecar would. */
|
|
51
|
+
export declare class ViewTestKit<E, Row, C extends View<E, Row>> {
|
|
52
|
+
#private;
|
|
53
|
+
readonly rows: Map<string, Row>;
|
|
54
|
+
private constructor();
|
|
55
|
+
static of<E, Row, C extends View<E, Row>>(cls: ViewClass<E, Row, C>, client?: ComponentClient): ViewTestKit<E, Row, C>;
|
|
56
|
+
/** A change from source instance `key`: the event, round-tripped through the source's event codec. */
|
|
57
|
+
onChange(key: string, event: E, metadata?: Metadata): Promise<ViewEffect<Row>>;
|
|
58
|
+
/** The source instance `key` was deleted. */
|
|
59
|
+
onDelete(key: string, metadata?: Metadata): Promise<ViewEffect<Row>>;
|
|
60
|
+
get(key: string): Row | null;
|
|
61
|
+
}
|
|
62
|
+
/** Feeds a consumer messages and collects what it produced. */
|
|
63
|
+
export declare class ConsumerTestKit<M, Out, C extends Consumer<M, Out>> {
|
|
64
|
+
#private;
|
|
65
|
+
/** Everything `produce` sent, round-tripped through the out codec. */
|
|
66
|
+
readonly produced: {
|
|
67
|
+
readonly payload: Out;
|
|
68
|
+
readonly metadata: Metadata;
|
|
69
|
+
}[];
|
|
70
|
+
private constructor();
|
|
71
|
+
static of<M, Out, C extends Consumer<M, Out>>(cls: ConsumerClass<M, Out, C>, client?: ComponentClient): ConsumerTestKit<M, Out, C>;
|
|
72
|
+
/** A message from source instance `subject`. */
|
|
73
|
+
onMessage(message: M, subject?: string, metadata?: Metadata): Promise<ConsumerEffect<Out>>;
|
|
74
|
+
onDelete(subject?: string, metadata?: Metadata): Promise<ConsumerEffect<Out>>;
|
|
75
|
+
}
|
|
76
|
+
/** Invokes a timed action's handlers as the sweeper would. */
|
|
77
|
+
export declare class TimedActionTestKit<C extends TimedAction> {
|
|
78
|
+
#private;
|
|
79
|
+
private constructor();
|
|
80
|
+
static of<C extends TimedAction>(cls: TimedActionClass<C>, client?: ComponentClient): TimedActionTestKit<C>;
|
|
81
|
+
invoke<I>(action: HandlerRef<C, I, any, any> | string, input?: I, metadata?: Metadata): Promise<TimedActionEffect>;
|
|
82
|
+
}
|
|
83
|
+
/** What a scripted model answers next. */
|
|
84
|
+
export type ModelResponse = {
|
|
85
|
+
readonly kind: "text";
|
|
86
|
+
readonly text: string;
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: "tool-call";
|
|
89
|
+
readonly tool: string;
|
|
90
|
+
readonly argumentsJson: string;
|
|
91
|
+
} | {
|
|
92
|
+
readonly kind: "refusal";
|
|
93
|
+
readonly message: string;
|
|
94
|
+
};
|
|
95
|
+
/** What the model saw when it answered: the kit records one per model call. */
|
|
96
|
+
export interface ModelCall {
|
|
97
|
+
readonly system: string | null;
|
|
98
|
+
readonly messages: readonly {
|
|
99
|
+
readonly role: "user" | "assistant" | "tool";
|
|
100
|
+
readonly text: string;
|
|
101
|
+
}[];
|
|
102
|
+
readonly tools: readonly string[];
|
|
103
|
+
}
|
|
104
|
+
/** A model that answers from a script and fails loudly when the script runs out. */
|
|
105
|
+
export declare class ScriptedModel {
|
|
106
|
+
#private;
|
|
107
|
+
readonly calls: ModelCall[];
|
|
108
|
+
expectText(text: string): this;
|
|
109
|
+
expectToolCall(tool: string, args?: Record<string, unknown> | string): this;
|
|
110
|
+
expectRefusal(message: string): this;
|
|
111
|
+
get remaining(): number;
|
|
112
|
+
/** @internal */
|
|
113
|
+
_next(call: ModelCall): ModelResponse;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Runs an agent's turn the way the sidecar does, in process: the plan, input guardrails, the model
|
|
117
|
+
* (scripted), tool calls back into the agent, output guardrails, and the session's history.
|
|
118
|
+
*/
|
|
119
|
+
export declare class AgentTestKit<C extends Agent> {
|
|
120
|
+
#private;
|
|
121
|
+
readonly sessionId: string;
|
|
122
|
+
/** The session's memory: what the user said and the agent answered, turn after turn. */
|
|
123
|
+
readonly history: {
|
|
124
|
+
role: "user" | "assistant";
|
|
125
|
+
text: string;
|
|
126
|
+
}[];
|
|
127
|
+
private constructor();
|
|
128
|
+
static of<C extends Agent>(cls: AgentClass<C>, sessionId: string, model: ScriptedModel, client?: ComponentClient): AgentTestKit<C>;
|
|
129
|
+
/** One turn: returns the reply text (JSON text for a `thenReplyJson` plan). A refusal or a blocked guardrail rejects with `CommandError`. */
|
|
130
|
+
ask<I>(handler: HandlerRef<C, I, any, any> | string, input?: I): Promise<string>;
|
|
131
|
+
/** A streaming handler's turn, token by token: the reply split on spaces, as a model streams words. */
|
|
132
|
+
stream<I>(handler: HandlerRef<C, I, any, any> | string, input?: I): AsyncIterable<string>;
|
|
133
|
+
}
|