@@ -0,0 +1,201 @@
|
||||
// The agent module's use cases on the in-memory adapter: starting lists and statuses, cancel's
|
||||
// resume event, and the approval queue — which must surface the EXACT stored proposed arguments.
|
||||
|
||||
import { AGENT_ENTITIES, dataAgentPersistence, dataRunStore, threadIdOf } from "@conusai-kit/agent"
|
||||
import type { TenantScope } from "@conusai-kit/core"
|
||||
import { createMemoryDataSource } from "@conusai-kit/data"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { cancelRun, getManyRuns, getRun, listApprovals, listRuns, reviewApproval, startRun } from "../core/runs.js"
|
||||
|
||||
const USER = "01900000-0000-7000-0000-000000000009"
|
||||
const scope: TenantScope = {
|
||||
level: "project",
|
||||
actor: { userId: USER },
|
||||
orgId: "01900000-0000-7000-0000-000000000001",
|
||||
workspaceId: "01900000-0000-7000-0000-000000000002",
|
||||
projectId: "01900000-0000-7000-0000-000000000003",
|
||||
}
|
||||
const makeDeps = () => ({ data: createMemoryDataSource({ entities: [...AGENT_ENTITIES] }) })
|
||||
|
||||
const PLAN = [
|
||||
{ key: "read", label: "Read the document", tool: "documents.read", args: { fileId: "f-1" } },
|
||||
{ key: "propose", label: "Propose lines", tool: "documents.extract", args: { fileId: "f-1", schema: "line" } },
|
||||
]
|
||||
|
||||
describe("the agent module's run use cases", () => {
|
||||
const deps = makeDeps()
|
||||
it("starts a run queued, with the plan visible, and lists it", async () => {
|
||||
const run = await startRun(deps, scope, { goal: "Review the contract", plan: PLAN })
|
||||
expect(run.status).toBe("queued")
|
||||
expect(run.goal).toBe("Review the contract")
|
||||
expect(run.plan).toEqual([
|
||||
{
|
||||
key: "read",
|
||||
label: "Read the document",
|
||||
tool: "documents.read",
|
||||
args: { fileId: "f-1" },
|
||||
needsApproval: false,
|
||||
status: "pending",
|
||||
attempts: 0,
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
key: "propose",
|
||||
label: "Propose lines",
|
||||
tool: "documents.extract",
|
||||
args: { fileId: "f-1", schema: "line" },
|
||||
needsApproval: false,
|
||||
status: "pending",
|
||||
attempts: 0,
|
||||
error: null,
|
||||
},
|
||||
])
|
||||
expect(run.percent).toBe(0)
|
||||
expect(run.answer).toBeNull()
|
||||
const listed = await listRuns(deps, scope, {})
|
||||
expect(listed.runs).toHaveLength(1)
|
||||
expect((await getRun(deps, scope, run.runId)).runId).toBe(run.runId)
|
||||
// The outbox event that enqueues the run job in the worker.
|
||||
const pending = await deps.data.outbox.pending(10)
|
||||
expect(pending.some((record) => record.name === "agent.runResumeRequested")).toBe(true)
|
||||
})
|
||||
|
||||
it("reads runs back by id for the live list, and answers nothing for another project's or no run", async () => {
|
||||
const own = makeDeps()
|
||||
const run = await startRun(own, scope, { goal: "Read back", plan: PLAN })
|
||||
const listed = (await listRuns(own, scope, {})).runs
|
||||
expect(await getManyRuns(own, scope, [run.runId, run.runId])).toEqual(listed)
|
||||
const elsewhere: TenantScope = { ...scope, projectId: "01900000-0000-7000-0000-000000000004" }
|
||||
expect(await getManyRuns(own, elsewhere, [run.runId])).toEqual([])
|
||||
expect(await getManyRuns(own, scope, ["01900000-0000-7000-0000-0000000000ff"])).toEqual([])
|
||||
})
|
||||
|
||||
it("answers a completed run's answer with the provenance it is stored with (AI Act Art. 50(2))", async () => {
|
||||
const fresh = makeDeps()
|
||||
const run = await startRun(fresh, scope, { goal: "Review the contract", plan: PLAN })
|
||||
expect(run.answerProvenance).toBeNull()
|
||||
const provenance = {
|
||||
ai_generated: true as const,
|
||||
mode: "machine-generated" as const,
|
||||
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
|
||||
generator: "conusai.ai-gateway",
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini-2024-07-18",
|
||||
generated_at: "2026-11-02T10:30:00Z",
|
||||
}
|
||||
const store = dataRunStore(fresh.data, scope)
|
||||
const record = await store.get(run.runId)
|
||||
await store.transition(run.runId, record?.version ?? 0, {
|
||||
status: "completed",
|
||||
finishedAt: "2026-11-02T10:30:01.000Z",
|
||||
answerProvenance: provenance,
|
||||
})
|
||||
await dataAgentPersistence(fresh.data, scope).stores.messages.saveThread(threadIdOf(run.runId), [
|
||||
{ role: "user", content: "Review the contract" },
|
||||
{ role: "assistant", content: "The contract was reviewed." },
|
||||
])
|
||||
const completed = await getRun(fresh, scope, run.runId)
|
||||
expect(completed.answer).toBe("The contract was reviewed.")
|
||||
expect(completed.answerProvenance).toEqual(provenance)
|
||||
})
|
||||
|
||||
it("refuses an over-long or repeated plan before anything is written", async () => {
|
||||
const fresh = makeDeps()
|
||||
const tooLong = await startRun(fresh, scope, {
|
||||
goal: "x",
|
||||
plan: Array.from({ length: 26 }, (_, at) => ({ key: `s${at}`, label: `Step ${at}`, tool: "t", args: {} })),
|
||||
}).catch((error: unknown) => error)
|
||||
expect((tooLong as { status?: number }).status).toBe(422)
|
||||
const repeated = await startRun(fresh, scope, {
|
||||
goal: "x",
|
||||
plan: [
|
||||
{ key: "same", label: "One", tool: "t", args: {} },
|
||||
{ key: "same", label: "Two", tool: "t", args: {} },
|
||||
],
|
||||
}).catch((error: unknown) => error)
|
||||
expect((repeated as { status?: number }).status).toBe(422)
|
||||
expect((await listRuns(fresh, scope, {})).runs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("cancel persists the request and re-enqueues the resume; a parked approval surfaces its exact arguments", async () => {
|
||||
const fresh = makeDeps()
|
||||
const run = await startRun(fresh, scope, { goal: "Approve-aware run", plan: PLAN })
|
||||
// Simulate the engine parking an approval by writing the step row as the engine would.
|
||||
const { dataRunStore } = await import("@conusai-kit/agent")
|
||||
const store = dataRunStore(fresh.data, scope)
|
||||
|
||||
await store.parkStepApproval(
|
||||
run.runId,
|
||||
{ key: "read", index: 0, label: "Read the document" },
|
||||
{
|
||||
actionId: "act-7",
|
||||
tool: "documents.read",
|
||||
arguments: { fileId: "f-1", dryRun: false },
|
||||
reviewUrl: "https://console/review?action=act-7",
|
||||
interrupt: null,
|
||||
},
|
||||
new Date().toISOString(),
|
||||
)
|
||||
await fresh.data
|
||||
.forScope(scope)
|
||||
// biome-ignore lint/suspicious/noExplicitAny: test adjusts the run's status as the engine would.
|
||||
.of(AGENT_ENTITIES[0] as any)
|
||||
.update(run.runId, { status: "awaiting_approval" }, 1)
|
||||
|
||||
const queue = await listApprovals(fresh, scope)
|
||||
expect(queue.approvals).toHaveLength(1)
|
||||
expect(queue.approvals[0]).toMatchObject({
|
||||
runId: run.runId,
|
||||
stepKey: "read",
|
||||
tool: "documents.read",
|
||||
arguments: { fileId: "f-1", dryRun: false },
|
||||
reviewUrl: "https://console/review?action=act-7",
|
||||
})
|
||||
|
||||
const cancelled = await cancelRun(fresh, scope, run.runId)
|
||||
expect(cancelled.cancelled).toBe(true)
|
||||
expect((await getRun(fresh, scope, run.runId)).cancelRequested).toBe(true)
|
||||
// The cancel re-published the resume event: a parked run notices the request only when resumed.
|
||||
const pending = await fresh.data.outbox.pending(10)
|
||||
expect(pending.filter((record) => record.name === "agent.runResumeRequested")).toHaveLength(2)
|
||||
|
||||
const review = await reviewApproval(fresh, scope, { runId: run.runId, stepKey: "read", decision: "approve" })
|
||||
expect(["queued", "running", "awaiting_approval", "cancelled"]).toContain(review.status)
|
||||
const bad = await reviewApproval(fresh, scope, { runId: run.runId, stepKey: "propose", decision: "approve" }).catch(
|
||||
(error: unknown) => error,
|
||||
)
|
||||
expect((bad as { status?: number }).status).toBe(422)
|
||||
// The decision is recorded on the step: the queue no longer offers it, and it is not decided twice.
|
||||
expect((await listApprovals(fresh, scope)).approvals).toHaveLength(0)
|
||||
const again = await reviewApproval(fresh, scope, { runId: run.runId, stepKey: "read", decision: "reject" }).catch(
|
||||
(error: unknown) => error,
|
||||
)
|
||||
expect((again as { status?: number }).status).toBe(409)
|
||||
expect((await store.listSteps(run.runId))[0]?.approval?.decision).toBe("approved")
|
||||
})
|
||||
})
|
||||
|
||||
it("does not offer or accept a pending approval after its run failed", async () => {
|
||||
const fresh = makeDeps()
|
||||
const run = await startRun(fresh, scope, { goal: "Synthetic approval", plan: PLAN })
|
||||
const store = dataRunStore(fresh.data, scope)
|
||||
await store.parkStepApproval(
|
||||
run.runId,
|
||||
{ key: "read", index: 0, label: "Read" },
|
||||
{
|
||||
actionId: null,
|
||||
tool: "records.write",
|
||||
arguments: {},
|
||||
reviewUrl: null,
|
||||
interrupt: null,
|
||||
},
|
||||
new Date().toISOString(),
|
||||
)
|
||||
const record = await store.get(run.runId)
|
||||
if (!record) throw new Error("expected a run")
|
||||
await store.transition(run.runId, record.version, { status: "failed", failureCode: "budget_exhausted" })
|
||||
expect((await listApprovals(fresh, scope)).approvals).toEqual([])
|
||||
await expect(
|
||||
reviewApproval(fresh, scope, { runId: run.runId, stepKey: "read", decision: "approve" }),
|
||||
).rejects.toMatchObject({ status: 409 })
|
||||
})
|
||||
Reference in New Issue
Block a user