@@ -22,6 +22,7 @@ vi.mock("../lib/api", () => ({
2222 fetchEvents : vi . fn ( ) ,
2323 fetchDiff : vi . fn ( ) ,
2424 fetchReports : vi . fn ( ) ,
25+ fetchArtifact : vi . fn ( ) ,
2526 fetchOperatorCopilotBrief : vi . fn ( ) ,
2627 fetchToolCalls : vi . fn ( ) ,
2728 fetchChainSpec : vi . fn ( ) ,
@@ -47,6 +48,7 @@ import {
4748 fetchEvents ,
4849 fetchDiff ,
4950 fetchReports ,
51+ fetchArtifact ,
5052 fetchOperatorCopilotBrief ,
5153 fetchToolCalls ,
5254 fetchChainSpec ,
@@ -95,6 +97,7 @@ describe("RunDetailPage p0 controls", () => {
9597 vi . mocked ( fetchEvents ) . mockReset ( ) ;
9698 vi . mocked ( fetchDiff ) . mockReset ( ) ;
9799 vi . mocked ( fetchReports ) . mockReset ( ) ;
100+ vi . mocked ( fetchArtifact ) . mockReset ( ) ;
98101 vi . mocked ( fetchToolCalls ) . mockReset ( ) ;
99102 vi . mocked ( fetchChainSpec ) . mockReset ( ) ;
100103 vi . mocked ( fetchAgentStatus ) . mockReset ( ) ;
@@ -121,6 +124,7 @@ describe("RunDetailPage p0 controls", () => {
121124 ] as any ) ;
122125 vi . mocked ( fetchDiff ) . mockResolvedValue ( { diff : "" } as any ) ;
123126 vi . mocked ( fetchReports ) . mockResolvedValue ( [ ] as any ) ;
127+ vi . mocked ( fetchArtifact ) . mockResolvedValue ( { data : [ ] } as any ) ;
124128 vi . mocked ( fetchOperatorCopilotBrief ) . mockResolvedValue ( {
125129 report_type : "operator_copilot_brief" ,
126130 generated_at : "2026-03-31T12:00:00Z" ,
@@ -255,12 +259,57 @@ describe("RunDetailPage p0 controls", () => {
255259 } ) ;
256260
257261 it ( "renders locale-aware operator labels on run detail when zh-CN is requested" , async ( ) => {
262+ vi . mocked ( fetchRun ) . mockResolvedValueOnce (
263+ makeRun ( {
264+ manifest : {
265+ artifacts : [
266+ { name : "planning_worker_prompt_contracts" , path : "artifacts/planning_worker_prompt_contracts.json" } ,
267+ { name : "planning_unblock_tasks" , path : "artifacts/planning_unblock_tasks.json" } ,
268+ ] ,
269+ } ,
270+ } ) ,
271+ ) ;
272+ vi . mocked ( fetchArtifact )
273+ . mockResolvedValueOnce ( {
274+ data : [
275+ {
276+ prompt_contract_id : "worker-zh" ,
277+ continuation_policy : {
278+ on_incomplete : "reply_auditor_reprompt_and_continue_same_session" ,
279+ on_blocked : "spawn_independent_temporary_unblock_task" ,
280+ } ,
281+ done_definition : { acceptance_checks : [ "repo_hygiene" , "test_report" ] } ,
282+ } ,
283+ ] ,
284+ } as any )
285+ . mockResolvedValueOnce ( {
286+ data : [
287+ {
288+ unblock_task_id : "unblock-zh" ,
289+ owner : "L0" ,
290+ mode : "independent_temporary_task" ,
291+ trigger : "spawn_independent_temporary_unblock_task" ,
292+ } ,
293+ ] ,
294+ } as any ) ;
295+
258296 render ( < RunDetailPage runId = "run-zh" onBack = { vi . fn ( ) } locale = "zh-CN" /> ) ;
259297
260298 expect ( await screen . findByText ( "AI 操作员副驾驶" ) ) . toBeInTheDocument ( ) ;
261299 expect ( screen . getByText ( "Run 总览" ) ) . toBeInTheDocument ( ) ;
262300 expect ( screen . getByText ( "执行角色" ) ) . toBeInTheDocument ( ) ;
263301 expect ( screen . getByText ( "证据与可追溯性" ) ) . toBeInTheDocument ( ) ;
302+ expect ( screen . getByText ( "完成治理摘要" ) ) . toBeInTheDocument ( ) ;
303+ expect ( screen . queryByText ( "Worker prompt contracts" ) ) . toBeNull ( ) ;
304+ expect ( screen . getByText ( "工作者提示合约" ) ) . toBeInTheDocument ( ) ;
305+ expect ( screen . getByText ( "未完成时" ) ) . toBeInTheDocument ( ) ;
306+ expect ( screen . getByText ( "阻塞时" ) ) . toBeInTheDocument ( ) ;
307+ expect ( screen . getByText ( "解阻塞任务" ) ) . toBeInTheDocument ( ) ;
308+ expect (
309+ screen . getByText (
310+ "这些摘要来自持久化的工作者提示合约和解阻塞任务;它们只提供参考,`task_contract` 仍然掌握执行权威。" ,
311+ ) ,
312+ ) . toBeInTheDocument ( ) ;
264313 expect ( screen . getByRole ( "button" , { name : "运行中" } ) ) . toHaveAttribute ( "title" , "暂停实时更新" ) ;
265314 expect ( screen . getByRole ( "button" , { name : / 事 件 时 间 线 ( 1 ) / } ) ) . toBeInTheDocument ( ) ;
266315 expect ( screen . getByRole ( "button" , { name : "回放对比" } ) ) . toBeInTheDocument ( ) ;
@@ -320,8 +369,37 @@ describe("RunDetailPage p0 controls", () => {
320369 } ,
321370 } ,
322371 } ,
372+ manifest : {
373+ artifacts : [
374+ { name : "planning_worker_prompt_contracts" , path : "artifacts/planning_worker_prompt_contracts.json" } ,
375+ { name : "planning_unblock_tasks" , path : "artifacts/planning_unblock_tasks.json" } ,
376+ ] ,
377+ } ,
323378 } ) ,
324379 ) ;
380+ vi . mocked ( fetchArtifact )
381+ . mockResolvedValueOnce ( {
382+ data : [
383+ {
384+ prompt_contract_id : "worker-1" ,
385+ continuation_policy : {
386+ on_incomplete : "reply_auditor_reprompt_and_continue_same_session" ,
387+ on_blocked : "spawn_independent_temporary_unblock_task" ,
388+ } ,
389+ done_definition : { acceptance_checks : [ "repo_hygiene" , "test_report" ] } ,
390+ } ,
391+ ] ,
392+ } as any )
393+ . mockResolvedValueOnce ( {
394+ data : [
395+ {
396+ unblock_task_id : "unblock-worker-1" ,
397+ owner : "L0" ,
398+ mode : "independent_temporary_task" ,
399+ trigger : "spawn_independent_temporary_unblock_task" ,
400+ } ,
401+ ] ,
402+ } as any ) ;
325403
326404 render ( < RunDetailPage runId = "run-binding" onBack = { vi . fn ( ) } /> ) ;
327405
@@ -338,6 +416,13 @@ describe("RunDetailPage p0 controls", () => {
338416 "Read-only note: this mirrors the persisted binding summary. task_contract still owns execution authority." ,
339417 ) ,
340418 ) . toBeInTheDocument ( ) ;
419+ expect ( screen . getByText ( "Completion governance" ) ) . toBeInTheDocument ( ) ;
420+ expect ( screen . getByText ( "Worker prompt contracts" ) ) . toBeInTheDocument ( ) ;
421+ expect ( screen . getByText ( "On incomplete" ) ) . toBeInTheDocument ( ) ;
422+ expect ( screen . getByText ( "On blocked" ) ) . toBeInTheDocument ( ) ;
423+ expect ( screen . getByText ( "Unblock tasks" ) ) . toBeInTheDocument ( ) ;
424+ expect ( screen . getByText ( "Unblock owner" ) ) . toBeInTheDocument ( ) ;
425+ expect ( screen . getByText ( "L0" ) ) . toBeInTheDocument ( ) ;
341426 } ) ;
342427
343428 it ( "recovers from error state after retry load" , async ( ) => {
@@ -703,4 +788,114 @@ describe("RunDetailPage p0 controls", () => {
703788 expect ( screen . getByText ( "new diff payload" ) ) . toBeInTheDocument ( ) ;
704789 expect ( screen . queryByText ( "old diff payload" ) ) . toBeNull ( ) ;
705790 } ) ;
791+
792+ it ( "ignores stale artifact summaries when runId switches during artifact loads" , async ( ) => {
793+ const oldPlanning = createDeferred < { data : Array < Record < string , unknown > > } > ( ) ;
794+
795+ vi . mocked ( fetchRun )
796+ . mockResolvedValueOnce (
797+ makeRun ( {
798+ run_id : "run-old" ,
799+ task_id : "task-old" ,
800+ manifest : {
801+ artifacts : [
802+ { name : "planning_worker_prompt_contracts" , path : "artifacts/planning_worker_prompt_contracts.json" } ,
803+ { name : "planning_unblock_tasks" , path : "artifacts/planning_unblock_tasks.json" } ,
804+ ] ,
805+ } ,
806+ } ) ,
807+ )
808+ . mockResolvedValueOnce (
809+ makeRun ( {
810+ run_id : "run-new" ,
811+ task_id : "task-new" ,
812+ manifest : {
813+ artifacts : [
814+ { name : "planning_worker_prompt_contracts" , path : "artifacts/planning_worker_prompt_contracts.json" } ,
815+ { name : "planning_unblock_tasks" , path : "artifacts/planning_unblock_tasks.json" } ,
816+ ] ,
817+ } ,
818+ } ) ,
819+ ) ;
820+ vi . mocked ( fetchEvents )
821+ . mockResolvedValueOnce ( [ { ts : "2026-02-19T00:00:01Z" , event : "OLD_EVENT" , level : "INFO" } ] as any )
822+ . mockResolvedValueOnce ( [ { ts : "2026-02-19T00:00:02Z" , event : "NEW_EVENT" , level : "INFO" } ] as any ) ;
823+ vi . mocked ( fetchArtifact ) . mockImplementation ( ( requestedRunId , artifactName ) => {
824+ if ( requestedRunId === "run-old" && artifactName === "planning_worker_prompt_contracts.json" ) {
825+ return oldPlanning . promise as Promise < any > ;
826+ }
827+ if ( requestedRunId === "run-old" && artifactName === "planning_unblock_tasks.json" ) {
828+ return Promise . resolve ( {
829+ data : [
830+ {
831+ unblock_task_id : "unblock-old" ,
832+ owner : "old-owner" ,
833+ mode : "old-mode" ,
834+ trigger : "old-trigger" ,
835+ } ,
836+ ] ,
837+ } as any ) ;
838+ }
839+ if ( requestedRunId === "run-new" && artifactName === "planning_worker_prompt_contracts.json" ) {
840+ return Promise . resolve ( {
841+ data : [
842+ {
843+ prompt_contract_id : "worker-new" ,
844+ continuation_policy : {
845+ on_incomplete : "new-incomplete" ,
846+ on_blocked : "new-blocked" ,
847+ } ,
848+ done_definition : { acceptance_checks : [ "new-check" ] } ,
849+ } ,
850+ ] ,
851+ } as any ) ;
852+ }
853+ if ( requestedRunId === "run-new" && artifactName === "planning_unblock_tasks.json" ) {
854+ return Promise . resolve ( {
855+ data : [
856+ {
857+ unblock_task_id : "unblock-new" ,
858+ owner : "new-owner" ,
859+ mode : "new-mode" ,
860+ trigger : "new-trigger" ,
861+ } ,
862+ ] ,
863+ } as any ) ;
864+ }
865+ return Promise . resolve ( { data : [ ] } as any ) ;
866+ } ) ;
867+
868+ const { rerender } = render ( < RunDetailPage runId = "run-old" onBack = { vi . fn ( ) } /> ) ;
869+ await waitFor ( ( ) => {
870+ expect ( fetchArtifact ) . toHaveBeenCalledWith ( "run-old" , "planning_worker_prompt_contracts.json" ) ;
871+ } ) ;
872+
873+ rerender ( < RunDetailPage runId = "run-new" onBack = { vi . fn ( ) } /> ) ;
874+ expect ( await screen . findByRole ( "heading" , { name : "run-new" } ) ) . toBeInTheDocument ( ) ;
875+ await waitFor ( ( ) => {
876+ expect ( screen . getByText ( "new-owner" ) ) . toBeInTheDocument ( ) ;
877+ expect ( screen . getByText ( "new-incomplete" ) ) . toBeInTheDocument ( ) ;
878+ } ) ;
879+
880+ await act ( async ( ) => {
881+ oldPlanning . resolve ( {
882+ data : [
883+ {
884+ prompt_contract_id : "worker-old" ,
885+ continuation_policy : {
886+ on_incomplete : "old-incomplete" ,
887+ on_blocked : "old-blocked" ,
888+ } ,
889+ done_definition : { acceptance_checks : [ "old-check" ] } ,
890+ } ,
891+ ] ,
892+ } ) ;
893+ await Promise . resolve ( ) ;
894+ } ) ;
895+
896+ expect ( screen . getByText ( "new-owner" ) ) . toBeInTheDocument ( ) ;
897+ expect ( screen . getByText ( "new-incomplete" ) ) . toBeInTheDocument ( ) ;
898+ expect ( screen . queryByText ( "old-incomplete" ) ) . toBeNull ( ) ;
899+ expect ( screen . queryByText ( "old-owner" ) ) . toBeNull ( ) ;
900+ } ) ;
706901} ) ;
0 commit comments