mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 14:17:08 +00:00
feat: small qol in fe
This commit is contained in:
1 parent
6dd5cf472a
commit
223a7588aa
12 files changed
+214
-34
No files matched your search
@@ -168,8 +168,9 @@ class NodeExecutionLog(BaseModel):
|
||||
duration_ms: Optional[int] = None
|
||||
error: Optional[str] = None
|
||||
# The node's state DELTA (keys it added or changed), not the full state:
|
||||
# point-in-time state is the merge of deltas up to this step.
|
||||
state_snapshot: Dict[str, Any] = Field(default_factory=dict)
|
||||
# point-in-time state is the merge of deltas up to this step. Runs
|
||||
# persisted before the rename carry this as ``state_snapshot``.
|
||||
state_delta: Dict[str, Any] = Field(default_factory=dict)
|
||||
# Compact per-node tool-call summary: [{tool_name, action_name, status}].
|
||||
tool_calls: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class WorkflowEngine:
|
||||
"node_type": node.type.value,
|
||||
"node_title": node.title,
|
||||
"status": "completed",
|
||||
"state_snapshot": log_entry["state_snapshot"],
|
||||
"state_delta": log_entry["state_delta"],
|
||||
"output": node_output,
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -172,7 +172,7 @@ class WorkflowEngine:
|
||||
"node_type": node.type.value,
|
||||
"node_title": node.title,
|
||||
"status": "failed",
|
||||
"state_snapshot": log_entry["state_snapshot"],
|
||||
"state_delta": log_entry["state_delta"],
|
||||
"error": user_friendly_error,
|
||||
}
|
||||
yield {"type": "error", "error": user_friendly_error}
|
||||
@@ -206,7 +206,7 @@ class WorkflowEngine:
|
||||
"completed_at": None,
|
||||
"status": ExecutionStatus.RUNNING.value,
|
||||
"error": None,
|
||||
"state_snapshot": {},
|
||||
"state_delta": {},
|
||||
}
|
||||
|
||||
def _finalize_log_entry(self, log_entry: Dict[str, Any], pre_state: Dict[str, Any]) -> None:
|
||||
@@ -214,7 +214,7 @@ class WorkflowEngine:
|
||||
completed_at = datetime.now(timezone.utc)
|
||||
log_entry["completed_at"] = completed_at
|
||||
log_entry["duration_ms"] = int((completed_at - log_entry["started_at"]).total_seconds() * 1000)
|
||||
log_entry["state_snapshot"] = self._state_delta(pre_state)
|
||||
log_entry["state_delta"] = self._state_delta(pre_state)
|
||||
if self._last_node_tool_calls:
|
||||
log_entry["tool_calls"] = list(self._last_node_tool_calls)
|
||||
|
||||
@@ -334,6 +334,11 @@ class WorkflowEngine:
|
||||
f'Model "{node_model_id}" does not support structured output for node "{node.title}"'
|
||||
)
|
||||
|
||||
node_prompt = node_config.system_prompt
|
||||
doc_manifest = self._node_document_manifest(node_config)
|
||||
if doc_manifest:
|
||||
node_prompt = f"{node_prompt}\n\n{doc_manifest}" if node_prompt else doc_manifest
|
||||
|
||||
factory_kwargs = {
|
||||
"agent_type": node_config.agent_type,
|
||||
"endpoint": self.agent.endpoint,
|
||||
@@ -342,7 +347,7 @@ class WorkflowEngine:
|
||||
"model_user_id": getattr(self.agent, "model_user_id", None),
|
||||
"api_key": node_api_key,
|
||||
"tool_ids": node_config.tools,
|
||||
"prompt": node_config.system_prompt,
|
||||
"prompt": node_prompt,
|
||||
"chat_history": self.agent.chat_history,
|
||||
"decoded_token": self.agent.decoded_token,
|
||||
"json_schema": node_json_schema,
|
||||
@@ -610,6 +615,54 @@ class WorkflowEngine:
|
||||
loaded.append(rel_path)
|
||||
return loaded
|
||||
|
||||
def _node_document_manifest(self, node_config: AgentNodeConfig) -> str:
|
||||
"""One-line-per-document manifest of the node's selected input documents.
|
||||
|
||||
Appended to the node's system prompt so the model knows the concrete
|
||||
refs (``A1``) and filenames it can pass to document/code tools —
|
||||
without it, models guess placeholder names ("attached_file") or paste
|
||||
file contents inline. Scoped to THIS node's ``input_documents``
|
||||
selection, so it never widens per-node document access. Best-effort:
|
||||
a resolution failure drops the manifest, never the node.
|
||||
"""
|
||||
from application.agents.tools.artifact_ref import make_ref, resolve_artifact_id
|
||||
from application.storage.db.repositories.artifacts import ArtifactsRepository
|
||||
from application.storage.db.session import db_readonly
|
||||
|
||||
try:
|
||||
raw_ids = self._resolve_input_artifact_ids(node_config.input_documents)
|
||||
if not raw_ids:
|
||||
return ""
|
||||
lines: List[str] = []
|
||||
with db_readonly() as conn:
|
||||
repo = ArtifactsRepository(conn)
|
||||
for raw in raw_ids:
|
||||
artifact_id = resolve_artifact_id(repo, raw, workflow_run_id=self.workflow_run_id)
|
||||
artifact = (
|
||||
repo.get_artifact_in_parent(artifact_id, workflow_run_id=self.workflow_run_id)
|
||||
if artifact_id is not None
|
||||
else None
|
||||
)
|
||||
if artifact is None:
|
||||
continue
|
||||
version = repo.get_version(artifact_id, artifact["current_version"]) or {}
|
||||
ref_seq = (artifact.get("metadata") or {}).get("ref_seq")
|
||||
# Legacy artifacts predate ref_seq; the full id works in every tool.
|
||||
handle = make_ref(int(ref_seq)) if ref_seq else str(artifact_id)
|
||||
filename = version.get("filename") or artifact.get("title") or str(artifact_id)
|
||||
mime = version.get("mime_type") or "application/octet-stream"
|
||||
lines.append(f"- {handle}: {filename} ({mime})")
|
||||
except Exception:
|
||||
logger.exception("Node document manifest failed; continuing without it")
|
||||
return ""
|
||||
if not lines:
|
||||
return ""
|
||||
return (
|
||||
"## Input documents for this node\n"
|
||||
"These files are staged for this node. Pass a ref (e.g. A1) or the "
|
||||
"filename to a document or code tool to read the file:\n" + "\n".join(lines)
|
||||
)
|
||||
|
||||
def _materialize_node_attachments(
|
||||
self,
|
||||
node_config: AgentNodeConfig,
|
||||
@@ -1135,7 +1188,7 @@ class WorkflowEngine:
|
||||
completed_at=log.get("completed_at"),
|
||||
duration_ms=log.get("duration_ms"),
|
||||
error=log.get("error"),
|
||||
state_snapshot=log.get("state_snapshot", {}),
|
||||
state_delta=log.get("state_delta", {}),
|
||||
tool_calls=log.get("tool_calls", []),
|
||||
)
|
||||
for log in self.execution_log
|
||||
|
||||
@@ -3030,6 +3030,7 @@ function WorkflowBuilderInner() {
|
||||
<Sheet open={showPreview} onOpenChange={setShowPreview}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
title="Workflow preview"
|
||||
className="bg-card w-full max-w-none p-0 sm:max-w-[600px] md:max-w-[700px] lg:max-w-[800px]"
|
||||
>
|
||||
<WorkflowPreview
|
||||
|
||||
@@ -145,8 +145,8 @@ function ExecutionDetails({
|
||||
const node = nodes.find((n) => n.id === step.nodeId);
|
||||
const displayName =
|
||||
node?.title || node?.data?.title || step.nodeTitle;
|
||||
const stateVars = step.stateSnapshot
|
||||
? Object.entries(step.stateSnapshot).filter(
|
||||
const stateVars = step.stateDelta
|
||||
? Object.entries(step.stateDelta).filter(
|
||||
([key]) => !['query', 'chat_history'].includes(key),
|
||||
)
|
||||
: [];
|
||||
@@ -253,10 +253,12 @@ function RunArtifactsSection({
|
||||
workflowRunId,
|
||||
isOpen,
|
||||
onToggle,
|
||||
runInProgress = false,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
runInProgress?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4 flex w-full flex-col flex-wrap items-start self-start lg:flex-nowrap">
|
||||
@@ -289,7 +291,12 @@ function RunArtifactsSection({
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="max-h-[480px] overflow-y-auto pr-2">
|
||||
{isOpen && <WorkflowRunArtifacts workflowRunId={workflowRunId} />}
|
||||
{isOpen && (
|
||||
<WorkflowRunArtifacts
|
||||
workflowRunId={workflowRunId}
|
||||
inProgress={runInProgress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -687,9 +694,10 @@ export default function WorkflowPreview({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Run artifacts (only once a persisted run id is known
|
||||
and the run is no longer streaming) */}
|
||||
{query.workflowRunId && !isStreamingLastQuery && (
|
||||
{/* Run artifacts — shown as soon as a persisted run id
|
||||
is known; mid-run it lists artifacts as nodes produce
|
||||
them and refetches when the run completes. */}
|
||||
{query.workflowRunId && (
|
||||
<RunArtifactsSection
|
||||
workflowRunId={query.workflowRunId}
|
||||
isOpen={openArtifactsIndex === index}
|
||||
@@ -698,6 +706,7 @@ export default function WorkflowPreview({
|
||||
openArtifactsIndex === index ? null : index,
|
||||
)
|
||||
}
|
||||
runInProgress={isStreamingLastQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -21,11 +21,15 @@ interface RunArtifactSummary {
|
||||
|
||||
interface WorkflowRunArtifactsProps {
|
||||
workflowRunId: string;
|
||||
/** True while the run is still streaming; flipping to false refetches so
|
||||
* a panel opened mid-run picks up the final artifact list. */
|
||||
inProgress?: boolean;
|
||||
}
|
||||
|
||||
/** List a workflow run's produced artifacts, with click-through preview + download. */
|
||||
export default function WorkflowRunArtifacts({
|
||||
workflowRunId,
|
||||
inProgress = false,
|
||||
}: WorkflowRunArtifactsProps) {
|
||||
const token = useSelector(selectToken);
|
||||
const [artifacts, setArtifacts] = useState<RunArtifactSummary[] | null>(null);
|
||||
@@ -77,7 +81,7 @@ export default function WorkflowRunArtifacts({
|
||||
useEffect(() => {
|
||||
const cleanup = loadList();
|
||||
return cleanup;
|
||||
}, [loadList]);
|
||||
}, [loadList, inProgress]);
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(artifactId: string) => {
|
||||
@@ -147,7 +151,9 @@ export default function WorkflowRunArtifacts({
|
||||
if (!artifacts || artifacts.length === 0) {
|
||||
return (
|
||||
<div className="px-3 py-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
No artifacts produced by this run.
|
||||
{inProgress
|
||||
? 'Run in progress — artifacts appear here as nodes produce them.'
|
||||
: 'No artifacts produced by this run.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface WorkflowExecutionStep {
|
||||
reasoning?: string;
|
||||
startedAt?: number;
|
||||
completedAt?: number;
|
||||
stateSnapshot?: Record<string, unknown>;
|
||||
stateDelta?: Record<string, unknown>;
|
||||
output?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
@@ -212,7 +212,8 @@ export const fetchWorkflowPreviewAnswer = createAsyncThunk<
|
||||
nodeTitle: data.node_title,
|
||||
status: data.status,
|
||||
reasoning: data.reasoning,
|
||||
stateSnapshot: data.state_snapshot,
|
||||
// Older streams (pre-rename) still say state_snapshot.
|
||||
stateDelta: data.state_delta ?? data.state_snapshot,
|
||||
output: data.output,
|
||||
error: data.error,
|
||||
},
|
||||
@@ -410,7 +411,7 @@ export const workflowPreviewSlice = createSlice({
|
||||
nodeTitle: step.nodeTitle,
|
||||
status: step.status,
|
||||
reasoning: step.reasoning,
|
||||
stateSnapshot: step.stateSnapshot,
|
||||
stateDelta: step.stateDelta,
|
||||
output: step.output,
|
||||
error: step.error,
|
||||
startedAt:
|
||||
@@ -426,8 +427,8 @@ export const workflowPreviewSlice = createSlice({
|
||||
};
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
updatedStep.stateSnapshot =
|
||||
step.stateSnapshot ?? querySteps[existingIndex].stateSnapshot;
|
||||
updatedStep.stateDelta =
|
||||
step.stateDelta ?? querySteps[existingIndex].stateDelta;
|
||||
updatedStep.output = step.output ?? querySteps[existingIndex].output;
|
||||
updatedStep.error = step.error ?? querySteps[existingIndex].error;
|
||||
querySteps[existingIndex] = updatedStep;
|
||||
|
||||
@@ -521,6 +521,7 @@ export default function ArtifactSidebar({
|
||||
<SheetContent
|
||||
side="right"
|
||||
showCloseButton={false}
|
||||
title={title || 'Artifact preview'}
|
||||
className="dark:bg-card bg-card flex h-full w-80 flex-col gap-0 border-l border-[#9ca3af]/10 p-0 sm:w-96 sm:max-w-none"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-700">
|
||||
|
||||
@@ -188,6 +188,12 @@ export function displayFilename(
|
||||
return version?.filename || artifact.title || `artifact-${artifact.id}`;
|
||||
}
|
||||
|
||||
// `frame-ancestors` is ignored when delivered via a <meta> element (CSP spec),
|
||||
// and these CSPs live in the srcdoc document's <meta> — there is no HTTP
|
||||
// response to attach a real header to. It only produced a console warning while
|
||||
// doing nothing; the preview iframe's framing is already constrained by its
|
||||
// sandbox (no allow-same-origin), so the directive is omitted here.
|
||||
|
||||
/**
|
||||
* Restrictive CSP for the preview iframe. No network, no scripts from origins,
|
||||
* no plugins; inline styles/SVG allowed so generated markup renders. Used for
|
||||
@@ -195,8 +201,7 @@ export function displayFilename(
|
||||
*/
|
||||
export const PREVIEW_CSP =
|
||||
"default-src 'none'; img-src data: blob:; style-src 'unsafe-inline'; " +
|
||||
"font-src data:; script-src 'none'; form-action 'none'; base-uri 'none'; " +
|
||||
"frame-ancestors 'none'";
|
||||
"font-src data:; script-src 'none'; form-action 'none'; base-uri 'none'";
|
||||
|
||||
/**
|
||||
* CSP for the mermaid preview. Mermaid's `securityLevel: 'sandbox'` output is an
|
||||
@@ -208,7 +213,7 @@ export const PREVIEW_CSP =
|
||||
export const PREVIEW_CSP_MERMAID =
|
||||
"default-src 'none'; img-src data: blob:; style-src 'unsafe-inline'; " +
|
||||
"font-src data:; script-src 'none'; frame-src data:; form-action 'none'; " +
|
||||
"base-uri 'none'; frame-ancestors 'none'";
|
||||
"base-uri 'none'";
|
||||
|
||||
/**
|
||||
* Wrap artifact markup in a minimal HTML document carrying the restrictive CSP.
|
||||
|
||||
@@ -47,10 +47,15 @@ function SheetContent({
|
||||
children,
|
||||
side = 'right',
|
||||
showCloseButton = true,
|
||||
title,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
showCloseButton?: boolean;
|
||||
// Accessible name for the dialog. Radix warns when a Dialog has no Title;
|
||||
// pass this to render a visually-hidden one when the panel has no visible
|
||||
// heading of its own (omit it if the children already render a SheetTitle).
|
||||
title?: string;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
@@ -71,6 +76,7 @@ function SheetContent({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{title ? <SheetTitle className="sr-only">{title}</SheetTitle> : null}
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
|
||||
@@ -851,6 +851,7 @@ const ConversationBubble = forwardRef<
|
||||
<Sheet open={isSidebarOpen} onOpenChange={setIsSidebarOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
title="Sources"
|
||||
className="bg-card w-64 border-l border-[#9ca3af]/10 sm:w-80 sm:max-w-none"
|
||||
>
|
||||
<div className="flex h-full flex-col items-center gap-2 px-6 py-4 text-center">
|
||||
|
||||
@@ -573,7 +573,7 @@ class TestGetExecutionSummary:
|
||||
"started_at": now,
|
||||
"completed_at": now,
|
||||
"error": None,
|
||||
"state_snapshot": {},
|
||||
"state_delta": {},
|
||||
}
|
||||
]
|
||||
summary = engine.get_execution_summary()
|
||||
@@ -879,10 +879,10 @@ class TestStepTelemetry:
|
||||
by_node = {log["node_id"]: log for log in engine.execution_log}
|
||||
|
||||
# The start node's delta carries the initial state it set...
|
||||
assert "query" in by_node["n1"]["state_snapshot"]
|
||||
assert "query" in by_node["n1"]["state_delta"]
|
||||
# ...and later nodes repeat none of it, only what they changed.
|
||||
assert by_node["n2"]["state_snapshot"] == {"x": 2}
|
||||
assert by_node["n3"]["state_snapshot"] == {}
|
||||
assert by_node["n2"]["state_delta"] == {"x": 2}
|
||||
assert by_node["n3"]["state_delta"] == {}
|
||||
|
||||
# The streamed step events carry the same deltas.
|
||||
streamed = {
|
||||
@@ -890,8 +890,8 @@ class TestStepTelemetry:
|
||||
for e in events
|
||||
if e.get("type") == "workflow_step" and e.get("status") == "completed"
|
||||
}
|
||||
assert streamed["n2"]["state_snapshot"] == {"x": 2}
|
||||
assert "query" not in streamed["n2"]["state_snapshot"]
|
||||
assert streamed["n2"]["state_delta"] == {"x": 2}
|
||||
assert "query" not in streamed["n2"]["state_delta"]
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_steps_carry_duration_ms(self):
|
||||
@@ -925,7 +925,7 @@ class TestStepTelemetry:
|
||||
events = list(engine.execute({}, "q"))
|
||||
|
||||
failed = [e for e in events if e.get("status") == "failed"]
|
||||
assert failed and "query" not in failed[0]["state_snapshot"]
|
||||
assert failed and "query" not in failed[0]["state_delta"]
|
||||
failed_log = engine.execution_log[-1]
|
||||
assert failed_log["status"] == "failed"
|
||||
assert failed_log["duration_ms"] >= 0
|
||||
@@ -962,3 +962,99 @@ class TestStepTelemetry:
|
||||
"status": "completed",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestNodeDocumentManifest:
|
||||
"""The engine appends a per-node manifest of staged input documents to the
|
||||
node system prompt, scoped to that node's input_documents selection."""
|
||||
|
||||
_ART_1 = "11111111-1111-4111-8111-111111111111"
|
||||
_ART_2 = "22222222-2222-4222-8222-222222222222"
|
||||
|
||||
def _engine_with_docs(self, monkeypatch):
|
||||
from contextlib import contextmanager
|
||||
|
||||
engine = WorkflowEngine(_make_graph([], []), _make_agent())
|
||||
engine.state["input_documents"] = [
|
||||
{"artifact_id": self._ART_1, "ref": "A1", "filename": "portfolio_v1.csv"},
|
||||
{"artifact_id": self._ART_2, "ref": "A2", "filename": "notes.txt"},
|
||||
]
|
||||
|
||||
rows = {
|
||||
self._ART_1: (
|
||||
{"current_version": 3, "metadata": {"ref_seq": 1}, "title": "Portfolio"},
|
||||
{"filename": "portfolio_v1.csv", "mime_type": "text/csv"},
|
||||
),
|
||||
self._ART_2: (
|
||||
{"current_version": 1, "metadata": {}, "title": "Notes"},
|
||||
{"filename": "notes.txt", "mime_type": "text/plain"},
|
||||
),
|
||||
}
|
||||
|
||||
class FakeRepo:
|
||||
def __init__(self, conn):
|
||||
pass
|
||||
|
||||
def resolve_id_by_ref_seq(self, position, conversation_id=None, workflow_run_id=None):
|
||||
for artifact_id, (artifact, _version) in rows.items():
|
||||
if artifact["metadata"].get("ref_seq") == position:
|
||||
return artifact_id
|
||||
return None
|
||||
|
||||
def artifact_id_at_position(self, position, conversation_id=None, workflow_run_id=None):
|
||||
return None
|
||||
|
||||
def get_artifact_in_parent(self, artifact_id, conversation_id=None, workflow_run_id=None):
|
||||
entry = rows.get(artifact_id)
|
||||
return entry[0] if entry else None
|
||||
|
||||
def get_version(self, artifact_id, version):
|
||||
return rows[artifact_id][1]
|
||||
|
||||
@contextmanager
|
||||
def fake_readonly():
|
||||
yield object()
|
||||
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", fake_readonly)
|
||||
monkeypatch.setattr(
|
||||
"application.storage.db.repositories.artifacts.ArtifactsRepository", FakeRepo
|
||||
)
|
||||
return engine
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_manifest_lists_refs_filenames_and_mimes(self, monkeypatch):
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
engine = self._engine_with_docs(monkeypatch)
|
||||
config = AgentNodeConfig(agent_type="classic", input_documents=["*"])
|
||||
manifest = engine._node_document_manifest(config)
|
||||
assert "- A1: portfolio_v1.csv (text/csv)" in manifest
|
||||
# Legacy artifact without ref_seq falls back to the full id, which tools accept.
|
||||
assert f"- {self._ART_2}: notes.txt (text/plain)" in manifest
|
||||
assert "Pass a ref" in manifest
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_manifest_empty_without_selection(self, monkeypatch):
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
engine = self._engine_with_docs(monkeypatch)
|
||||
config = AgentNodeConfig(agent_type="classic", input_documents=[])
|
||||
assert engine._node_document_manifest(config) == ""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_manifest_failure_never_breaks_the_node(self, monkeypatch):
|
||||
from contextlib import contextmanager
|
||||
|
||||
from application.agents.workflows.schemas import AgentNodeConfig
|
||||
|
||||
engine = WorkflowEngine(_make_graph([], []), _make_agent())
|
||||
engine.state["input_documents"] = [{"artifact_id": self._ART_1}]
|
||||
|
||||
@contextmanager
|
||||
def broken_readonly():
|
||||
raise RuntimeError("db down")
|
||||
yield
|
||||
|
||||
monkeypatch.setattr("application.storage.db.session.db_readonly", broken_readonly)
|
||||
config = AgentNodeConfig(agent_type="classic", input_documents=["*"])
|
||||
assert engine._node_document_manifest(config) == ""
|
||||
@@ -468,7 +468,7 @@ class TestNodeExecutionLog:
|
||||
assert log.node_id == "n1"
|
||||
assert log.completed_at is None
|
||||
assert log.error is None
|
||||
assert log.state_snapshot == {}
|
||||
assert log.state_delta == {}
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_full_log(self):
|
||||
@@ -481,10 +481,10 @@ class TestNodeExecutionLog:
|
||||
started_at=started,
|
||||
completed_at=completed,
|
||||
error=None,
|
||||
state_snapshot={"key": "value"},
|
||||
state_delta={"key": "value"},
|
||||
)
|
||||
assert log.completed_at == completed
|
||||
assert log.state_snapshot == {"key": "value"}
|
||||
assert log.state_delta == {"key": "value"}
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_extra_forbidden(self):
|
||||
|
||||
Reference in new issue
Block a user