@anteros/core 0.0.41 → 0.0.44
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 +9 -59
- package/database/collection.ts +3 -1
- package/database/deleteLog.ts +67 -3
- package/database/file.ts +4 -3
- package/database/mongodbadapter.ts +103 -42
- package/database/replication.ts +724 -58
- package/database/ttl.ts +68 -0
- package/database/workflow.ts +466 -240
- package/index.ts +6 -0
- package/lib/agent.ts +1567 -0
- package/lib/agentMemory.ts +689 -0
- package/lib/agents.ts +180 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/auditResult.ts +13 -0
- package/lib/define.ts +43 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +2 -1
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +7 -1
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/mcpClient.ts +128 -0
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +970 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- package/lib/skills.ts +481 -0
- 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 +472 -0
- package/server/api.ts +80 -65
- package/server/boot.ts +11 -0
- package/server/hono.ts +8 -3
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +404 -0
- package/tests/agent.test.ts +1241 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-operations.test.ts +1 -1
- package/tests/audit-redact.test.ts +29 -3
- package/tests/audit-run-action.test.ts +1 -1
- package/tests/endpoints.test.ts +80 -0
- package/tests/file-collection-replication.test.ts +2 -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/repl-only/app.ts +3 -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/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
- 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/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +4 -2
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +246 -0
- package/tests/replication.test.ts +2 -0
- package/tests/upload.test.ts +44 -12
- package/tests/vars-api.test.ts +1 -1
- package/tests/vars-replication.test.ts +5 -2
- 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 +1067 -0
- package/types/config.d.ts +16 -0
- package/types/file.d.ts +7 -7
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +6 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +70 -4
- 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 +13 -0
- package/types/vars.d.ts +2 -2
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { formatConfig } from "../server/config";
|
|
4
|
+
import { syncTenants } from "../database/tenant";
|
|
5
|
+
import { syncWorkflows, ensureWorkflowIndexes, getWorkflow, workflowStats } from "../lib/workflow";
|
|
6
|
+
import { useRest } from "../database/rest";
|
|
7
|
+
|
|
8
|
+
const TENANT = "wf-test";
|
|
9
|
+
// Second tenant sharing the same code folder — same workflows, other database
|
|
10
|
+
const TENANT_2 = "wf-test-2";
|
|
11
|
+
const DIR = "packages/core/tests/fixtures/workflow-tenant";
|
|
12
|
+
|
|
13
|
+
let rest: InstanceType<typeof useRest>;
|
|
14
|
+
|
|
15
|
+
async function indexNames(tenantId: string): Promise<string[]> {
|
|
16
|
+
const tenantRest = new useRest({ tenant_id: tenantId, internal: true });
|
|
17
|
+
return (await tenantRest.db.collection("_workflows_").listIndexes().toArray()).map((i: any) => i.name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
formatConfig({
|
|
22
|
+
server: { port: 4000 },
|
|
23
|
+
tenants: [
|
|
24
|
+
{ id: TENANT, dir: DIR, database: { uri: "mongodb://localhost:27017/_WF_TEST" } },
|
|
25
|
+
{ id: TENANT_2, dir: DIR, database: { uri: "mongodb://localhost:27017/_WF_TEST_2" } },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
await syncTenants();
|
|
29
|
+
await syncWorkflows();
|
|
30
|
+
// The loader fires the index pass in the background — await it here so the
|
|
31
|
+
// assertions below are deterministic (the non-blocking behaviour is tested
|
|
32
|
+
// separately, via `syncWorkflows()` alone).
|
|
33
|
+
await ensureWorkflowIndexes();
|
|
34
|
+
|
|
35
|
+
rest = new useRest({ tenant_id: TENANT, internal: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
for (const id of [TENANT, TENANT_2]) {
|
|
40
|
+
try { await new useRest({ tenant_id: id, internal: true }).db.collection("_workflows_").drop(); } catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("workflow loader", () => {
|
|
45
|
+
it("registers the enabled workflows of the tenant", () => {
|
|
46
|
+
const invoice = getWorkflow("invoice", TENANT);
|
|
47
|
+
expect(invoice).toBeDefined();
|
|
48
|
+
expect(invoice!._tenant_).toBe(TENANT);
|
|
49
|
+
expect(invoice!.version).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps exactly one definition for a duplicated id", () => {
|
|
53
|
+
const order = getWorkflow("order", TENANT);
|
|
54
|
+
// Which file wins depends on the filesystem order — what matters is that
|
|
55
|
+
// there is a single, valid definition (never two, never a crash)
|
|
56
|
+
expect(["Orders", "Orders (duplicate definition)"]).toContain(order!.name);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("skips disabled workflows (`enabled: false`)", () => {
|
|
60
|
+
expect(getWorkflow("nightly", TENANT)).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("ignores a workflow without an `id`", () => {
|
|
64
|
+
// A missing id used to be registered under the literal `undefined` key
|
|
65
|
+
expect(getWorkflow("undefined", TENANT)).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("isolates a file that throws on import", () => {
|
|
69
|
+
expect(getWorkflow("broken", TENANT)).toBeUndefined();
|
|
70
|
+
// …and the other workflows are still there
|
|
71
|
+
expect(getWorkflow("order", TENANT)).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("loads the same folder independently for each tenant", () => {
|
|
75
|
+
expect(getWorkflow("order", TENANT)).toBeDefined();
|
|
76
|
+
expect(getWorkflow("order", TENANT_2)).toBeDefined();
|
|
77
|
+
expect(getWorkflow("order", TENANT_2)!._tenant_).toBe(TENANT_2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("reports what was loaded", () => {
|
|
81
|
+
const stats = workflowStats();
|
|
82
|
+
expect(stats.total).toBeGreaterThanOrEqual(2); // one per tenant
|
|
83
|
+
expect(stats.tenants).toContain(TENANT);
|
|
84
|
+
expect(stats.tenants).toContain(TENANT_2);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("workflow indexes", () => {
|
|
89
|
+
it("creates the indexes the engine queries on", async () => {
|
|
90
|
+
const names = await indexNames(TENANT);
|
|
91
|
+
expect(names).toContain("workflowId_1_createdAt_-1"); // listRuns()
|
|
92
|
+
expect(names).toContain("status_1"); // resumeAll()
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("creates the indexes declared by the workflow context", async () => {
|
|
96
|
+
const names = await indexNames(TENANT);
|
|
97
|
+
expect(names).toContain("context.customerId_1");
|
|
98
|
+
expect(names).toContain("context.total_-1");
|
|
99
|
+
expect(names).toContain("context.companyId_1");
|
|
100
|
+
// a field without `index` is not indexed
|
|
101
|
+
expect(names).not.toContain("context.note_1");
|
|
102
|
+
expect(names).not.toContain("context.processedAt_1");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("creates the explicit `indexes` — compound, unique, sparse, named", async () => {
|
|
106
|
+
const names = await indexNames(TENANT);
|
|
107
|
+
expect(names).toContain("context.companyId_1_status_1"); // compound
|
|
108
|
+
expect(names).toContain("uniq_batch"); // explicit name
|
|
109
|
+
|
|
110
|
+
const uniq: any = (await rest.db.collection("_workflows_").listIndexes().toArray())
|
|
111
|
+
.find((i: any) => i.name === "uniq_batch");
|
|
112
|
+
expect(uniq.unique).toBe(true);
|
|
113
|
+
expect(uniq.sparse).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("is idempotent — running it again creates nothing", async () => {
|
|
117
|
+
const before = (await indexNames(TENANT)).length;
|
|
118
|
+
await ensureWorkflowIndexes();
|
|
119
|
+
expect((await indexNames(TENANT)).length).toBe(before);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("serves the engine's own query with an index scan", async () => {
|
|
123
|
+
const col: any = rest.db.collection("_workflows_");
|
|
124
|
+
// A run must exist for the collection to be queryable/explainable
|
|
125
|
+
await rest.workflow.run("invoice", { ref: "A-1" });
|
|
126
|
+
const plan = JSON.stringify(await col.find({ workflowId: "invoice" }).explain("queryPlanner"));
|
|
127
|
+
expect(plan).toContain("IXSCAN"); // COLLSCAN before the loader indexed `workflowId`
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("workflow engine wiring", () => {
|
|
132
|
+
it("runs a workflow loaded from the filesystem", async () => {
|
|
133
|
+
const run = await rest.workflow.run("invoice", { ref: "A-1" }, { companyId: "c-1" });
|
|
134
|
+
expect(run.status).toBe("completed");
|
|
135
|
+
expect(run.workflowVersion).toBe(3);
|
|
136
|
+
expect(run.context).toEqual({ companyId: "c-1" });
|
|
137
|
+
expect(run.steps.length).toBe(2);
|
|
138
|
+
// The run says which process executed it (`reusePort` / forked workers)
|
|
139
|
+
expect(run.pid).toBe(process.pid);
|
|
140
|
+
expect(run.hostname).toBe(os.hostname());
|
|
141
|
+
|
|
142
|
+
const runs = await rest.workflow.listRuns("invoice");
|
|
143
|
+
expect(runs.length).toBeGreaterThanOrEqual(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("refuses to run a disabled workflow", async () => {
|
|
147
|
+
await expect(rest.workflow.run("nightly", {})).rejects.toThrow();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("workflow context validation", () => {
|
|
152
|
+
it("accepts a context that matches the declaration", async () => {
|
|
153
|
+
const run = await rest.workflow.run("invoice", { ref: "B-1" }, { companyId: "c-2" });
|
|
154
|
+
expect(run.context).toEqual({ companyId: "c-2" });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("rejects a wrong type with a clear code", async () => {
|
|
158
|
+
await expect(
|
|
159
|
+
rest.workflow.run("invoice", {}, { companyId: 42 as any }),
|
|
160
|
+
).rejects.toThrow(/must be a string/);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("coerces an ISO string to a real Date (so `context.*` indexes stay typed)", async () => {
|
|
164
|
+
const run = await rest.workflow.run("invoice", {}, {
|
|
165
|
+
companyId: "c-3",
|
|
166
|
+
processedAt: "2026-03-04T05:06:07.000Z" as any,
|
|
167
|
+
});
|
|
168
|
+
expect(run.context.processedAt).toBeInstanceOf(Date);
|
|
169
|
+
expect((run.context.processedAt as Date).toISOString()).toBe("2026-03-04T05:06:07.000Z");
|
|
170
|
+
|
|
171
|
+
// …and it is stored as a Date in MongoDB too
|
|
172
|
+
const stored: any = await rest.workflow.getRun(run._id);
|
|
173
|
+
expect(stored.context.processedAt).toBeInstanceOf(Date);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects an invalid date", async () => {
|
|
177
|
+
await expect(
|
|
178
|
+
rest.workflow.run("invoice", {}, { processedAt: "not-a-date" as any }),
|
|
179
|
+
).rejects.toThrow(/must be a date/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("keeps undeclared keys as-is", async () => {
|
|
183
|
+
const run = await rest.workflow.run("invoice", {}, { companyId: "c-4", extra: { any: "thing" } });
|
|
184
|
+
expect(run.context.extra).toEqual({ any: "thing" });
|
|
185
|
+
});
|
|
186
|
+
});
|