@anteros/core 0.0.40 → 0.0.42
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/database/audit.ts +143 -0
- package/database/collection.ts +6 -4
- package/database/deleteLog.ts +98 -0
- package/database/file.ts +7 -6
- package/database/mongodbadapter.ts +420 -185
- package/database/replication.ts +858 -0
- package/database/schema.ts +140 -125
- package/database/tenant.ts +4 -0
- package/database/ttl.ts +68 -0
- package/database/vars.ts +405 -0
- package/database/workflow.ts +466 -240
- package/index.ts +16 -0
- package/lib/agent.ts +1327 -0
- package/lib/agentMemory.ts +586 -0
- package/lib/agents.ts +157 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/audit.ts +271 -0
- package/lib/auditResult.ts +133 -0
- package/lib/define.ts +62 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +66 -12
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +147 -0
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +867 -0
- package/lib/redact.ts +117 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- package/lib/sockets.ts +3 -1
- package/lib/workflow.ts +209 -37
- package/lib/zip.ts +109 -0
- package/package.json +1 -1
- package/server/access.ts +96 -0
- package/server/agents.ts +451 -0
- package/server/api.ts +121 -95
- package/server/boot.ts +196 -53
- package/server/flags.ts +136 -0
- package/server/hono.ts +9 -4
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-working-memory.test.ts +402 -0
- package/tests/agent.test.ts +1239 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-file.test.ts +296 -0
- package/tests/audit-operations.test.ts +184 -0
- package/tests/audit-redact.test.ts +259 -0
- package/tests/audit-results.test.ts +188 -0
- package/tests/audit-retention-config.test.ts +76 -0
- package/tests/audit-run-action.test.ts +120 -0
- package/tests/audit.test.ts +320 -0
- package/tests/db-cache.test.ts +7 -7
- package/tests/endpoints.test.ts +80 -0
- package/tests/file-collection-replication.test.ts +98 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
- package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
- package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
- package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
- package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
- package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
- package/tests/fixtures/attachment/data.csv +2 -0
- package/tests/fixtures/fake-provider.ts +128 -0
- package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
- package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
- package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
- package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
- package/tests/fixtures/repl-only/app.ts +26 -0
- package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
- package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
- package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
- package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
- package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
- package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
- package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +9 -0
- package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
- package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
- package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
- package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
- package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
- package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
- package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
- package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
- package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
- package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
- package/tests/hono.test.ts +6 -3
- package/tests/lifecycle.test.ts +96 -0
- package/tests/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-control.test.ts +128 -0
- package/tests/replication-initial-sync.test.ts +86 -0
- package/tests/replication-meta.test.ts +243 -0
- package/tests/replication-only.test.ts +358 -0
- package/tests/replication.test.ts +188 -0
- package/tests/upload.test.ts +141 -8
- package/tests/vars-api.test.ts +123 -0
- package/tests/vars-replication.test.ts +96 -0
- package/tests/vars.test.ts +216 -0
- package/tests/workflow-engine.test.ts +340 -0
- package/tests/workflow-retention.test.ts +103 -0
- package/tests/workflow.test.ts +186 -0
- package/types/agent.d.ts +837 -0
- package/types/collection.d.ts +3 -0
- package/types/config.d.ts +80 -0
- package/types/file.d.ts +11 -8
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +77 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +228 -0
- package/types/route.d.ts +7 -1
- package/types/scripts.d.ts +6 -0
- package/types/service.d.ts +7 -1
- package/types/tenant.d.ts +36 -0
- package/types/vars.d.ts +147 -0
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
- package/utils/func.ts +67 -52
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { MongoClient } from "mongodb";
|
|
3
|
+
import { useRest } from "../database/rest";
|
|
4
|
+
import { formatConfig, cfg } from "../server/config";
|
|
5
|
+
import { startReplication, stopReplication, replicateTenant } from "../database/replication";
|
|
6
|
+
|
|
7
|
+
const TEST_TENANT = "files-repl";
|
|
8
|
+
const SRC_DB = "mongodb://localhost:27017/_FILES_REPL_SRC";
|
|
9
|
+
const DEST_DB = "mongodb://localhost:27017/_FILES_REPL_DEST";
|
|
10
|
+
const SLUG = "photos";
|
|
11
|
+
|
|
12
|
+
let rest: InstanceType<typeof useRest>;
|
|
13
|
+
let sourceClient: MongoClient;
|
|
14
|
+
let destClient: MongoClient;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
formatConfig({
|
|
18
|
+
server: { port: 4000 },
|
|
19
|
+
tenants: [{
|
|
20
|
+
id: TEST_TENANT,
|
|
21
|
+
dir: "src",
|
|
22
|
+
database: { uri: SRC_DB },
|
|
23
|
+
replication: {
|
|
24
|
+
runOnBoot: false,
|
|
25
|
+
schedule: { interval: "1h" },
|
|
26
|
+
destinations: [{ id: "backup", uri: DEST_DB }],
|
|
27
|
+
},
|
|
28
|
+
}],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
sourceClient = new MongoClient(SRC_DB, { serverSelectionTimeoutMS: 3000 });
|
|
32
|
+
await sourceClient.connect();
|
|
33
|
+
const tenant: any = cfg.tenants?.[0];
|
|
34
|
+
tenant.database.client = sourceClient;
|
|
35
|
+
tenant.database.db = sourceClient.db();
|
|
36
|
+
|
|
37
|
+
(cfg as any).fileCollections = [{
|
|
38
|
+
_tenant_: TEST_TENANT,
|
|
39
|
+
_isFileCollection_: true,
|
|
40
|
+
slug: SLUG,
|
|
41
|
+
fields: [{ name: "label", type: "string" }],
|
|
42
|
+
api: { access: { "*": true } },
|
|
43
|
+
// Opt the file collection's documents into the data replication engine.
|
|
44
|
+
replication: { enabled: true },
|
|
45
|
+
}];
|
|
46
|
+
|
|
47
|
+
await startReplication();
|
|
48
|
+
|
|
49
|
+
rest = new useRest({ tenant_id: TEST_TENANT });
|
|
50
|
+
destClient = new MongoClient(DEST_DB, { serverSelectionTimeoutMS: 3000 });
|
|
51
|
+
await destClient.connect();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await stopReplication();
|
|
56
|
+
try { await sourceClient.db().dropDatabase(); } catch (_) {}
|
|
57
|
+
try { await destClient.db().dropDatabase(); } catch (_) {}
|
|
58
|
+
try { await destClient.close(); } catch (_) {}
|
|
59
|
+
try { await sourceClient.close(); } catch (_) {}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const destDoc = (label: string) => destClient.db().collection(SLUG).findOne({ label });
|
|
63
|
+
|
|
64
|
+
describe("file collection replication", () => {
|
|
65
|
+
it("replicates file-collection documents", async () => {
|
|
66
|
+
await rest.insertOne(SLUG, { label: "a", _file: { filename: "a.png" } });
|
|
67
|
+
await replicateTenant(TEST_TENANT);
|
|
68
|
+
|
|
69
|
+
const dest = await destDoc("a");
|
|
70
|
+
expect(dest).toBeDefined();
|
|
71
|
+
expect((dest as any)._file.filename).toBe("a.png");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("propagates deletions via tombstones", async () => {
|
|
75
|
+
const doc: any = await rest.insertOne(SLUG, { label: "del-me" });
|
|
76
|
+
await replicateTenant(TEST_TENANT);
|
|
77
|
+
expect(await destDoc("del-me")).toBeDefined();
|
|
78
|
+
|
|
79
|
+
await rest.deleteOne(SLUG, doc._id);
|
|
80
|
+
await replicateTenant(TEST_TENANT);
|
|
81
|
+
expect(await destDoc("del-me")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("does not replicate a file collection that did not opt in", async () => {
|
|
85
|
+
(cfg as any).fileCollections.push({
|
|
86
|
+
_tenant_: TEST_TENANT,
|
|
87
|
+
_isFileCollection_: true,
|
|
88
|
+
slug: "private-files",
|
|
89
|
+
fields: [],
|
|
90
|
+
api: { access: { "*": true } },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await rest.insertOne("private-files", { label: "secret" });
|
|
94
|
+
await replicateTenant(TEST_TENANT);
|
|
95
|
+
|
|
96
|
+
expect(await destClient.db().collection("private-files").findOne({ label: "secret" })).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/** A second agent of the fixture tenant — no tool, no memory. */
|
|
4
|
+
export default define.Agent({
|
|
5
|
+
id: "support",
|
|
6
|
+
description: "Answers support questions.",
|
|
7
|
+
instructions: "You are a support agent.",
|
|
8
|
+
provider: { model: "claude-test", compatible: "anthropic", apiKey: "fixture-key" },
|
|
9
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { define, v } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loader fixture — `{tenant.dir}/agents/**\/*.agent.ts`.
|
|
5
|
+
* No call is ever made against the provider here: only the loader is exercised.
|
|
6
|
+
*/
|
|
7
|
+
export default define.Agent({
|
|
8
|
+
id: "weather",
|
|
9
|
+
name: "Weather Agent",
|
|
10
|
+
description: "Answers weather questions with a tool call",
|
|
11
|
+
instructions: "You are a concise weather assistant.",
|
|
12
|
+
provider: { model: "test-model", compatible: "openai", apiKey: "fixture-key" },
|
|
13
|
+
tools: {
|
|
14
|
+
forecast: define.Tool({
|
|
15
|
+
id: "forecast",
|
|
16
|
+
description: "Current weather for a city",
|
|
17
|
+
inputSchema: v.object({ city: v.string().required() }),
|
|
18
|
+
execute: async ({ city }) => ({ city, celsius: 21 }),
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { define, v, agents } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP-API fixture — exposed through `POST /api/:tenant_id/agents/:agent/:action`.
|
|
5
|
+
*
|
|
6
|
+
* The provider endpoint comes from the environment so the test can point it at
|
|
7
|
+
* its own fake provider (the port is chosen by the OS).
|
|
8
|
+
*/
|
|
9
|
+
export default define.Agent({
|
|
10
|
+
id: "assistant",
|
|
11
|
+
name: "Assistant",
|
|
12
|
+
description: "Fixture agent for the HTTP API",
|
|
13
|
+
instructions: "You are a fixture agent.",
|
|
14
|
+
provider: {
|
|
15
|
+
model: "test-model",
|
|
16
|
+
compatible: "openai",
|
|
17
|
+
apiKey: "test-key",
|
|
18
|
+
baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
|
|
19
|
+
options: { retries: 0 },
|
|
20
|
+
},
|
|
21
|
+
maxSteps: 3,
|
|
22
|
+
memory: new agents.memory.InMemory(),
|
|
23
|
+
tools: {
|
|
24
|
+
echo: define.Tool({
|
|
25
|
+
id: "echo",
|
|
26
|
+
description: "Echoes the given text",
|
|
27
|
+
inputSchema: v.object({ text: v.string().required() }),
|
|
28
|
+
execute: async ({ text }) => ({ echoed: text }),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
api: {
|
|
32
|
+
access: {
|
|
33
|
+
// public metadata — no token needed
|
|
34
|
+
info: true,
|
|
35
|
+
generate: (ctx) => !!ctx.token.value,
|
|
36
|
+
stream: (ctx) => !!ctx.token.value,
|
|
37
|
+
object: (ctx) => !!ctx.token.value,
|
|
38
|
+
history: (ctx) => !!ctx.token.value,
|
|
39
|
+
threads: (ctx) => !!ctx.token.value,
|
|
40
|
+
clear: (ctx) => !!ctx.token.value,
|
|
41
|
+
},
|
|
42
|
+
object: {
|
|
43
|
+
schema: v.object({ city: v.string().required(), celsius: v.number().required() }),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public agent — `generate` is allowed without a token. Declares no memory and
|
|
5
|
+
* no `api.object`, to exercise `AGENT_NO_MEMORY` / `AGENT_NO_OBJECT_SCHEMA`.
|
|
6
|
+
*/
|
|
7
|
+
export default define.Agent({
|
|
8
|
+
id: "plain",
|
|
9
|
+
description: "A plain agent with no memory and no object schema.",
|
|
10
|
+
instructions: "You are a plain agent.",
|
|
11
|
+
provider: {
|
|
12
|
+
model: "test-model",
|
|
13
|
+
compatible: "openai",
|
|
14
|
+
apiKey: "test-key",
|
|
15
|
+
baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
|
|
16
|
+
options: { retries: 0 },
|
|
17
|
+
},
|
|
18
|
+
api: {
|
|
19
|
+
// public on purpose: `object` also needs a rule so the *missing schema*
|
|
20
|
+
// error can be exercised (otherwise the action is denied first)
|
|
21
|
+
access: { generate: true, object: true },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/** Declares no `api` at all — every action must be denied (secure by default). */
|
|
4
|
+
export default define.Agent({
|
|
5
|
+
id: "private",
|
|
6
|
+
description: "Not exposed over HTTP.",
|
|
7
|
+
instructions: "You are a private agent.",
|
|
8
|
+
provider: {
|
|
9
|
+
model: "test-model",
|
|
10
|
+
compatible: "openai",
|
|
11
|
+
apiKey: "test-key",
|
|
12
|
+
baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
|
|
13
|
+
options: { retries: 0 },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A collection whose hook uses `agents` — proving the LLM registry is injected
|
|
5
|
+
* into the hook context exactly like `rest` and `io`.
|
|
6
|
+
*
|
|
7
|
+
* The observation is published on `globalThis` (the test reads it back): a hook
|
|
8
|
+
* must not stuff arbitrary keys into `meta.data`, the collection schema rejects
|
|
9
|
+
* them (`allowUnknown: false`).
|
|
10
|
+
*/
|
|
11
|
+
export default define.Collection({
|
|
12
|
+
slug: "notes",
|
|
13
|
+
fields: [{ name: "title", type: "string" }],
|
|
14
|
+
hooks: {
|
|
15
|
+
beforeOperation: define.Hook(async ({ agents, api, rest, meta }) => {
|
|
16
|
+
if (meta.action !== "insertOne") return;
|
|
17
|
+
(globalThis as any).__agentsHookProbe = {
|
|
18
|
+
ids: agents.ids().sort(),
|
|
19
|
+
bound: agents.get("assistant")?.getRest() === rest,
|
|
20
|
+
// The in-process facade, bound to the same `rest`
|
|
21
|
+
slug: api.collection("notes").getSlug(),
|
|
22
|
+
notes: await api.collection("notes").countDocuments(),
|
|
23
|
+
};
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/** A route reading the LLM registry **and the in-process facade** from its context. */
|
|
4
|
+
export default define.Route({
|
|
5
|
+
method: "GET",
|
|
6
|
+
path: "/probe",
|
|
7
|
+
handler: async ({ agents, api, rest }) => Response.json({
|
|
8
|
+
agents: agents.ids().sort(),
|
|
9
|
+
hasAssistant: !!agents.get("assistant"),
|
|
10
|
+
// `api` groups the same calls as `rest` — bound and namespaced
|
|
11
|
+
slug: api.collection("notes").getSlug(),
|
|
12
|
+
notes: await api.collection("notes").countDocuments(),
|
|
13
|
+
sameAsRest: (await api.collection("notes").find()) .length === (await rest.find("notes")).length,
|
|
14
|
+
agentBound: api.agent("assistant")?.getRest() === rest,
|
|
15
|
+
unknownAgent: api.agent("ghost"),
|
|
16
|
+
vars: typeof api.vars.get,
|
|
17
|
+
serviceRun: typeof api.service("probe").run,
|
|
18
|
+
serviceCall: await api.service("probe").run("echo", { from: "route" }),
|
|
19
|
+
fileUrl: api.files.url("invoices", "f1.pdf", { width: 800 }),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { define } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/** A service action reading the LLM registry and the facade from its context. */
|
|
4
|
+
export default define.Service({
|
|
5
|
+
name: "probe",
|
|
6
|
+
enabled: true,
|
|
7
|
+
actions: {
|
|
8
|
+
check: async ({ agents, api, rest }) => ({
|
|
9
|
+
agents: agents.ids().sort(),
|
|
10
|
+
hasAssistant: !!agents.get("assistant"),
|
|
11
|
+
bound: agents.get("assistant")?.getRest() === rest,
|
|
12
|
+
tenant: rest.tenant_id,
|
|
13
|
+
// `api.service(name).run` is the grouped `rest.runService`
|
|
14
|
+
nested: await api.service("probe").run("echo", { from: "api" }),
|
|
15
|
+
varType: typeof api.vars.get,
|
|
16
|
+
}),
|
|
17
|
+
echo: async ({ data }) => data,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fake model providers for the agent tests — one local HTTP endpoint per
|
|
3
|
+
* protocol, recording what the runtime sent.
|
|
4
|
+
*
|
|
5
|
+
* Shared by `agent.test.ts` (runtime) and `agents-api.test.ts` (HTTP surface),
|
|
6
|
+
* so a change in the wire format is fixed in one place.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type Recorded = { path: string; body: any; headers: Record<string, string> };
|
|
10
|
+
|
|
11
|
+
export type Fake = {
|
|
12
|
+
requests: Recorded[];
|
|
13
|
+
url: string;
|
|
14
|
+
hits: () => number;
|
|
15
|
+
stop: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A local endpoint that records the requests and answers from `handler`. */
|
|
19
|
+
export function fakeProvider(handler: (body: any, req: Request, hits: number) => Response | Promise<Response>): Fake {
|
|
20
|
+
const requests: Recorded[] = [];
|
|
21
|
+
let hits = 0;
|
|
22
|
+
const server = Bun.serve({
|
|
23
|
+
port: 0,
|
|
24
|
+
hostname: "127.0.0.1",
|
|
25
|
+
fetch: async (req) => {
|
|
26
|
+
const body = await req.json().catch(() => null);
|
|
27
|
+
requests.push({
|
|
28
|
+
path: new URL(req.url).pathname,
|
|
29
|
+
body,
|
|
30
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
31
|
+
});
|
|
32
|
+
hits += 1;
|
|
33
|
+
return handler(body, req, hits);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
requests,
|
|
38
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
39
|
+
hits: () => hits,
|
|
40
|
+
stop: () => server.stop(true),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Typed element access (`noUncheckedIndexedAccess` is on). */
|
|
45
|
+
export function at<T>(list: T[], index = 0): T {
|
|
46
|
+
const item = list[index];
|
|
47
|
+
if (item === undefined) throw new Error(`No element #${index} (length ${list.length})`);
|
|
48
|
+
return item;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function request(provider: Fake, index = 0): Recorded {
|
|
52
|
+
const recorded = provider.requests[index];
|
|
53
|
+
if (!recorded) throw new Error(`No request #${index} was recorded (got ${provider.requests.length})`);
|
|
54
|
+
return recorded;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const USAGE = { prompt_tokens: 11, completion_tokens: 4, total_tokens: 15 };
|
|
58
|
+
export const A_USAGE = { input_tokens: 13, output_tokens: 6 };
|
|
59
|
+
|
|
60
|
+
/** An SSE response from a list of already-serialized events. */
|
|
61
|
+
export function sse(events: string[]): Response {
|
|
62
|
+
return new Response(events.map((event) => `data: ${event}\n\n`).join(""), {
|
|
63
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── OpenAI-compatible ───────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export function openaiText(text: string, finishReason = "stop"): Response {
|
|
70
|
+
return Response.json({
|
|
71
|
+
choices: [{ message: { role: "assistant", content: text }, finish_reason: finishReason }],
|
|
72
|
+
usage: USAGE,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function openaiTool(name: string, args: any, id = "call_1"): Response {
|
|
77
|
+
return Response.json({
|
|
78
|
+
choices: [{
|
|
79
|
+
message: {
|
|
80
|
+
role: "assistant",
|
|
81
|
+
content: null,
|
|
82
|
+
tool_calls: [{ id, type: "function", function: { name, arguments: JSON.stringify(args) } }],
|
|
83
|
+
},
|
|
84
|
+
finish_reason: "tool_calls",
|
|
85
|
+
}],
|
|
86
|
+
usage: USAGE,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function openaiStream(pieces: string[]): Response {
|
|
91
|
+
const events = pieces.map((text) => JSON.stringify({ choices: [{ delta: { content: text } }] }));
|
|
92
|
+
events.push(JSON.stringify({ choices: [], usage: USAGE }));
|
|
93
|
+
events.push(JSON.stringify({ choices: [{ delta: {}, finish_reason: "stop" }] }));
|
|
94
|
+
events.push("[DONE]");
|
|
95
|
+
return sse(events);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Anthropic ───────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export function anthropicText(text: string, stopReason = "end_turn"): Response {
|
|
101
|
+
return Response.json({
|
|
102
|
+
content: [{ type: "text", text }],
|
|
103
|
+
stop_reason: stopReason,
|
|
104
|
+
usage: A_USAGE,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function anthropicTool(name: string, input: any, id = "toolu_1"): Response {
|
|
109
|
+
return Response.json({
|
|
110
|
+
content: [{ type: "tool_use", id, name, input }],
|
|
111
|
+
stop_reason: "tool_use",
|
|
112
|
+
usage: A_USAGE,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function anthropicStream(pieces: string[]): Response {
|
|
117
|
+
const events: string[] = [
|
|
118
|
+
JSON.stringify({ type: "message_start", message: { usage: { input_tokens: 9, output_tokens: 0 } } }),
|
|
119
|
+
JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }),
|
|
120
|
+
];
|
|
121
|
+
for (const text of pieces) {
|
|
122
|
+
events.push(JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text } }));
|
|
123
|
+
}
|
|
124
|
+
events.push(JSON.stringify({ type: "content_block_stop", index: 0 }));
|
|
125
|
+
events.push(JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 5 } }));
|
|
126
|
+
events.push(JSON.stringify({ type: "message_stop" }));
|
|
127
|
+
return sse(events);
|
|
128
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Test fixture — disabled lifecycle (must be skipped).
|
|
2
|
+
const calls: string[] = [];
|
|
3
|
+
(globalThis as any).__lcDisabled = calls;
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
_isLifecycle_: true,
|
|
7
|
+
enabled: false,
|
|
8
|
+
beforeBoot: async () => { calls.push("beforeBoot"); },
|
|
9
|
+
afterBoot: async () => { calls.push("afterBoot"); },
|
|
10
|
+
onDestroy: async () => { calls.push("onDestroy"); },
|
|
11
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Test fixture — `onDestroy` never resolves (used to test destroyTimeout).
|
|
2
|
+
const calls: string[] = [];
|
|
3
|
+
(globalThis as any).__lcHang = calls;
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
_isLifecycle_: true,
|
|
7
|
+
enabled: true,
|
|
8
|
+
destroyTimeout: 100,
|
|
9
|
+
onDestroy: async () => {
|
|
10
|
+
calls.push("onDestroy:start");
|
|
11
|
+
await new Promise(() => { /* never resolves */ });
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Test fixture — records lifecycle calls on globalThis.
|
|
2
|
+
const calls: string[] = [];
|
|
3
|
+
(globalThis as any).__lcNormal = calls;
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
_isLifecycle_: true,
|
|
7
|
+
enabled: true,
|
|
8
|
+
destroyTimeout: 200,
|
|
9
|
+
beforeBoot: async ({ replication }: any) => {
|
|
10
|
+
calls.push("beforeBoot");
|
|
11
|
+
// The API is tenant-bound (like `rest`) — no tenant id is passed, and the
|
|
12
|
+
// implicit tenant must match this lifecycle's tenant.
|
|
13
|
+
const seeded = await replication.seed("latest");
|
|
14
|
+
(globalThis as any).__lcReplicationApi =
|
|
15
|
+
typeof replication?.seed === "function" &&
|
|
16
|
+
typeof replication?.reset === "function" &&
|
|
17
|
+
typeof replication?.now === "function" &&
|
|
18
|
+
typeof replication?.state === "function" &&
|
|
19
|
+
seeded?.tenant === "normal";
|
|
20
|
+
},
|
|
21
|
+
afterBoot: async () => { calls.push("afterBoot"); },
|
|
22
|
+
onDestroy: async ({ reason }: any) => { calls.push(`onDestroy:${reason}`); },
|
|
23
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture — boots the server through `bootApp`, to be launched with a boot mode
|
|
3
|
+
* flag (see `packages/core/tests/replication-only.test.ts`).
|
|
4
|
+
* Port and database URIs are overridable through the environment so the same
|
|
5
|
+
* fixture serves every mode without clashes.
|
|
6
|
+
*
|
|
7
|
+
* Run manually:
|
|
8
|
+
* bun packages/core/tests/fixtures/repl-only/app.ts --replication-only
|
|
9
|
+
* bun packages/core/tests/fixtures/repl-only/app.ts --no-replication
|
|
10
|
+
*/
|
|
11
|
+
import { bootApp } from "../../../server/boot";
|
|
12
|
+
|
|
13
|
+
await bootApp({
|
|
14
|
+
server: { port: Number(Bun.env.RO_PORT ?? 5556) },
|
|
15
|
+
tenants: [{
|
|
16
|
+
id: "ro",
|
|
17
|
+
dir: "packages/core/tests/fixtures/repl-only/tenant",
|
|
18
|
+
database: { uri: Bun.env.RO_SRC ?? "mongodb://localhost:27017/_RO_SRC" },
|
|
19
|
+
replication: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
runOnBoot: true,
|
|
22
|
+
schedule: { interval: "1h" },
|
|
23
|
+
destinations: [{ id: "backup", uri: Bun.env.RO_DST ?? "mongodb://localhost:27017/_RO_DST" }],
|
|
24
|
+
},
|
|
25
|
+
}],
|
|
26
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Test fixture — a replicable collection (plain object + marker, no import needed).
|
|
2
|
+
export default {
|
|
3
|
+
_isCollection_: true,
|
|
4
|
+
_isTimeSerie_: false,
|
|
5
|
+
type: "document",
|
|
6
|
+
slug: "orders",
|
|
7
|
+
fields: [
|
|
8
|
+
{ name: "ref", type: "string" },
|
|
9
|
+
{ name: "total", type: "number" },
|
|
10
|
+
],
|
|
11
|
+
api: { access: { "*": true } },
|
|
12
|
+
// Opt this collection into the data replication engine
|
|
13
|
+
replication: { enabled: true },
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Test fixture — `beforeBoot` / `onDestroy` must still run in replication-only
|
|
2
|
+
// mode (the documented place to seed or reset a replication before it starts).
|
|
3
|
+
// Each hook drops a marker file the test can assert on.
|
|
4
|
+
// (`enabled` mirrors what `define.Lifecycle` sets by default.)
|
|
5
|
+
export default {
|
|
6
|
+
_isLifecycle_: true,
|
|
7
|
+
enabled: true,
|
|
8
|
+
beforeBoot: async () => {
|
|
9
|
+
await Bun.write(new URL("./.markers/lifecycle.txt", import.meta.url), "beforeBoot");
|
|
10
|
+
},
|
|
11
|
+
onDestroy: async () => {
|
|
12
|
+
await Bun.write(new URL("./.markers/onDestroy.txt", import.meta.url), "onDestroy");
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { define, agents } from "../../../../index";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The collection this tenant names for its agent memory. The loader publishes it
|
|
5
|
+
* (`cfg.agentMemories`), so the replication engine copies it like a declared
|
|
6
|
+
* collection — `replication.exclude: ['memory']` is the opt-out.
|
|
7
|
+
*/
|
|
8
|
+
export const AGENT_THREADS = "agent_threads";
|
|
9
|
+
|
|
10
|
+
export default define.Agent({
|
|
11
|
+
id: "chat",
|
|
12
|
+
description: "Fixture agent whose memory lives in a tenant-named collection.",
|
|
13
|
+
instructions: "Fixture agent — no model call is ever made in these tests.",
|
|
14
|
+
provider: { model: "test-model", compatible: "openai", apiKey: "fixture" },
|
|
15
|
+
memory: new agents.memory.Mongo({ collection: AGENT_THREADS }),
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Test fixture — a collection whose replication key is a **number** (not a date).
|
|
2
|
+
// The delete guard must still match on the destination.
|
|
3
|
+
export default {
|
|
4
|
+
_isCollection_: true,
|
|
5
|
+
_isTimeSerie_: false,
|
|
6
|
+
type: "document",
|
|
7
|
+
slug: "counters",
|
|
8
|
+
fields: [
|
|
9
|
+
{ name: "seq", type: "number" },
|
|
10
|
+
{ name: "title", type: "string" },
|
|
11
|
+
],
|
|
12
|
+
replication: { enabled: true, key: "seq" },
|
|
13
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Test fixture — a `define.Vars` definition (plain object + marker).
|
|
2
|
+
export default {
|
|
3
|
+
_isVars_: true,
|
|
4
|
+
namespace: "config",
|
|
5
|
+
scope: { name: "company", type: "relationship", relation: { to: "companies" } },
|
|
6
|
+
meta: [
|
|
7
|
+
{ name: "note", type: "string" },
|
|
8
|
+
{ name: "expiresAt", type: "datetime-local" },
|
|
9
|
+
],
|
|
10
|
+
vars: {
|
|
11
|
+
licence: "RDX00",
|
|
12
|
+
maxUsers: { type: "number", defaultValue: 100 },
|
|
13
|
+
session: { type: "string", ttl: "1h" },
|
|
14
|
+
},
|
|
15
|
+
api: {
|
|
16
|
+
access: {
|
|
17
|
+
"*": true,
|
|
18
|
+
set: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// Opt this namespace into replication (per-namespace filter on `ns`).
|
|
22
|
+
replication: { enabled: true },
|
|
23
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Test fixture — a namespace WITHOUT `api.access` (must be denied over HTTP),
|
|
2
|
+
// and which explicitly opts OUT of replication.
|
|
3
|
+
export default {
|
|
4
|
+
_isVars_: true,
|
|
5
|
+
namespace: "private",
|
|
6
|
+
vars: { secret: "shh" },
|
|
7
|
+
// Framework collections are replicated by default: opting out is explicit
|
|
8
|
+
replication: { enabled: false },
|
|
9
|
+
};
|