feat(server): W2-D agent assembly, session API, runAgent/resolveDecision and backend e2e
- Capabilities: all 14 registered (AO: inventory.query, download.plan/submit, organize.plan/execute,
policy.preview/apply; agent: task.inspect/retry/cancel); startup asserts count == 14.
Registry injects server-side authorize (automation_grant by action/scope/policy version/budget,
one-shot approval bound to plan id + payload hash + revision + expiry; model claims ignored;
automation switch), audit_event, server idempotency keys and a capability_invocation op ledger
(migration 0021) used for crash reconciliation. download.plan creates draft plans only;
download.submit promotes draft -> reserved after authorization.
- Agent session API: POST/GET /agent/runs, GET /agent/runs/{id} (events by seq, messages, report),
POST .../messages (wake), POST .../abort; automation-grants, approvals, capabilities invoke, tasks.
- runAgent task: Pi or fake provider from settings (apiKey via secrets), events persisted to
agent_run_event then published, resume + ledger reconciliation when pendingOperations > 0,
waits for goal-linked downloads/imports and reports from business tables (submitted != imported).
- Background triggers: decision.created -> bounded plan_only investigation (dedupe per decision);
decision.updated(resolved) -> resolveDecision executes chosen option.
- W2 closure: all OpenAPI routes registered in contracts api.ts (+ subset test); TMDB key from
settings secret_ref with env fallback; PUT /sites/{id}/credentials documented as alias of
/site-accounts; completion events documented as existing status events.
- e2e (real PG + OpenWorkflow + testkit mocks): RSS->library, agent delegation with grant/deny,
background continuation, worker kill -9 recovery (single qB add), auto investigation dedupe.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0dec0854df
commit
6280d52ec3
46 files changed
+5133
-111
No files matched your search
@@ -0,0 +1,76 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { delegateScript } from "./fixtures/scripts.ts";
|
||||
import {
|
||||
completeDownload,
|
||||
createE2E,
|
||||
type E2E,
|
||||
pgAvailable,
|
||||
runRow,
|
||||
seedBase,
|
||||
seedMovieTarget,
|
||||
waitFor,
|
||||
} from "./harness.ts";
|
||||
|
||||
// E3 关闭聊天不影响后台:只发一次 POST /agent/runs(HTTP 请求里不跑模型,立即返回 queued),之后不再有任何 API 请求
|
||||
// 也不订阅 SSE;持久任务独立推进:模型会话 → 提交 → (外部下载器完成)→ 核验入库 → run succeeded。
|
||||
|
||||
const hasPg = await pgAvailable();
|
||||
|
||||
describe.skipIf(!hasPg)("E3 关闭聊天不影响后台", () => {
|
||||
let e: E2E;
|
||||
let target: Awaited<ReturnType<typeof seedMovieTarget>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
e = await createE2E("e3", { fakeScripts: [delegateScript] });
|
||||
const base = await seedBase(e);
|
||||
target = await seedMovieTarget(e, base.siteId);
|
||||
await e.db
|
||||
.insertInto("automation_grant")
|
||||
.values({ action: "download.submit", scope: JSON.stringify({}), granted_by: "user:e2e" })
|
||||
.execute();
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await e?.close();
|
||||
}, 60_000);
|
||||
|
||||
it("创建后不再请求,任务系统独立推进到 completed", async () => {
|
||||
const t0 = Date.now();
|
||||
const r = await e.api("POST", "/agent/runs", { goal: "把《沙丘2》1080p 收进来", mode: "execute" });
|
||||
// HTTP 不驱动模型:立即返回 queued(模型会话在 worker 的持久任务里跑)
|
||||
expect(r.status).toBe(201);
|
||||
expect(r.body.data.status).toBe("queued");
|
||||
expect(Date.now() - t0).toBeLessThan(5_000);
|
||||
const runId = r.body.data.id as string;
|
||||
const task = await e.db
|
||||
.selectFrom("task")
|
||||
.selectAll()
|
||||
.where("id", "=", r.body.data.taskId)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(task).toMatchObject({ kind: "agentRun", subject_type: "agent_run", subject_id: runId });
|
||||
expect(task.engine_run_id).toBeTruthy();
|
||||
|
||||
// "聊天已关闭":从这里开始只有外部世界(下载器)在变化
|
||||
await completeDownload(e, "Dune.Part.Two.2024.1080p.BluRay.x264-GRP");
|
||||
const run = await waitFor(
|
||||
async () => {
|
||||
const x = await runRow(e, runId);
|
||||
return x.status === "succeeded" ? x : null;
|
||||
},
|
||||
120_000,
|
||||
"run succeeded",
|
||||
);
|
||||
expect(run.finished_at).not.toBeNull();
|
||||
expect(run.summary).toContain("已入库 1 个");
|
||||
const engineRun = await e.booted.ctx.engine.get(task.engine_run_id!);
|
||||
expect(engineRun?.status).toBe("completed");
|
||||
const t = await e.db.selectFrom("task").select("status").where("id", "=", task.id).executeTakeFirstOrThrow();
|
||||
expect(t.status).toBe("succeeded");
|
||||
const slot = await e.db
|
||||
.selectFrom("acquisition_slot")
|
||||
.select("status")
|
||||
.where("id", "=", target.slotId)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(slot.status).toBe("satisfied");
|
||||
}, 180_000);
|
||||
});
|
||||
Reference in new issue
Block a user