feat(investigation): preserve cumulative reference graphs

Accumulate investigation paths and preserve failed attempts across rounds. Keep graph annotations separate from execution state, synchronize the API contract, and consolidate documentation.
This commit is contained in:
2026-11-11 19:10:41 +08:00
parent e47c61c121
commit b853b61a0b
38 changed files with 2445 additions and 147 deletions
+992
View File
@@ -0,0 +1,992 @@
[
{
"name": "zero and sparse historical data",
"accepted": true,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "old mixed charts are read only",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "line",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "missing history timestamps",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
]
}
]
}
},
{
"name": "missing observed timestamp",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
null,
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "timestamp on a missing value",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
"2026-07-09T08:00:00+08:00",
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "wrong projected slot",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:30:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "mixed historical dates",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-10T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "same day history",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
}
]
}
},
{
"name": "future history",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-17T08:15:00+08:00",
"2026-07-17T08:30:00+08:00"
]
}
]
}
},
{
"name": "duplicate slots",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:00",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "date appended axis",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"07-16 08:00",
"07-16 08:15",
"07-16 08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "inconsistent slot precision",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "bars mixed with comparison",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "bar",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "no historical series",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
}
]
}
},
{
"name": "no current observations",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
null,
null,
null
],
"sample_times": [
null,
null,
null
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "missing samples with missing timestamps",
"accepted": true,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
null,
28,
30
],
"sample_times": [
null,
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "UTC spelling preserves projected instant",
"accepted": true,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T00:15:00Z",
"2026-07-09T00:30:00Z"
]
}
]
}
},
{
"name": "ordinary line",
"accepted": true,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "line",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "合成读数",
"data": [
0,
null,
30
]
}
]
}
},
{
"name": "ordinary bar",
"accepted": true,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "bar",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "合成读数",
"data": [
0,
null,
30
]
}
]
}
},
{
"name": "midnight rollover keeps one window",
"accepted": true,
"chart": {
"title": "跨午夜同期对比",
"chart_type": "time_comparison",
"x_data": [
"23:45",
"00:00"
],
"series": [
{
"name": "当前",
"type": "line",
"data": [
0,
25
],
"sample_times": [
"2026-07-16T23:45:00+08:00",
"2026-07-17T00:00:00+08:00"
]
},
{
"name": "历史",
"type": "scatter",
"data": [
26,
27
],
"sample_times": [
"2026-07-15T23:45:00+08:00",
"2026-07-16T00:00:00+08:00"
]
}
]
}
},
{
"name": "microseconds are not aligned",
"accepted": false,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00.000001+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "fraction beyond Python microsecond precision",
"accepted": true,
"chart": {
"title": "P1 合成压力七日对照(m)",
"chart_type": "time_comparison",
"x_axis_name": "时间",
"y_axis_name": "压力(m)",
"x_data": [
"08:00",
"08:15",
"08:30"
],
"series": [
{
"name": "P1 当前 2026-07-16",
"type": "line",
"data": [
0,
28,
30
],
"sample_times": [
"2026-07-16T08:00:00.0000001+08:00",
"2026-07-16T08:15:00+08:00",
"2026-07-16T08:30:00+08:00"
]
},
{
"name": "P1 2026-07-09",
"type": "scatter",
"data": [
null,
32,
33
],
"sample_times": [
null,
"2026-07-09T08:15:00+08:00",
"2026-07-09T08:30:00+08:00"
]
}
]
}
},
{
"name": "calendar overflow",
"accepted": false,
"chart": {
"title": "calendar overflow",
"chart_type": "time_comparison",
"x_data": [
"07:45"
],
"series": [
{
"name": "当前",
"type": "line",
"data": [
1
],
"sample_times": [
"9999-12-31T23:45:00Z"
]
},
{
"name": "历史缺测",
"type": "scatter",
"data": [
null
],
"sample_times": [
null
]
}
]
}
},
{
"name": "calendar underflow",
"accepted": false,
"chart": {
"title": "calendar underflow",
"chart_type": "time_comparison",
"x_data": [
"08:00"
],
"series": [
{
"name": "当前",
"type": "line",
"data": [
1
],
"sample_times": [
"0001-01-01T00:00:00+23:59"
]
},
{
"name": "历史缺测",
"type": "scatter",
"data": [
null
],
"sample_times": [
null
]
}
]
}
}
]
+14
View File
@@ -0,0 +1,14 @@
{"record_type":"presentation","command":{"action":"locate_features","layer":"scada","feature_ids":["P1"]}}
{"record_type":"presentation","command":{"action":"zoom_to_map","center":[121.48,31.1],"zoom":14}}
{"record_type":"presentation","command":{"action":"highlight_features","layer":"scada","feature_ids":["P1"],"effect":"pulse"}}
{"record_type":"presentation","command":{"action":"open_chart","chart_id":"fixture-pressure-P1","evidence_refs":["dev:synthetic"],"chart":{"title":"P1 合成压力七日对照(m)","chart_type":"time_comparison","x_axis_name":"时间","y_axis_name":"压力(m)","x_data":["08:00","08:15","08:30"],"series":[{"name":"P1 当前 2026-07-16","type":"line","data":[32,28,30],"sample_times":["2026-07-16T08:00:00+08:00","2026-07-16T08:15:00+08:00","2026-07-16T08:30:00+08:00"]},{"name":"P1 2026-07-09","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-09T08:15:00+08:00","2026-07-09T08:30:00+08:00"]},{"name":"P1 2026-07-10","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-10T08:15:00+08:00","2026-07-10T08:30:00+08:00"]},{"name":"P1 2026-07-11","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-11T08:15:00+08:00","2026-07-11T08:30:00+08:00"]},{"name":"P1 2026-07-12","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-12T08:15:00+08:00","2026-07-12T08:30:00+08:00"]},{"name":"P1 2026-07-13","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-13T08:15:00+08:00","2026-07-13T08:30:00+08:00"]},{"name":"P1 2026-07-14","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-14T08:15:00+08:00","2026-07-14T08:30:00+08:00"]},{"name":"P1 2026-07-15","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-15T08:15:00+08:00","2026-07-15T08:30:00+08:00"]}]}}}
{"record_type":"presentation","command":{"action":"open_chart","chart_id":"fixture-pressure-P2","evidence_refs":["dev:synthetic"],"chart":{"title":"P2 合成压力七日对照(m)","chart_type":"time_comparison","x_axis_name":"时间","y_axis_name":"压力(m)","x_data":["08:00","08:15","08:30"],"series":[{"name":"P2 当前 2026-07-16","type":"line","data":[31,27,29],"sample_times":["2026-07-16T08:00:00+08:00","2026-07-16T08:15:00+08:00","2026-07-16T08:30:00+08:00"]},{"name":"P2 2026-07-09","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-09T08:15:00+08:00","2026-07-09T08:30:00+08:00"]},{"name":"P2 2026-07-10","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-10T08:15:00+08:00","2026-07-10T08:30:00+08:00"]},{"name":"P2 2026-07-11","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-11T08:15:00+08:00","2026-07-11T08:30:00+08:00"]},{"name":"P2 2026-07-12","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-12T08:15:00+08:00","2026-07-12T08:30:00+08:00"]},{"name":"P2 2026-07-13","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-13T08:15:00+08:00","2026-07-13T08:30:00+08:00"]},{"name":"P2 2026-07-14","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-14T08:15:00+08:00","2026-07-14T08:30:00+08:00"]},{"name":"P2 2026-07-15","type":"scatter","data":[null,32,33],"sample_times":[null,"2026-07-15T08:15:00+08:00","2026-07-15T08:30:00+08:00"]}]}}}
{"record_type":"presentation","command":{"action":"set_overlay","overlay_id":"fixture-area","title":"合成空间样例","mode":"geometry","evidence_id":"dev:synthetic","data":{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"origin":"synthetic"},"geometry":{"type":"Point","coordinates":[121.48,31.1]}}]}}}
{"record_type":"presentation","command":{"action":"render_scada_analysis","items":[{"device_id":"P1","level":"high"}]}}
{"record_type":"ui_block","evidence_refs":[],"block":{"version":"answer-ui@1","block_id":"fixture-summary","phase":"final","fallback_text":"合成候选对比,仅供展示测试。","spec":{"root":"table","elements":{"table":{"type":"Table","props":{"title":"合成候选对比","columns":["候选","说明"],"rows":[["DEMO-PIPE-01","仅供展示测试"],["DEMO-PIPE-02","未执行定位分析"]]},"children":[]}}}}}
{"record_type":"candidate","origin":"synthetic","source_evidence_refs":[],"element_id":"DEMO-PIPE-01","rank":1,"status":"example_only"}
{"record_type":"presentation","command":{"action":"close_chart","chart_id":"fixture-pressure-P1"}}
{"record_type":"presentation","command":{"action":"close_chart","chart_id":"fixture-pressure-P2"}}
{"record_type":"presentation","command":{"action":"clear_overlay","overlay_id":"fixture-area"}}
{"record_type":"presentation","command":{"action":"clear_highlights"}}
{"record_type":"presentation","command":{"action":"clear_scada_analysis"}}
@@ -0,0 +1,84 @@
"""Opt-in read-only JSONB probes; no tables or business records are written."""
import asyncio
import os
from copy import deepcopy
import pytest
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from app.infra.db.postgresql import read_queries
@pytest.mark.parametrize("section", ["progress", "report"])
def test_reference_annotations_are_normalized_before_nested_paging(section):
dsn = os.environ.get("MCP_QUERY_TEST_DSN")
if not dsn:
pytest.skip("MCP_QUERY_TEST_DSN is required for a read-only PostgreSQL JSONB probe")
async def check():
statuses = ["running", "completed", "waiting", "waiting_human", "skipped", "cancelled", "failed", None]
original = {"section": section, "data": {
"revision": 3, "execution_generation": 2,
section: {"diagram": {"mermaid": "flowchart TD\nA[原图]", "node_refs": [
{"node_id": f"n{index}", "status": status, "detail": "保留原说明", "evidence_refs": ["e1"]}
for index, status in enumerate(statuses)
] + [{"node_id": "unset"}]}},
"status": "running", "action": {"status": "completed"},
}}
untouched = deepcopy(original)
async with await AsyncConnection.connect(
dsn, row_factory=dict_row, options="-c default_transaction_read_only=on",
) as conn:
async def page(path=(), *, limit=50, offset=0, full=False):
statement, params = read_queries._reference_diagram_projection(
"SELECT %s::jsonb AS snapshot", (Jsonb(original),), section,
)
return await read_queries._snapshot_page(
conn, stored_sql=statement, stored_params=params, data_path=list(path),
limit=limit, offset=offset, mcp_sources=[], full_snapshot=full, max_bytes=8000,
)
scalar = await page([section, "diagram", "node_refs", "0", "status"])
assert scalar["value"] == "reference"
second = await page([section, "diagram", "node_refs"], limit=1, offset=1)
assert second["value"][0]["status"] == "recorded"
assert second["total"] == 9 and second["nested"]
assert second["value"][0]["evidence_refs"]["data_path"] == [section, "diagram", "node_refs", "1", "evidence_refs"]
snapshot = await page(full=True)
nodes = snapshot["data"][section]["diagram"]["node_refs"]
assert [node.get("status") for node in nodes] == [
"reference", "recorded", "needs_input", "needs_input", "not_taken", "not_taken", "failed", None, None,
]
assert "status" not in nodes[-1]
assert nodes[6]["detail"] == "保留原说明" and nodes[6]["evidence_refs"] == ["e1"]
assert snapshot["data"]["status"] == "running"
assert snapshot["data"]["action"]["status"] == "completed"
assert snapshot["data"][section]["diagram"]["mermaid"] == original["data"][section]["diagram"]["mermaid"]
assert original == untouched
asyncio.run(check())
@pytest.mark.parametrize("diagram", [None, {}, {"node_refs": []}])
def test_reference_projection_preserves_empty_and_missing_legacy_diagrams(diagram):
dsn = os.environ.get("MCP_QUERY_TEST_DSN")
if not dsn:
pytest.skip("MCP_QUERY_TEST_DSN is required for a read-only PostgreSQL JSONB probe")
async def check():
original = {"section": "progress", "data": {"progress": {"diagram": diagram}, "status": "running"}}
async with await AsyncConnection.connect(
dsn, row_factory=dict_row, options="-c default_transaction_read_only=on",
) as conn:
statement, params = read_queries._reference_diagram_projection(
"SELECT %s::jsonb AS snapshot", (Jsonb(original),), "progress",
)
result = await read_queries._snapshot_page(
conn, stored_sql=statement, stored_params=params, data_path=[],
limit=20, offset=0, mcp_sources=[], full_snapshot=True,
)
assert result == original
asyncio.run(check())
@@ -104,6 +104,19 @@ class TaskRepository:
async def get_latest_progress(self, task_id, generation):
return self.progress.get((task_id, generation))
async def get_latest_graph_progress(self, task_id):
snapshots = [value for (stored_id, _), value in self.progress.items()
if stored_id == task_id
and (value["progress"].get("diagram") or {}).get("edges") is not None]
return max(snapshots, key=lambda row: (row["execution_generation"], row["revision"])) if snapshots else None
async def get_latest_diagram(self, task_id):
diagrams = [(row["published_at"], row["progress"].get("diagram"))
for (stored_id, _), row in self.progress.items() if stored_id == task_id]
diagrams.extend((row["published_at"], row["report"].get("diagram")) for row in self.reports)
return next((diagram for _, diagram in sorted(diagrams, key=lambda item: item[0], reverse=True)
if diagram is not None), None)
async def save_progress(self, task_id, snapshot):
self.progress[(task_id, snapshot["execution_generation"])] = snapshot
@@ -0,0 +1,48 @@
"""Generated fixtures keep executable examples and publication semantics aligned across repositories."""
import json
from pathlib import Path
import pytest
from pydantic import ValidationError
from app.domain.domain_errors import DomainError
from app.domain.schemas.operational_task import TaskPresentationEventCommand, TaskSafeChart
from app.services.time_comparison_chart import validate_time_comparison_chart
FIXTURES = Path(__file__).parents[1] / "fixtures"
CASES = json.loads((FIXTURES / "chart-publication-cases.json").read_text())
@pytest.mark.parametrize("case", CASES, ids=lambda case: case["name"])
def test_chart_publication_matches_frontend_and_agent(case):
def validate():
chart = TaskSafeChart.model_validate(case["chart"]).model_dump(mode="json")
validate_time_comparison_chart(chart)
if case["accepted"]:
validate()
else:
with pytest.raises((DomainError, ValidationError)):
validate()
def test_historical_mixed_charts_remain_structurally_readable():
chart = next(case["chart"] for case in CASES if case["name"] == "old mixed charts are read only")
TaskSafeChart.model_validate(chart)
def test_exported_command_examples_use_the_current_publication_contract():
for line in (FIXTURES / "unified-presentation.jsonl").read_text().splitlines():
record = json.loads(line)
if record["record_type"] != "presentation":
continue
event = TaskPresentationEventCommand.model_validate({
"request_id": "95000000-0000-4000-8000-000000000001",
"run_id": "95000000-0000-4000-8000-000000000002",
"source_session_id": "synthetic-chart-contract",
"command": record["command"],
})
command = event.command
if command.action == "open_chart":
validate_time_comparison_chart(command.chart.model_dump(mode="json"))
@@ -28,7 +28,7 @@ def test_waiting_and_unknown_node_states_survive_progress_and_final_report():
stage="final", outcome="concluded", data_gaps=[], diagram=diagram,
)
assert report.diagram == progress.diagram
assert [node.status for node in report.diagram.node_refs] == ["completed", "waiting", None, None]
assert [node.status for node in report.diagram.node_refs] == ["recorded", "needs_input", None, None]
assert "status" not in report.diagram.node_refs[-1].model_fields_set
@@ -43,7 +43,7 @@ def test_class_attached_nodes_are_valid_declarations_without_accepting_partial_i
"node_refs": [{"node_id": "QUERY", "label": "核查", "status": "running"},
{"node_id": "REVIEW", "label": "复核", "status": "pending"}]},
}}
assert AgentArtifactsCommand.model_validate(payload).progress.diagram.node_refs[0].status == "running"
assert AgentArtifactsCommand.model_validate(payload).progress.diagram.node_refs[0].status == "reference"
payload["progress"]["diagram"]["node_refs"][0]["node_id"] = "QUER"
with pytest.raises(ValidationError, match="QUER"):
AgentArtifactsCommand.model_validate(payload)
+315
View File
@@ -0,0 +1,315 @@
from datetime import datetime, timedelta, timezone
from uuid import uuid4
import pytest
from app.domain.domain_errors import DomainError
from app.domain.schemas.operational_task import AgentArtifactsCommand
from app.domain.schemas.operational_task import InvestigationDiagram, InvestigationGraphUpdate
from app.domain.schemas.operational_data import TaskDataSnapshot
from app.services.investigation_graph import update_investigation_graph
from app.services.operational_tasks import OperationalTaskService
from tests.support.operational_task_repository import (
TaskRepository, _active_run, _context, _task_document,
)
@pytest.fixture
def anyio_backend():
return "asyncio"
def setup_service():
now = datetime(2026, 11, 10, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repo = TaskRepository(task, _active_run(task, now))
return task, repo, OperationalTaskService(repo, context, set(), clock=lambda: now)
async def publish(service, repo, task_id, *, nodes=(), edges=(), **progress):
command = AgentArtifactsCommand.model_validate({
"request_id": str(uuid4()), "source_session_id": repo.run["agent_session_id"],
"run_id": repo.run["run_id"],
"progress": {"summary": "更新调查路线", "graph_update": {
"nodes": list(nodes), "edges": list(edges),
}, **progress},
})
return await service.publish_artifacts(task_id, command)
@pytest.mark.anyio
async def test_failed_path_is_retained_when_a_new_route_is_added():
task, repo, service = setup_service()
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "read", "label": "读取全量 SCADA", "status": "recorded"},
{"node_id": "locate_1", "label": "第一次候选定位", "status": "reference"},
], edges=[{"source": "read", "target": "locate_1"}])
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "locate_1", "label": "第一次候选定位", "status": "failed", "detail": "计算超时"},
{"node_id": "locate_2", "label": "调整窗口后重试", "status": "reference"},
], edges=[{"source": "locate_1", "target": "locate_2", "label": "调整后重试"}])
result = await publish(service, repo, task["task_id"], nodes=[
{"node_id": "locate_2", "label": "调整窗口后重试", "status": "recorded"},
])
diagram = result.task.latest_progress.progress.diagram
assert [(n.node_id, n.status) for n in diagram.node_refs] == [
("read", "recorded"), ("locate_1", "failed"), ("locate_2", "recorded"),
]
assert diagram.node_refs[1].detail == "计算超时"
assert len(diagram.edges) == 2
assert "第一次候选定位(失败)" in diagram.mermaid
assert 'locate_1 -->|"调整后重试"| locate_2' in diagram.mermaid
# A text-only update cannot clear the accumulated graph.
summary = AgentArtifactsCommand.model_validate({
"request_id": str(uuid4()), "source_session_id": repo.run["agent_session_id"],
"run_id": repo.run["run_id"], "progress": {"summary": "等待人工", "diagram": None},
})
assert (await service.publish_artifacts(task["task_id"], summary)).task.latest_progress.progress.diagram == diagram
@pytest.mark.anyio
async def test_failed_attempt_cannot_be_relabelled_as_a_success():
task, repo, service = setup_service()
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "attempt_1", "label": "候选定位", "status": "failed", "detail": "超时"},
])
with pytest.raises(DomainError, match="new node"):
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "attempt_1", "label": "候选定位", "status": "recorded"},
])
@pytest.mark.anyio
async def test_new_execution_round_continues_the_same_task_graph():
task, repo, service = setup_service()
first = await publish(service, repo, task["task_id"], nodes=[
{"node_id": "first", "label": "首次查询", "status": "failed"},
])
archived = first.task.latest_progress.progress.diagram.model_dump(mode="json")
repo.task["execution_generation"] = 2
repo.task["execution_request_id"] = str(uuid4())
repo.run["status"] = "succeeded"
repo.run = _active_run(repo.task, datetime(2026, 11, 10, tzinfo=timezone.utc))
repo.runs[repo.run["run_id"]] = repo.run
assert (await service.detail(task["task_id"])).latest_progress.progress.diagram.node_refs[0].status == "failed"
result = await publish(service, repo, task["task_id"], nodes=[
{"node_id": "second", "label": "补充信息后查询", "status": "recorded"},
], edges=[{"source": "first", "target": "second"}])
assert result.progress_revision == 1
assert result.task.latest_progress.execution_generation == 2
assert len(result.task.latest_progress.progress.diagram.node_refs) == 2
assert repo.progress[(task["task_id"], 1)]["progress"]["diagram"] == archived
@pytest.mark.anyio
async def test_reports_archive_the_host_graph_and_keep_older_versions_unchanged():
task, repo, service = setup_service()
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "failed", "label": "数据读取", "status": "failed", "detail": "接口超时"},
])
command = AgentArtifactsCommand.model_validate({
"request_id": str(uuid4()), "source_session_id": repo.run["agent_session_id"],
"run_id": repo.run["run_id"], "report": {
"title": "调查", "current_conclusion": "已有阶段判断", "conclusion_boundary": "限于现有资料",
"stage": "final", "outcome": "concluded", "data_gaps": [],
"diagram": {"title": "重新生成的图", "mermaid": "flowchart TD\nNEW[新图]"},
},
})
first = await service.publish_artifacts(task["task_id"], command)
assert first.task.latest_report.report.diagram.node_refs[0].status == "failed"
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "retry", "label": "重新读取", "status": "recorded"},
], edges=[{"source": "failed", "target": "retry"}])
assert len((await service.report(task["task_id"], 1)).report.diagram.node_refs) == 1
assert len((await service.detail(task["task_id"])).latest_progress.progress.diagram.node_refs) == 2
# Idempotent replays do not append nodes or change the archived report.
assert (await service.publish_artifacts(task["task_id"], command)).report_revision == 1
@pytest.mark.anyio
async def test_graph_updates_validate_references_and_cannot_replace_a_managed_graph():
task, repo, service = setup_service()
with pytest.raises(DomainError, match="unknown evidence"):
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "unknown", "label": "未知依据", "evidence_refs": ["forged"]},
])
await publish(service, repo, task["task_id"], nodes=[
{"node_id": "kept", "label": "实际步骤", "status": "recorded"},
])
with pytest.raises(DomainError, match="graph_update"):
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand.model_validate({
"request_id": str(uuid4()), "source_session_id": repo.run["agent_session_id"],
"run_id": repo.run["run_id"], "progress": {
"summary": "重画", "diagram": {"title": "替换", "mermaid": "flowchart TD\nNEW[新图]"},
},
}))
def test_graph_renderer_escapes_labels_and_keeps_explicit_parallel_and_failed_paths():
graph = update_investigation_graph(None, InvestigationGraphUpdate.model_validate({
"nodes": [
{"node_id": "start", "label": '核查 "地点" <script> ``` %%{init: {}}'},
{"node_id": "left", "label": "监测", "status": "failed"},
{"node_id": "right", "label": "工单", "status": "reference"},
], "edges": [{"source": "start", "target": target} for target in ["left", "right"]],
}))
assert "<script>" not in graph.mermaid
assert "#lt;script#gt;" in graph.mermaid
assert "#quot;地点#quot;" in graph.mermaid
assert "```" not in graph.mermaid and "%%{" not in graph.mermaid
assert "监测(失败)" in graph.mermaid
updated = update_investigation_graph(graph, InvestigationGraphUpdate.model_validate({
"nodes": [{"node_id": "right", "label": "工单", "status": "recorded"}],
"edges": [{"source": "start", "target": "right"}],
}))
assert len(updated.edges) == 2
assert graph.node_refs[2].status == "reference"
def test_first_structured_update_preserves_an_existing_mermaid_without_reinterpreting_it():
original = InvestigationDiagram.model_validate({
"title": "原调查路线", "mermaid": 'flowchart LR\nA{核查条件} -->|具备| B[原路径]\nA --> C[另一分支]',
"node_refs": [{"node_id": "B", "label": "原路径", "status": "failed", "detail": "查询超时"}],
})
updated = update_investigation_graph(original, InvestigationGraphUpdate.model_validate({
"nodes": [{"node_id": "retry", "label": "改用其他数据", "status": "reference"}],
"edges": [{"source": "B", "target": "retry"}],
}))
assert updated.mermaid.startswith(original.mermaid + "\n")
assert updated.source_mermaid == original.mermaid
assert updated.node_refs[0].status == "failed"
final = update_investigation_graph(updated, InvestigationGraphUpdate.model_validate({
"nodes": [{"node_id": "retry", "label": "改用其他数据", "status": "recorded"}],
}))
assert final.mermaid.count("A{核查条件}") == 1
assert "B --> retry" in final.mermaid
with pytest.raises(DomainError, match="original Mermaid"):
update_investigation_graph(updated, InvestigationGraphUpdate.model_validate({
"nodes": [{"node_id": "C", "label": "与旧图冲突的新步骤"}],
}))
@pytest.mark.anyio
async def test_first_update_uses_the_last_saved_legacy_report_graph():
task, repo, service = setup_service()
repo.reports.append({
"revision": 1, "execution_generation": 1, "source_session_id": "session-1",
"published_at": "2026-11-09T00:00:00Z", "published_by": "agent", "report": {
"title": "旧报告", "current_conclusion": "已有判断", "conclusion_boundary": "已有证据",
"diagram": {"title": "旧路线", "mermaid": "flowchart TD\nold[此前查询]",
"node_refs": [{"node_id": "old", "label": "此前查询", "status": "failed"}]},
},
})
result = await publish(service, repo, task["task_id"], nodes=[
{"node_id": "retry", "label": "接续查询", "status": "reference"},
], edges=[{"source": "old", "target": "retry"}])
diagram = result.task.latest_progress.progress.diagram
assert diagram.source_mermaid == "flowchart TD\nold[此前查询]"
assert [n.node_id for n in diagram.node_refs] == ["old", "retry"]
@pytest.mark.parametrize("update,code", [
({"nodes": []}, "GRAPH_NODE_REQUIRED"),
({"nodes": [{"node_id": "end", "label": "保留字"}]}, "GRAPH_NODE_ID_INVALID"),
({"nodes": [{"node_id": "n", "label": "一"}, {"node_id": "n", "label": "二"}]}, "GRAPH_NODE_DUPLICATE"),
({"nodes": [{"node_id": "n", "label": "一"}], "edges": [{"source": "n", "target": "missing"}]}, "GRAPH_EDGE_NODE_NOT_FOUND"),
])
def test_graph_updates_reject_invalid_structure_without_erasing_history(update, code):
with pytest.raises(DomainError) as error:
update_investigation_graph(None, InvestigationGraphUpdate.model_validate(update))
assert error.value.code == code
@pytest.mark.anyio
@pytest.mark.parametrize("node_refs", [[], [{"node_id": "old", "label": "旧尝试", "status": "failed", "detail": "查询超时"}]])
async def test_summary_only_adopts_legacy_graph_before_the_first_incremental_update(node_refs):
task, repo, service = setup_service()
original = {"title": "旧路线", "mermaid": "flowchart LR\nold{旧尝试} --> other[另一分支]", "node_refs": node_refs}
for progress in [{"summary": "此前调查", "diagram": original}, {"summary": "继续调查"}]:
result = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", run_id=repo.run["run_id"], progress=progress,
))
diagram = result.task.latest_progress.progress.diagram
assert diagram is not None
assert diagram.mermaid == diagram.source_mermaid == original["mermaid"]
assert diagram.edges == []
assert diagram.node_refs == InvestigationDiagram.model_validate(original).node_refs
@pytest.mark.anyio
async def test_identical_effective_reports_do_not_repeat_notifications_for_inherited_graph_evidence():
task, repo, service = setup_service()
evidence, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot(
source="query_scada_readings", source_ref="probe", queried_at=service._now(),
origin="system_data", truncated=False, data={"data": []},
))
await publish(service, repo, task["task_id"], nodes=[{
"node_id": "finding", "label": "已有结果", "status": "recorded", "evidence_refs": [evidence.evidence_id],
}])
saved = []
for _ in range(2):
result = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", report={
"title": "调查", "current_conclusion": "已有判断", "conclusion_boundary": "当前资料",
"stage": "final", "outcome": "concluded", "data_gaps": [],
},
))
saved.append(result.task.latest_report.report)
assert saved[0] == saved[1]
assert saved[0].evidence_refs == [evidence.evidence_id]
assert len([notice for notice in repo.notifications if notice["kind"] == "report_updated"]) == 1
@pytest.mark.anyio
async def test_final_scada_report_validates_the_effective_inherited_evidence():
task, repo, service = setup_service()
task["workflow"] = "scada_anomaly"
now = service._now()
evidence, _ = await service.record_data_snapshot(task["task_id"], TaskDataSnapshot(
source="query_scada_readings", source_ref="complete-scada", queried_at=now,
origin="system_data", truncated=False, data={
"data": {"coverage": {"mode": "all_scada", "query_complete": True}},
"window": {"start": (now - timedelta(hours=1)).isoformat(), "end": now.isoformat()},
},
))
await publish(service, repo, task["task_id"], nodes=[{
"node_id": "all_scada", "label": "全量读取", "evidence_refs": [evidence.evidence_id],
}])
result = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", report={
"title": "调查", "current_conclusion": "已有判断", "conclusion_boundary": "当前资料",
"stage": "final", "outcome": "concluded", "data_gaps": [],
},
))
assert result.task.latest_report.report.evidence_refs == [evidence.evidence_id]
@pytest.mark.anyio
async def test_evidence_only_publication_does_not_load_the_graph_baseline(monkeypatch):
task, repo, service = setup_service()
calls = []
original = repo.get_latest_graph_progress
async def load_graph(task_id):
calls.append(task_id)
return await original(task_id)
# The returned task detail can read progress; count separately from publication.
original_detail = service.detail
async def detail(task_id):
before_detail = len(calls)
result = await original_detail(task_id)
del calls[before_detail:]
return result
monkeypatch.setattr(repo, "get_latest_graph_progress", load_graph)
monkeypatch.setattr(service, "detail", detail)
await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", evidence=[{
"evidence_id": "demo", "kind": "synthetic", "summary": "演练依据",
"source_label": "fixture", "observed_at": service._now(),
}],
))
assert calls == []
@@ -0,0 +1,45 @@
from copy import deepcopy
from datetime import datetime, timezone
import pytest
from scripts.migrate_investigation_graphs import prepare_progress_document
from tests.support.operational_task_repository import _active_run, _context, _task_document
@pytest.mark.parametrize("node_refs", [[], [
{"node_id": "old", "label": "旧尝试", "status": "failed", "detail": "原查询超时", "evidence_refs": ["e1"]},
{"node_id": "next", "label": "后续核查", "status": "running"},
]])
def test_migration_keeps_source_paths_failures_and_original_publication_provenance(node_refs):
now = datetime(2026, 11, 10, tzinfo=timezone.utc)
task = _task_document(_context(), now)
run = _active_run(task, now)
document = {
"revision": 7, "run_id": run["run_id"], "execution_generation": 1,
"source_session_id": "session-1", "published_at": now.isoformat(),
"progress": {"summary": "原调查说明", "diagram": {"title": "旧图", "node_refs": node_refs,
"mermaid": "flowchart LR\nold{旧尝试} -->|失败后重试| next[后续核查]"}},
}
original = deepcopy(document)
migrated = prepare_progress_document(document)
assert document == original
assert migrated["revision"] == 8
assert {key: migrated[key] for key in original if key not in {"revision", "progress"}} == {
key: original[key] for key in original if key not in {"revision", "progress"}
}
assert migrated["progress"]["summary"] == original["progress"]["summary"]
graph = migrated["progress"]["diagram"]
assert graph["mermaid"] == graph["source_mermaid"] == original["progress"]["diagram"]["mermaid"]
assert graph["edges"] == []
if node_refs:
assert graph["node_refs"][0]["detail"] == "原查询超时"
assert graph["node_refs"][0]["evidence_refs"] == ["e1"]
assert [node["status"] for node in graph["node_refs"]] == ["failed", "reference"]
assert prepare_progress_document(migrated) is None
def test_migration_does_not_invent_a_graph_when_none_was_saved():
document = {"progress": {"summary": "仅说明", "diagram": None}}
assert prepare_progress_document(document) is None
assert document == {"progress": {"summary": "仅说明", "diagram": None}}
@@ -0,0 +1,27 @@
import pytest
from pydantic import ValidationError
from app.domain.schemas.operational_task import InvestigationDiagramNodeRef, InvestigationGraphUpdate
@pytest.mark.parametrize("status", ["reference", "recorded", "needs_input", "failed", "not_taken"])
def test_graph_updates_only_describe_reference_steps_and_recorded_outcomes(status):
update = InvestigationGraphUpdate.model_validate({"nodes": [{"node_id": "check", "label": "核查", "status": status}]})
assert update.nodes[0].status == status
@pytest.mark.parametrize("legacy,expected", [
("pending", "reference"), ("running", "reference"), ("completed", "recorded"),
("waiting", "needs_input"), ("waiting_human", "needs_input"),
("skipped", "not_taken"), ("cancelled", "not_taken"),
])
def test_old_diagrams_are_read_as_reference_annotations_without_changing_stored_documents(legacy, expected):
document = {"node_id": "check", "label": "核查", "status": legacy}
assert InvestigationDiagramNodeRef.model_validate(document).status == expected
assert document["status"] == legacy
@pytest.mark.parametrize("status", ["running", "pending", "completed", "waiting", "waiting_human", "skipped", "cancelled"])
def test_new_incremental_writes_reject_execution_lifecycle_states(status):
with pytest.raises(ValidationError):
InvestigationGraphUpdate.model_validate({"nodes": [{"node_id": "check", "label": "核查", "status": status}]})
+6 -5
View File
@@ -314,12 +314,13 @@ async def test_progress_can_publish_without_conclusion_and_replaces_the_complete
updated = await service.publish_artifacts(task["task_id"], replacement)
assert updated.progress_revision == 2
assert [node.node_id for node in updated.task.latest_progress.progress.diagram.node_refs] == ["other"]
cleared = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
retained = await service.publish_artifacts(task["task_id"], AgentArtifactsCommand(
request_id=uuid4(), source_session_id="session-1", run_id=repository.run["run_id"],
progress={"summary": "当前不需要图示。", "diagram": None},
progress={"summary": "补充本轮说明。", "diagram": None},
))
assert cleared.progress_revision == 3
assert cleared.task.latest_progress.progress.diagram is None
assert retained.progress_revision == 3
assert retained.task.latest_progress.progress.diagram.mermaid == updated.task.latest_progress.progress.diagram.mermaid
assert retained.task.latest_progress.progress.diagram.edges == []
assert not repository.notifications
@@ -1318,7 +1319,7 @@ async def test_final_report_archives_its_progress_diagram_across_new_rounds():
)
await service.publish_artifacts(task["task_id"], command)
assert repository.reports[-1]["report"]["diagram"]["title"] == "调查过程"
assert repository.reports[-1]["report"]["diagram"]["node_refs"][0]["status"] == "running"
assert repository.reports[-1]["report"]["diagram"]["node_refs"][0]["status"] == "reference"
assert command.report.diagram is None # Do not change the idempotency request.
repository.task["execution_generation"] = 2
detail = await service.detail(task["task_id"])