Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { listPending, setStatus } from "./dialog-review"
import { listPending, setStatus, listEnvFacts, decideEnvFact, modifyEnvFact } from "./dialog-review.api"

// P1-C route contract: the V3.1 self-learning Review dialog talks to the raw-request escape-hatch
// routes (NOT the generated SDK). These assertions lock the exact method/url/body so a backend
Expand Down Expand Up @@ -70,4 +70,53 @@ describe("DeepAgent review dialog route contract", () => {
},
])
})

// V3.8.1 §G use-gate route contract.
test("listEnvFacts GETs /deepagent/env-facts and unwraps adopted/pending", async () => {
const calls: Recorded[] = []
const data = {
adopted: [],
pending: [{ fact_id: "f1", version: 1, description: "milvus", body: null, degraded: false }],
}
const result = await listEnvFacts(client(calls, data))
expect(calls).toEqual([{ method: "GET", url: "/deepagent/env-facts" }])
expect(result).toEqual(data)
})

test("listEnvFacts tolerates missing fields", async () => {
const calls: Recorded[] = []
expect(await listEnvFacts(client(calls, {}))).toEqual({ adopted: [], pending: [] })
})

test("decideEnvFact POSTs /deepagent/env-facts/decide with { factId, decision }", async () => {
const calls: Recorded[] = []
await decideEnvFact(client(calls, { ok: true }), "f1", "adopt")
expect(calls).toEqual([
{
method: "POST",
url: "/deepagent/env-facts/decide",
body: { factId: "f1", decision: "adopt" },
headers: { "Content-Type": "application/json" },
},
])
})

test("modifyEnvFact POSTs /deepagent/env-facts/modify with the full edit payload", async () => {
const calls: Recorded[] = []
const input = {
factId: "f1",
description: "milvus test",
body: { host: "10.0.0.5", port: 19530, last_confirmed_at: "2026-07-09T00:00:00Z" },
mode: "global" as const,
}
await modifyEnvFact(client(calls, { ok: true, factId: "f1" }), input)
expect(calls).toEqual([
{
method: "POST",
url: "/deepagent/env-facts/modify",
body: input,
headers: { "Content-Type": "application/json" },
},
])
})
})
104 changes: 104 additions & 0 deletions packages/app/src/components/review/dialog-review.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Pure HTTP client functions + types for the DeepAgent Review dialog. Split out from the .tsx so it
// carries NO UI imports (Kobalte/solid-web run client-only code at module eval, which crashes a
// server-side unit test). The route contract test imports THIS module; the component re-exports these
// for back-compat. Keep this file free of any solid-js/UI imports.

export type KnowledgeItem = {
id: string
type: "knowledge" | "strategy" | "methodology" | "memory" | "skill" | "failure_dossier"
summary: string
evidence_strength: "strong" | "medium" | "weak" | "none"
evidence_refs: string[]
approval_status: "pending" | "approved" | "rejected"
}

type RawSdkClient = {
client: {
request<TData>(options: {
method: string
url: string
body?: unknown
headers?: Record<string, string>
}): Promise<{ data?: TData }>
}
}

export type ReviewClient = RawSdkClient

export const listPending = async (client: ReviewClient): Promise<KnowledgeItem[]> => {
const response = await client.client.request<{ items: KnowledgeItem[] }>({
method: "GET",
url: "/deepagent/knowledge/pending",
})
return response.data?.items ?? []
}

export const setStatus = async (
client: ReviewClient,
action: "approve" | "reject-ids",
ids: string[],
): Promise<void> => {
await client.client.request<{ updated: string[] }>({
method: "POST",
url: `/deepagent/knowledge/${action}`,
body: { ids },
headers: { "Content-Type": "application/json" },
})
}

// V3.8.1 §G environment-fact use-gate. Provisional user-global environment facts surface here so the
// user decides, per project, whether to adopt them (§G.5). Credentials never appear — only secret_ref
// pointers. `degraded` marks a fact whose last connection attempt failed (§G.6).
export type EnvFactBody = {
host?: string
port?: number
container?: string
purpose?: string
secret_refs?: string[]
last_confirmed_at: string
notes?: string
}
export type EnvFactItem = {
fact_id: string
version: number
description: string
body: EnvFactBody | null
degraded: boolean
}
export type EnvFactList = { adopted: EnvFactItem[]; pending: EnvFactItem[] }

export const listEnvFacts = async (client: ReviewClient): Promise<EnvFactList> => {
const response = await client.client.request<EnvFactList>({ method: "GET", url: "/deepagent/env-facts" })
return { adopted: response.data?.adopted ?? [], pending: response.data?.pending ?? [] }
}

export const decideEnvFact = async (
client: ReviewClient,
factId: string,
decision: "adopt" | "reject",
): Promise<void> => {
await client.client.request<{ ok: boolean }>({
method: "POST",
url: "/deepagent/env-facts/decide",
body: { factId, decision },
headers: { "Content-Type": "application/json" },
})
}

// §G.5 modify: edit a fact then adopt it. mode=global corrects the shared fact for every project;
// mode=project writes a project-local override, leaving the global fact untouched for others.
export type EnvFactModifyInput = {
factId: string
description: string
body: EnvFactBody
domain?: string | null
mode: "global" | "project"
}
export const modifyEnvFact = async (client: ReviewClient, input: EnvFactModifyInput): Promise<void> => {
await client.client.request<{ ok: boolean; factId: string }>({
method: "POST",
url: "/deepagent/env-facts/modify",
body: input,
headers: { "Content-Type": "application/json" },
})
}
Loading
Loading