Persist workflow artifacts and auto-run migrations

This commit is contained in:
Codex
2026-05-13 23:34:14 +08:00
parent ba9f24086a
commit 84cbd0a8c4
9 changed files with 301 additions and 14 deletions
+8 -4
View File
@@ -2,7 +2,7 @@
## Current Goal
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. This run closed F26/H27 as a real backend batch: digest preferences now persist on the server, digest snapshots archive into a history table, cockpit reads back the persisted mode/history archive, and the front-end writes preference changes back through a dedicated API instead of local-only storage. Local validation passed, remote migration `044_create_decision_assistant_digest_state.sql` was applied, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. This run closed F26/W5/H27 as a real backend persistence batch: digest preferences now persist on the server, digest snapshots archive into a history table, workflow artifacts archive into their own history table, cockpit reads back both persisted digest/workflow archives, and the front-end writes digest preference changes back through a dedicated API instead of local-only storage. Local validation passed, remote migrations `044_create_decision_assistant_digest_state.sql` and `045_create_decision_assistant_workflow_history.sql` were applied, the current working tree was deployed, and remote smoke confirmed the latest Decision Assistant fields on the live host.
## Current Branch
@@ -131,6 +131,8 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_review_handoff_slice: review_entry_handoff_summary
- latest_decision_assistant_digest_persistence_slice: digest_preferences_api_archive_history
- latest_decision_assistant_digest_archive_slice: persisted_digest_archive_visibility
- latest_decision_assistant_workflow_persistence_slice: workflow_history_archive_persistence
- latest_decision_assistant_workflow_archive_slice: persisted_workflow_archive_visibility
- latest_decision_assistant_summary_status: implemented
- latest_decision_assistant_change_summary_status: implemented
- latest_decision_assistant_evidence_status: implemented
@@ -246,10 +248,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_review_handoff_status: implemented
- latest_decision_assistant_digest_persistence_status: implemented
- latest_decision_assistant_digest_archive_status: implemented
- latest_decision_assistant_workflow_persistence_status: implemented
- latest_decision_assistant_workflow_archive_status: implemented
- latest_decision_assistant_workbench_status: visible_local_smoke
- latest_remote_decision_assistant_deploy_status: deployed_current_head
- latest_remote_decision_assistant_smoke_status: passed
- latest_remote_decision_assistant_review_packet_status: digest_preferences_archive_visible
- latest_remote_decision_assistant_review_packet_status: digest_and_workflow_archives_visible
- latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- latest_p14_slice: webhook_transport_retry
- latest_p15_pxx_tasklist_status: ready
@@ -665,12 +669,12 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- Latest E7 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed `chart_adapter_mode=optional_reference`, `chart_primary_mode=internal_review_packet_html`, and a non-blocking external chart adapter even when other supporting context sources are stale.
- Latest W4/D25/F25/H25 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed `workflow_artifact_summary`, `digest_action_queue`, `learning_focus_matrix`, and `workspace_continuity_handoff` plus matching UI strings.
- Latest C3/G20/H26 validation passed: focused operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, `node --check src/web/app.js`, deploy smoke passed 11 endpoints, remote ops smoke passed 9 checks, and remote field smoke confirmed `watchlist focus matrix`, `review_entry_action_queue`, `review_entry_handoff_summary`, and watchlist-focus presence inside `assistant_loop_queue`.
- Latest F26/H27 validation passed: focused operator workspace/API tests, 69 related unit/API/static tests, 714 full unit tests, 73 script static checks, `node --check src/web/app.js`, remote migration `044_create_decision_assistant_digest_state.sql` applied successfully, deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, and `digest_history_archive`.
- Latest F26/W5/H27 validation passed: focused operator workspace/API/deploy tests, 75 related unit/API/static tests, 715 full unit tests, 73 script static checks, `node --check src/web/app.js`, remote migrations `044_create_decision_assistant_digest_state.sql` and `045_create_decision_assistant_workflow_history.sql` applied successfully, deploy completed, remote ops smoke passed 9 checks, and remote field smoke confirmed `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, `digest_history_archive`, and `workflow_history_archive`.
- Real-live order submission remains intentionally disabled because the OKX order adapter is still not a real-live submitter.
## Exact Next Step
F26/H27 backend digest persistence batch is closed. External chart URLs remain optional reference enhancements and are not required for the current analysis-only loop. Internal non-trading backlog still exists, but PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
F26/W5/H27 backend persistence batch is closed. External chart URLs remain optional reference enhancements and are not required for the current analysis-only loop. Internal non-trading backlog still exists, but PXX remains closed; do not create P25/P26 automatically, and do not enable real trading.
## Validation Commands Used Recently
+8 -3
View File
@@ -161,6 +161,7 @@
- [x] G20 review entry action queue slice
- [x] H26 review/watchlist continuity into loop queue slice
- [x] F26 digest preference backend persistence / history archive slice
- [x] W5 workflow artifact archive persistence slice
- [x] H27 persisted digest archive / cockpit sync slice
- [x] P8 local closure complete
- [x] P9 local closure complete
@@ -286,6 +287,8 @@
- [x] latest_decision_assistant_review_handoff_slice: review_entry_handoff_summary
- [x] latest_decision_assistant_digest_persistence_slice: digest_preferences_api_archive_history
- [x] latest_decision_assistant_digest_archive_slice: persisted_digest_archive_visibility
- [x] latest_decision_assistant_workflow_persistence_slice: workflow_history_archive_persistence
- [x] latest_decision_assistant_workflow_archive_slice: persisted_workflow_archive_visibility
- [x] latest_decision_assistant_summary_status: implemented
- [x] latest_decision_assistant_change_summary_status: implemented
- [x] latest_decision_assistant_evidence_status: implemented
@@ -398,10 +401,12 @@
- [x] latest_decision_assistant_review_handoff_status: implemented
- [x] latest_decision_assistant_digest_persistence_status: implemented
- [x] latest_decision_assistant_digest_archive_status: implemented
- [x] latest_decision_assistant_workflow_persistence_status: implemented
- [x] latest_decision_assistant_workflow_archive_status: implemented
- [x] latest_decision_assistant_workbench_status: visible_local_smoke
- [x] latest_remote_decision_assistant_deploy_status: deployed_current_head
- [x] latest_remote_decision_assistant_smoke_status: passed
- [x] latest_remote_decision_assistant_review_packet_status: digest_preferences_archive_visible
- [x] latest_remote_decision_assistant_review_packet_status: digest_and_workflow_archives_visible
- [x] latest_p7_selector_choice: continue_periodic_analysis_decision_monitoring_with_maturity_rollup
- [x] latest_p14_slice: webhook_transport_retry
- [x] latest_p15_pxx_tasklist_status: ready
@@ -713,8 +718,8 @@
- [x] Deployed W4/D25/F25/H25 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet/static field smoke for `workflow_artifact_summary`, `digest_action_queue`, `learning_focus_matrix`, and `workspace_continuity_handoff`
- [x] Completed C3/G20/H26 focused validation: operator workspace tests, 68 related unit/API/static tests, 713 full unit tests, 73 script static checks, and dashboard JS syntax
- [x] Deployed C3/G20/H26 current tree and passed remote smoke: 11 deploy endpoints, 9 remote ops checks, and review-packet/static field smoke for `watchlist focus matrix`, `review_entry_action_queue`, `review_entry_handoff_summary`, and watchlist-focus presence inside `assistant_loop_queue`
- [x] Completed F26/H27 focused validation: operator workspace/API tests, 69 related unit/API/static tests, 714 full unit tests, 73 script static checks, and dashboard JS syntax
- [x] Deployed F26/H27 current tree, applied remote migration `044_create_decision_assistant_digest_state.sql`, and passed remote smoke for `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, and `digest_history_archive`
- [x] Completed F26/W5/H27 focused validation: operator workspace/API/deploy tests, 75 related unit/API/static tests, 715 full unit tests, 73 script static checks, and dashboard JS syntax
- [x] Deployed F26/W5/H27 current tree, applied remote migrations `044_create_decision_assistant_digest_state.sql` and `045_create_decision_assistant_workflow_history.sql`, and passed remote smoke for `/trading/alerts/digest-preferences`, persisted `digest_preference_profile`, `digest_history_archive`, and `workflow_history_archive`
## Blocked
@@ -0,0 +1,17 @@
create table if not exists decision_assistant_workflow_history (
id bigserial primary key,
profile_key varchar(64) not null default 'local_operator',
instrument_id bigint not null,
current_session varchar(32),
next_phase varchar(32),
next_artifact_kind varchar(64),
snapshot_signature varchar(128) not null,
payload jsonb not null default '{}'::jsonb,
created_ts timestamptz not null default now()
);
create index if not exists idx_decision_assistant_workflow_history_instrument_ts
on decision_assistant_workflow_history (instrument_id, created_ts desc);
create index if not exists idx_decision_assistant_workflow_history_signature
on decision_assistant_workflow_history (instrument_id, snapshot_signature);
+1
View File
@@ -123,6 +123,7 @@ def build_deploy_command(*, remote_path: str, remote_archive: str) -> str:
f"cd {remote_path_q}",
"((docker compose down || docker-compose down) >/dev/null 2>&1 || true)",
"((docker compose up -d --build) || (docker-compose up -d --build))",
"((docker compose exec -T ai-ict python scripts/apply_migrations.py) || (docker-compose exec -T ai-ict python scripts/apply_migrations.py))",
"((docker compose ps) || (docker-compose ps))",
]
)
@@ -167,3 +167,89 @@ class DecisionAssistantRepository:
},
).mappings().one()
return dict(row)
def fetch_recent_workflow_history(
self,
*,
instrument_id: int,
profile_key: str = DEFAULT_PROFILE_KEY,
limit: int = 20,
) -> list[dict]:
sql = text(
"""
select id, profile_key, instrument_id, current_session, next_phase,
next_artifact_kind, snapshot_signature, payload, created_ts
from decision_assistant_workflow_history
where instrument_id = :instrument_id and profile_key = :profile_key
order by created_ts desc, id desc
limit :limit
"""
)
with SessionLocal() as session:
rows = session.execute(
sql,
{"instrument_id": instrument_id, "profile_key": profile_key, "limit": limit},
).mappings().all()
return [dict(row) for row in rows]
def fetch_latest_workflow_history(
self,
*,
instrument_id: int,
profile_key: str = DEFAULT_PROFILE_KEY,
) -> dict | None:
rows = self.fetch_recent_workflow_history(instrument_id=instrument_id, profile_key=profile_key, limit=1)
return rows[0] if rows else None
def insert_workflow_history(
self,
*,
instrument_id: int,
profile_key: str = DEFAULT_PROFILE_KEY,
current_session: str | None = None,
next_phase: str | None = None,
next_artifact_kind: str | None = None,
snapshot_signature: str,
payload: dict | None = None,
created_ts=None,
) -> dict:
sql = text(
"""
insert into decision_assistant_workflow_history (
profile_key,
instrument_id,
current_session,
next_phase,
next_artifact_kind,
snapshot_signature,
payload,
created_ts
) values (
:profile_key,
:instrument_id,
:current_session,
:next_phase,
:next_artifact_kind,
:snapshot_signature,
cast(:payload as jsonb),
coalesce(:created_ts, now())
)
returning id, profile_key, instrument_id, current_session, next_phase,
next_artifact_kind, snapshot_signature, payload, created_ts
"""
)
with SessionLocal.begin() as session:
row = session.execute(
sql,
{
"profile_key": profile_key,
"instrument_id": instrument_id,
"current_session": current_session,
"next_phase": next_phase,
"next_artifact_kind": next_artifact_kind,
"snapshot_signature": snapshot_signature,
"payload": json.dumps(payload or {}, ensure_ascii=False, default=str),
"created_ts": created_ts,
},
).mappings().one()
return dict(row)
@@ -178,6 +178,10 @@ class DecisionAssistantSummaryService:
repository=self.decision_assistant_repository,
instrument_id=instrument_id,
)
persisted_workflow_history = _safe_fetch_workflow_history(
repository=self.decision_assistant_repository,
instrument_id=instrument_id,
)
summary = build_decision_assistant_summary(
instrument_id=instrument_id,
timeframe=timeframe,
@@ -196,25 +200,44 @@ class DecisionAssistantSummaryService:
recent_executions=list(recent_executions or []),
persisted_digest_preference=dict(persisted_digest_preference or {}),
persisted_digest_history=list(persisted_digest_history or []),
persisted_workflow_history=list(persisted_workflow_history or []),
)
persistence_result = _safe_persist_digest_history_snapshot(
repository=self.decision_assistant_repository,
instrument_id=instrument_id,
summary=summary,
)
workflow_persistence_result = _safe_persist_workflow_history_snapshot(
repository=self.decision_assistant_repository,
instrument_id=instrument_id,
summary=summary,
)
refreshed_digest_history = _safe_fetch_digest_history(
repository=self.decision_assistant_repository,
instrument_id=instrument_id,
)
refreshed_workflow_history = _safe_fetch_workflow_history(
repository=self.decision_assistant_repository,
instrument_id=instrument_id,
)
digest_history_archive = _build_decision_assistant_digest_history_archive(refreshed_digest_history)
workflow_history_archive = _build_decision_assistant_workflow_history_archive(refreshed_workflow_history)
summary["digest_history_archive"] = digest_history_archive
summary["digest_persistence_status"] = persistence_result.get("status")
summary["workflow_history_archive"] = workflow_history_archive
summary["workflow_persistence_status"] = workflow_persistence_result.get("status")
digest_surface = dict(summary.get("digest_surface") or {})
digest_surface["digest_history_archive"] = digest_history_archive
digest_surface["digest_history_archive_count"] = digest_history_archive.get("snapshot_count") or 0
digest_surface["next_digest_history_archive_review"] = digest_history_archive.get("next_review_prompt")
digest_surface["digest_persistence_status"] = persistence_result.get("status")
summary["digest_surface"] = digest_surface
workflow_artifact_summary = dict(summary.get("workflow_artifact_summary") or {})
workflow_artifact_summary["workflow_history_archive"] = workflow_history_archive
workflow_artifact_summary["workflow_history_archive_count"] = workflow_history_archive.get("snapshot_count") or 0
workflow_artifact_summary["next_workflow_history_review"] = workflow_history_archive.get("next_review_prompt")
workflow_artifact_summary["workflow_persistence_status"] = workflow_persistence_result.get("status")
summary["workflow_artifact_summary"] = workflow_artifact_summary
return summary
@@ -295,6 +318,13 @@ def _safe_fetch_digest_history(*, repository: DecisionAssistantRepository, instr
return []
def _safe_fetch_workflow_history(*, repository: DecisionAssistantRepository, instrument_id: int, limit: int = 20) -> list[dict]:
try:
return list(repository.fetch_recent_workflow_history(instrument_id=instrument_id, limit=limit) or [])
except Exception:
return []
def _safe_persist_digest_history_snapshot(
*,
repository: DecisionAssistantRepository,
@@ -332,6 +362,43 @@ def _safe_persist_digest_history_snapshot(
return {"status": "unavailable", "snapshot_signature": signature}
def _safe_persist_workflow_history_snapshot(
*,
repository: DecisionAssistantRepository,
instrument_id: int,
summary: dict,
) -> dict:
workflow_artifact_summary = dict(summary.get("workflow_artifact_summary") or {})
workflow_artifacts = list(summary.get("workflow_artifacts") or [])
snapshot_payload = {
"current_session": summary.get("current_session"),
"focus_mode": summary.get("focus_mode"),
"artifact_count": workflow_artifact_summary.get("artifact_count"),
"next_artifact_kind": workflow_artifact_summary.get("next_artifact_kind"),
"next_artifact_phase": workflow_artifact_summary.get("next_artifact_phase"),
"ready_for_handoff_count": workflow_artifact_summary.get("ready_for_handoff_count"),
"needs_update_count": workflow_artifact_summary.get("needs_update_count"),
"pending_capture_count": workflow_artifact_summary.get("pending_capture_count"),
"artifacts": workflow_artifacts[:3],
}
signature = hashlib.sha1(json.dumps(snapshot_payload, sort_keys=True, ensure_ascii=False, default=str).encode("utf-8")).hexdigest()
try:
latest = repository.fetch_latest_workflow_history(instrument_id=instrument_id)
if latest and str(latest.get("snapshot_signature") or "") == signature:
return {"status": "duplicate_skipped", "snapshot_signature": signature}
repository.insert_workflow_history(
instrument_id=instrument_id,
current_session=str(summary.get("current_session") or ""),
next_phase=str(workflow_artifact_summary.get("next_artifact_phase") or ""),
next_artifact_kind=str(workflow_artifact_summary.get("next_artifact_kind") or ""),
snapshot_signature=signature,
payload=snapshot_payload,
)
return {"status": "persisted", "snapshot_signature": signature}
except Exception:
return {"status": "unavailable", "snapshot_signature": signature}
def _build_research_council_summary(run: Optional[dict]) -> dict:
payload = dict(((run or {}).get("payload") or {}).get("decision") or {})
return {
@@ -368,6 +435,7 @@ def build_decision_assistant_summary(
recent_executions: list[dict],
persisted_digest_preference: dict,
persisted_digest_history: list[dict],
persisted_workflow_history: list[dict],
) -> dict:
market_state = dict(launch_readiness.get("market_state") or {})
session_state = dict(market_state.get("session") or {})
@@ -597,6 +665,7 @@ def build_decision_assistant_summary(
"digest_history_groups": digest_history_groups,
"digest_history_browser": digest_history_browser,
"digest_history_archive": digest_history_archive,
"workflow_history_archive": _build_decision_assistant_workflow_history_archive(persisted_workflow_history),
"digest_surface": digest_surface,
"setup_learning_rollup": setup_learning_rollup,
"decision_timeline": decision_timeline,
@@ -3703,6 +3772,47 @@ def _build_decision_assistant_digest_history_archive(rows: list[dict]) -> dict:
}
def _build_decision_assistant_workflow_history_archive(rows: list[dict]) -> dict:
if not rows:
return {
"status": "idle",
"snapshot_count": 0,
"latest_created_ts": None,
"latest_next_phase": None,
"next_review_prompt": None,
"items": [],
}
items = []
for row in rows[:12]:
payload = dict(row.get("payload") or {})
items.append(
{
"created_ts": row.get("created_ts"),
"current_session": row.get("current_session"),
"next_phase": row.get("next_phase"),
"next_artifact_kind": row.get("next_artifact_kind"),
"focus_mode": payload.get("focus_mode"),
"artifact_count": payload.get("artifact_count"),
"ready_for_handoff_count": payload.get("ready_for_handoff_count"),
"needs_update_count": payload.get("needs_update_count"),
"pending_capture_count": payload.get("pending_capture_count"),
}
)
latest = dict(items[0] or {})
return {
"status": "active",
"snapshot_count": len(rows),
"latest_created_ts": latest.get("created_ts"),
"latest_next_phase": latest.get("next_phase"),
"next_review_prompt": (
f"Review persisted workflow snapshot from {latest.get('created_ts')} before creating the next phase brief."
if latest.get("created_ts")
else None
),
"items": items,
}
def _digest_history_browser_hint(*, recommended_kind: str, total_count: int) -> str:
if recommended_kind:
return f"Open {recommended_kind} first, then review the remaining {total_count} digest history item(s) by freshness debt."
+28 -7
View File
@@ -1397,13 +1397,16 @@ function renderDecisionAssistant(summary) {
buildInfoGrid([
['workflow artifacts', summary.workflow_artifact_summary.status || 'idle'],
['artifact count', String(summary.workflow_artifact_summary.artifact_count ?? 0)],
['artifacts ready', String(summary.workflow_artifact_summary.ready_for_handoff_count ?? 0)],
['artifacts updating', String(summary.workflow_artifact_summary.needs_update_count ?? 0)],
['artifacts pending', String(summary.workflow_artifact_summary.pending_capture_count ?? 0)],
['next artifact', summary.workflow_artifact_summary.next_artifact_kind || 'n/a'],
['next artifact prompt', summary.workflow_artifact_summary.next_artifact_prompt || 'n/a'],
])
);
['artifacts ready', String(summary.workflow_artifact_summary.ready_for_handoff_count ?? 0)],
['artifacts updating', String(summary.workflow_artifact_summary.needs_update_count ?? 0)],
['artifacts pending', String(summary.workflow_artifact_summary.pending_capture_count ?? 0)],
['next artifact', summary.workflow_artifact_summary.next_artifact_kind || 'n/a'],
['next artifact prompt', summary.workflow_artifact_summary.next_artifact_prompt || 'n/a'],
['workflow archive', String(summary.workflow_artifact_summary.workflow_history_archive_count ?? 0)],
['workflow archive review', summary.workflow_artifact_summary.next_workflow_history_review || 'n/a'],
['workflow persistence', summary.workflow_artifact_summary.workflow_persistence_status || 'n/a'],
])
);
}
const workflowGrid = createElement('div', 'cards');
workflowDigest.forEach((item) => {
@@ -1445,6 +1448,24 @@ function renderDecisionAssistant(summary) {
});
decisionAssistantPanel.appendChild(artifactGrid);
}
if (summary.workflow_artifact_summary?.workflow_history_archive?.items?.length) {
const workflowArchiveGrid = createElement('div', 'cards assistant-tight-grid');
(summary.workflow_artifact_summary.workflow_history_archive.items || []).forEach((item) => {
workflowArchiveGrid.appendChild(
buildItemCard(
item.next_artifact_kind || item.next_phase || 'workflow archive',
item.current_session || 'session',
[
['time', formatTimestamp(item.created_ts)],
['focus mode', item.focus_mode || 'n/a'],
['artifacts', String(item.artifact_count ?? 0)],
['ready/update/pending', `${item.ready_for_handoff_count ?? 0} / ${item.needs_update_count ?? 0} / ${item.pending_capture_count ?? 0}`],
]
)
);
});
decisionAssistantPanel.appendChild(workflowArchiveGrid);
}
}
if (opportunities.length) {
appendSectionTitle(decisionAssistantPanel, 'Opportunities');
+8
View File
@@ -35,6 +35,14 @@ class DeployWithPasswordTests(unittest.TestCase):
self.assertIn("/trading/maintenance-summary?instrument_id=1", smoke_source)
self.assertIn("/trading/unattended-harness?instrument_id=1", smoke_source)
def test_deploy_command_runs_migrations_after_compose_up(self) -> None:
command = deploy_with_password.build_deploy_command(
remote_path="/root/deploy/ai-ict",
remote_archive="/root/ai-ict-deploy.tgz",
)
self.assertIn("scripts/apply_migrations.py", command)
if __name__ == "__main__":
unittest.main()
@@ -184,6 +184,9 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("workflow_artifact_summary", summary)
self.assertTrue(summary["workflow_artifacts"])
self.assertIn("next_artifact_kind", summary["workflow_artifact_summary"])
self.assertIn("workflow_history_archive", summary["workflow_artifact_summary"])
self.assertIn("workflow_history_archive_count", summary["workflow_artifact_summary"])
self.assertIn("workflow_persistence_status", summary["workflow_artifact_summary"])
self.assertIn("assistant_loop_queue", summary)
self.assertIn("assistant_loop_summary", summary)
self.assertTrue(summary["assistant_loop_queue"])
@@ -351,6 +354,7 @@ class OperatorWorkspaceServiceTests(unittest.TestCase):
self.assertIn("digest_history_archive", summary["digest_surface"])
self.assertIn("digest_history_archive_count", summary["digest_surface"])
self.assertIn("digest_persistence_status", summary["digest_surface"])
self.assertIn("next_digest_history_archive_review", summary["digest_surface"])
self.assertIn("next_high_value_change", summary["digest_surface"])
self.assertIn("digest_review_back_actions", summary["digest_surface"])
self.assertIn("digest_review_back_action_count", summary["digest_surface"])
@@ -896,6 +900,37 @@ class _DecisionAssistantRepo:
"created_ts": "2026-05-12T02:07:00+00:00",
}
def fetch_recent_workflow_history(self, **kwargs):
return [
{
"profile_key": "local_operator",
"instrument_id": kwargs["instrument_id"],
"current_session": "LONDON",
"next_phase": "intraday",
"next_artifact_kind": "intraday_monitor_digest",
"snapshot_signature": "unit-workflow-signature",
"payload": {
"focus_mode": "review_top_setup",
"artifact_count": 3,
"ready_for_handoff_count": 1,
"needs_update_count": 1,
"pending_capture_count": 1,
},
"created_ts": "2026-05-12T02:08:00+00:00",
}
]
def fetch_latest_workflow_history(self, **kwargs):
return self.fetch_recent_workflow_history(**kwargs)[0]
def insert_workflow_history(self, **kwargs):
return {
"id": 10,
"instrument_id": kwargs["instrument_id"],
"snapshot_signature": kwargs["snapshot_signature"],
"created_ts": "2026-05-12T02:09:00+00:00",
}
if __name__ == "__main__":
unittest.main()