fix: resume incomplete SCADA presentations
This commit is contained in:
@@ -55,6 +55,8 @@ SCADA地图定位、等级标记、表格与图表采用同一目标设备集合
|
||||
|
||||
同图最多100横轴槽、8组序列,每组与x_data对齐。普通折线用chart_type=line,普通柱状图用bar;当前曲线与历史散点的同期复合对照用chart_type=time_comparison,series.type分别为line和scatter。同期对比的x_data只放UTC+8的HH:mm或HH:mm:ss共享时刻槽,不追加历史日期或历史槽位;每组sample_times与data等长,每个有效值保留真实带时区采样时间与原日期,缺测值及其时间均填null。当前曲线使用同一窗口,历史每组来自此前某一天的对应窗口,历史点填到同一时刻槽;跨午夜时按窗口时间顺序排列。超限按对象分图或时间分段,保留目标设备覆盖;聚合时说明方法,各单位和各类业务含义分别展示。同主题同分组更新同一chart_id,保留图表供查看;报告保留关键结论和来源。
|
||||
|
||||
SCADA监测过程图填写chart.scada={role:current或comparison,metric:pressure或flow,unit,window_start,window_end},窗口为本轮完整三小时,截止非整刻仍按真实采样网格;超过100槽时连续分段并用不同稳定chart_id,分段scada窗口保持完整三小时,Server合并采样槽校验覆盖;历史散点仅保留完整窗口末三个采样槽;低频设备无计划采样槽时完整查询对应当天及历史窗口,以截止时刻单个null保留入口;每条series显式填写device_id和day_offset(当天0、此前七天1至7)。同期图逐设备保留当天和七天共8组,证据引用覆盖全部当前与历史采样;value_field默认monitored_value。缺测需完整查询证明,再保留null,Server生成scada.gaps;仅供参考的设备仍要展示。报告scada_device_ids列出监测明细表和展示目标的完整设备集合。Server按本轮累计目标校验当前总览及同期图,SCADA_PRESENTATION_INCOMPLETE表示必须按返回设备补图,不能删除目标、关闭窗口或声明不参与结论来绕过;报告和成功结束均校验。浏览器连接及显示回执不参与这项产物校验。
|
||||
|
||||
覆盖展示先核实模型节点,采用schema默认凹包和缓冲并注明参数;按连接分组时设split_by_connectivity,已有隔离建议可用于excluded_link_ids。只有result.map_payload.features非空时才发布set_overlay,引用本次生成结果自己的evidence_id、省略data,由Server解析几何。返回的data.presentation.command可直接作为command,另生成稳定request_id。缺少部分几何时展示有效部分并说明覆盖;全部缺失时展示可用点位或记录限制。
|
||||
|
||||
对象ID、数量、图层与样式遵循schema,坐标使用受控来源。展示失败时修正调用,仍失败则记录具体限制并继续独立分析。recorded表示请求已保存,浏览器跟随、连接和暂停由前端处理,收尾依据分析结果与发布回执。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -42,6 +42,8 @@ const reportFields = {
|
||||
conclusion_boundary: text,
|
||||
evidence_refs: tool.schema.array(text).default([]),
|
||||
ui_blocks: taskReportUiBlocksSchema,
|
||||
scada_device_ids: tool.schema.array(identifier).max(1000).optional()
|
||||
.describe("SCADA devices listed in monitoring tables or used as display targets, including reference-only devices. Server checks their current and seven-day chart coverage."),
|
||||
};
|
||||
|
||||
export const taskPublicationReportSchema = tool.schema.discriminatedUnion("outcome", [
|
||||
|
||||
@@ -4032,6 +4032,23 @@
|
||||
"title": "Recommendations",
|
||||
"type": "array"
|
||||
},
|
||||
"scada_device_ids": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"maxLength": 128,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Scada Device Ids"
|
||||
},
|
||||
"stage": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -4144,6 +4161,23 @@
|
||||
"title": "Recommendations",
|
||||
"type": "array"
|
||||
},
|
||||
"scada_device_ids": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"maxLength": 128,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Scada Device Ids"
|
||||
},
|
||||
"stage": {
|
||||
"enum": [
|
||||
"interim",
|
||||
@@ -7383,6 +7417,110 @@
|
||||
"title": "RunSimulationManuallyByDateRest",
|
||||
"type": "object"
|
||||
},
|
||||
"ScadaChartContext": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gaps": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ScadaChartGap"
|
||||
},
|
||||
"maxItems": 8,
|
||||
"title": "Gaps",
|
||||
"type": "array"
|
||||
},
|
||||
"metric": {
|
||||
"enum": [
|
||||
"pressure",
|
||||
"flow"
|
||||
],
|
||||
"title": "Metric",
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"enum": [
|
||||
"current",
|
||||
"comparison"
|
||||
],
|
||||
"title": "Role",
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"maxLength": 128,
|
||||
"minLength": 1,
|
||||
"title": "Unit",
|
||||
"type": "string"
|
||||
},
|
||||
"value_field": {
|
||||
"default": "monitored_value",
|
||||
"enum": [
|
||||
"monitored_value",
|
||||
"cleaned_value"
|
||||
],
|
||||
"title": "Value Field",
|
||||
"type": "string"
|
||||
},
|
||||
"window_end": {
|
||||
"format": "date-time",
|
||||
"title": "Window End",
|
||||
"type": "string"
|
||||
},
|
||||
"window_start": {
|
||||
"format": "date-time",
|
||||
"title": "Window Start",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"metric",
|
||||
"unit",
|
||||
"window_start",
|
||||
"window_end"
|
||||
],
|
||||
"title": "ScadaChartContext",
|
||||
"type": "object"
|
||||
},
|
||||
"ScadaChartGap": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"day_offset": {
|
||||
"maximum": 7.0,
|
||||
"minimum": 0.0,
|
||||
"title": "Day Offset",
|
||||
"type": "integer"
|
||||
},
|
||||
"device_id": {
|
||||
"maxLength": 128,
|
||||
"minLength": 1,
|
||||
"title": "Device Id",
|
||||
"type": "string"
|
||||
},
|
||||
"missing_count": {
|
||||
"maximum": 100.0,
|
||||
"minimum": 1.0,
|
||||
"title": "Missing Count",
|
||||
"type": "integer"
|
||||
},
|
||||
"reason": {
|
||||
"enum": [
|
||||
"no_readings",
|
||||
"outside_frozen_scope",
|
||||
"outside_frozen_window",
|
||||
"no_scheduled_samples"
|
||||
],
|
||||
"title": "Reason",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"device_id",
|
||||
"day_offset",
|
||||
"missing_count",
|
||||
"reason"
|
||||
],
|
||||
"title": "ScadaChartGap",
|
||||
"type": "object"
|
||||
},
|
||||
"ScadaDeviceResponse": {
|
||||
"description": "Project SCADA metadata keyed by the canonical device identifier.",
|
||||
"properties": {
|
||||
@@ -10450,6 +10588,16 @@
|
||||
"title": "Chart Type",
|
||||
"type": "string"
|
||||
},
|
||||
"scada": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ScadaChartContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"series": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TaskSafeChartSeries"
|
||||
@@ -10527,6 +10675,32 @@
|
||||
"title": "Data",
|
||||
"type": "array"
|
||||
},
|
||||
"day_offset": {
|
||||
"anyOf": [
|
||||
{
|
||||
"maximum": 7.0,
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Day Offset"
|
||||
},
|
||||
"device_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"maxLength": 128,
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Device Id"
|
||||
},
|
||||
"name": {
|
||||
"maxLength": 128,
|
||||
"title": "Name",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"contracts": {
|
||||
"server": {
|
||||
"file": "server-v1.openapi.json",
|
||||
"sha256": "7ed3f6485909c3ef7fac8d2083d6931765e4d197e690c3b9914b905a8aa743b2"
|
||||
"sha256": "d96df154b86b797d0db913d1e69cf335d92d34dd119f800c9346b90c6119c9bb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export function createChartSampleTimeSchema(zod: typeof z = z) {
|
||||
|
||||
type PublicationChart = {
|
||||
chart_type: string;
|
||||
scada?: { role: "current" | "comparison"; window_start: string };
|
||||
x_data: string[];
|
||||
series: Array<{ type?: string; data: Array<number | null>; sample_times?: Array<string | null> }>;
|
||||
};
|
||||
@@ -57,8 +58,12 @@ export function chartPublicationError(chart: PublicationChart): string | null {
|
||||
if ((series.type ?? "line") === "line") for (const base of bases) current.add(base);
|
||||
else historical.push(...bases);
|
||||
}
|
||||
if (current.size !== 1) return "当前 line 必须包含真实采样时间,且所有当前曲线使用同一窗口";
|
||||
const start = [...current][0];
|
||||
const emptyScada = current.size === 0 && chart.scada?.role === "comparison";
|
||||
if (current.size !== 1 && !emptyScada) return "当前 line 必须包含真实采样时间,且所有当前曲线使用同一窗口";
|
||||
const windowStart = emptyScada ? Date.parse(chart.scada!.window_start) + 8 * 3600_000 : 0;
|
||||
const midnight = Math.floor(windowStart / 86400_000) * 86400_000;
|
||||
const firstSlot = midnight + seconds[0] * 1000;
|
||||
const start = emptyScada ? firstSlot + (firstSlot < windowStart ? 86400_000 : 0) : [...current][0];
|
||||
return historical.some((base) => start <= base || (start - base) % 86400_000 !== 0)
|
||||
? "历史 scatter 必须来自当前窗口之前的对应日期" : null;
|
||||
}
|
||||
@@ -68,9 +73,25 @@ export function createViewSchemas(zod: typeof z = z, publication = false, valida
|
||||
const z = zod;
|
||||
|
||||
const presentationIdentifierSchema = z.string().trim().min(1).max(128);
|
||||
const scadaChartSchema = z.object({
|
||||
role: z.enum(["current", "comparison"]),
|
||||
metric: z.enum(["pressure", "flow"]),
|
||||
unit: presentationIdentifierSchema,
|
||||
window_start: createChartSampleTimeSchema(z),
|
||||
window_end: createChartSampleTimeSchema(z),
|
||||
value_field: z.enum(["monitored_value", "cleaned_value"]).optional(),
|
||||
// Populated by Server after checking observations, never a display exemption.
|
||||
gaps: z.array(z.object({
|
||||
device_id: presentationIdentifierSchema,
|
||||
day_offset: z.number().int().min(0).max(7),
|
||||
missing_count: z.number().int().min(1).max(100),
|
||||
reason: z.enum(["no_readings", "outside_frozen_scope", "outside_frozen_window", "no_scheduled_samples"])
|
||||
}).strict()).max(8).optional()
|
||||
}).strict();
|
||||
const chartSchema = z
|
||||
.object({
|
||||
title: z.string().max(200),
|
||||
scada: scadaChartSchema.optional(),
|
||||
chart_type: z.enum(["line", "bar", "time_comparison"]),
|
||||
x_axis_name: z.string().max(128).optional(),
|
||||
y_axis_name: z.string().max(128).optional(),
|
||||
@@ -80,6 +101,8 @@ export function createViewSchemas(zod: typeof z = z, publication = false, valida
|
||||
z
|
||||
.object({
|
||||
name: z.string().max(128),
|
||||
device_id: presentationIdentifierSchema.optional(),
|
||||
day_offset: z.number().int().min(0).max(7).optional(),
|
||||
type: z.enum(["line", "bar", "scatter"]).optional(),
|
||||
data: z.array(z.number().finite().nullable()).min(1).max(100),
|
||||
sample_times: z.array(createChartSampleTimeSchema(z).nullable()).min(1).max(100).optional()
|
||||
|
||||
@@ -207,6 +207,97 @@
|
||||
"type": "string",
|
||||
"maxLength": 200
|
||||
},
|
||||
"scada": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"current",
|
||||
"comparison"
|
||||
]
|
||||
},
|
||||
"metric": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pressure",
|
||||
"flow"
|
||||
]
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128
|
||||
},
|
||||
"window_start": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:[1-9][0-9]{3}|0[1-9][0-9]{2}|00[1-9][0-9]|000[1-9])-[0-9]{2}-[0-9]{2}T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$"
|
||||
},
|
||||
"window_end": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:[1-9][0-9]{3}|0[1-9][0-9]{2}|00[1-9][0-9]|000[1-9])-[0-9]{2}-[0-9]{2}T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\.[0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$"
|
||||
},
|
||||
"value_field": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"monitored_value",
|
||||
"cleaned_value"
|
||||
]
|
||||
},
|
||||
"gaps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"device_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128
|
||||
},
|
||||
"day_offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 7
|
||||
},
|
||||
"missing_count": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"no_readings",
|
||||
"outside_frozen_scope",
|
||||
"outside_frozen_window",
|
||||
"no_scheduled_samples"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"device_id",
|
||||
"day_offset",
|
||||
"missing_count",
|
||||
"reason"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"maxItems": 8
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"metric",
|
||||
"unit",
|
||||
"window_start",
|
||||
"window_end"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"chart_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -241,6 +332,16 @@
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"device_id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 128
|
||||
},
|
||||
"day_offset": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 7
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Vendored
+56
@@ -7219,6 +7219,8 @@ export interface components {
|
||||
pending_items?: string[];
|
||||
/** Recommendations */
|
||||
recommendations?: string[];
|
||||
/** Scada Device Ids */
|
||||
scada_device_ids?: string[] | null;
|
||||
/** Stage */
|
||||
stage?: ("interim" | "final") | null;
|
||||
/** Stop Reason */
|
||||
@@ -7251,6 +7253,8 @@ export interface components {
|
||||
pending_items?: string[];
|
||||
/** Recommendations */
|
||||
recommendations?: string[];
|
||||
/** Scada Device Ids */
|
||||
scada_device_ids?: string[] | null;
|
||||
/**
|
||||
* Stage
|
||||
* @enum {string}
|
||||
@@ -8505,6 +8509,53 @@ export interface components {
|
||||
*/
|
||||
start_time: string;
|
||||
};
|
||||
/** ScadaChartContext */
|
||||
ScadaChartContext: {
|
||||
/** Gaps */
|
||||
gaps?: components["schemas"]["ScadaChartGap"][];
|
||||
/**
|
||||
* Metric
|
||||
* @enum {string}
|
||||
*/
|
||||
metric: "pressure" | "flow";
|
||||
/**
|
||||
* Role
|
||||
* @enum {string}
|
||||
*/
|
||||
role: "current" | "comparison";
|
||||
/** Unit */
|
||||
unit: string;
|
||||
/**
|
||||
* Value Field
|
||||
* @default monitored_value
|
||||
* @enum {string}
|
||||
*/
|
||||
value_field: "monitored_value" | "cleaned_value";
|
||||
/**
|
||||
* Window End
|
||||
* Format: date-time
|
||||
*/
|
||||
window_end: string;
|
||||
/**
|
||||
* Window Start
|
||||
* Format: date-time
|
||||
*/
|
||||
window_start: string;
|
||||
};
|
||||
/** ScadaChartGap */
|
||||
ScadaChartGap: {
|
||||
/** Day Offset */
|
||||
day_offset: number;
|
||||
/** Device Id */
|
||||
device_id: string;
|
||||
/** Missing Count */
|
||||
missing_count: number;
|
||||
/**
|
||||
* Reason
|
||||
* @enum {string}
|
||||
*/
|
||||
reason: "no_readings" | "outside_frozen_scope" | "outside_frozen_window" | "no_scheduled_samples";
|
||||
};
|
||||
/**
|
||||
* ScadaDeviceResponse
|
||||
* @description Project SCADA metadata keyed by the canonical device identifier.
|
||||
@@ -9656,6 +9707,7 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
chart_type: "line" | "bar" | "time_comparison";
|
||||
scada?: components["schemas"]["ScadaChartContext"] | null;
|
||||
/** Series */
|
||||
series: components["schemas"]["TaskSafeChartSeries"][];
|
||||
/** Title */
|
||||
@@ -9671,6 +9723,10 @@ export interface components {
|
||||
TaskSafeChartSeries: {
|
||||
/** Data */
|
||||
data: (number | null)[];
|
||||
/** Day Offset */
|
||||
day_offset?: number | null;
|
||||
/** Device Id */
|
||||
device_id?: string | null;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Sample Times */
|
||||
|
||||
@@ -107,6 +107,7 @@ export const taskReportSchema = z.object({
|
||||
data_gaps: z.array(dataGapSchema),
|
||||
diagram: investigationDiagramSchema.nullish(),
|
||||
ui_blocks: z.array(reportUiBlockSchema).max(8).optional(),
|
||||
scada_device_ids: z.array(text.max(128)).max(1000).optional(),
|
||||
}).strict().superRefine((report, ctx) => {
|
||||
const invalid = (message: string, path: (string | number)[] = []) =>
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message, path });
|
||||
|
||||
@@ -404,6 +404,12 @@ export class OperationalTaskClientError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Feed a concrete Server coverage rejection back into the same active investigation. */
|
||||
export function scadaPresentationContinuation(error: unknown): string | null {
|
||||
if (!(error instanceof OperationalTaskClientError) || error.code !== "SCADA_PRESENTATION_INCOMPLETE") return null;
|
||||
return `Server尚未接受本轮调查结束:${error.message}\n请按返回设备补充publish_task_presentation监测图,必要时更新publish_task_report。保留目标设备与真实缺测说明,完成后再结束本轮。`;
|
||||
}
|
||||
|
||||
const parseJson = (text: string): unknown => {
|
||||
if (!text.trim()) return {};
|
||||
try {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
clearOperationalTaskLease,
|
||||
operationalTaskClient,
|
||||
releaseOperationalTaskCredential,
|
||||
scadaPresentationContinuation,
|
||||
} from "../product/operational-task-client.js";
|
||||
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||
@@ -798,7 +799,7 @@ export const registerChatStreamRoute = (
|
||||
if (messaging && (messaging.hasInputThisTurn(binding.sessionId) || !await messaging.closeInteractive(activeSessionRecord))) return false;
|
||||
return inputTracker.canClose();
|
||||
},
|
||||
beforeIdle: messaging || (sessionPurpose === "task_execution" && waitTaskCalculations) ? async () => {
|
||||
beforeIdle: messaging || sessionPurpose === "task_execution" ? async () => {
|
||||
if (sessionPurpose !== "task_execution") return messaging ? messaging.closeInteractive(activeSessionRecord) : true;
|
||||
if (await messaging?.flush(binding.sessionId)) return false;
|
||||
const calculationResume = await waitTaskCalculations?.(binding.sessionId, abortController.signal);
|
||||
@@ -818,6 +819,8 @@ export const registerChatStreamRoute = (
|
||||
if (task.lifecycle_status === "failed") throw new Error("Server rejected incomplete investigation artifacts");
|
||||
return true;
|
||||
} catch (error) {
|
||||
const continuation = scadaPresentationContinuation(error);
|
||||
if (continuation) return continuation;
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "PENDING_AGENT_MESSAGES") {
|
||||
await messaging?.flush(binding.sessionId);
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { expect, it } from "bun:test";
|
||||
import { taskPresentationCommandSchema, type TaskPresentationCommand } from "../../src/product/task-presentation.js";
|
||||
import { taskPublicationCommandSchema, taskPublicationReportSchema } from "../../.opencode/tools/task-publication-schema.js";
|
||||
import { taskReportSchema } from "../../src/product/investigation-report.js";
|
||||
import { OperationalTaskClientError, scadaPresentationContinuation } from "../../src/product/operational-task-client.js";
|
||||
|
||||
it("preserves SCADA device identities through native and host publication boundaries", () => {
|
||||
const command: TaskPresentationCommand = { action: "open_chart", chart_id: "flow-overview", evidence_refs: ["observations"], chart: {
|
||||
title: "流量当前趋势", chart_type: "line", x_data: ["05:00", "05:15"],
|
||||
scada: { role: "current", metric: "flow", unit: "m3/h", window_start: "2026-07-16T05:00:00+08:00", window_end: "2026-07-16T08:00:00+08:00" },
|
||||
series: [{ name: "524529", device_id: "524529", day_offset: 0, type: "line", data: [-6.8, 303.3],
|
||||
sample_times: ["2026-07-16T05:00:00+08:00", "2026-07-16T05:15:00+08:00"] }],
|
||||
} };
|
||||
expect(taskPresentationCommandSchema.parse(command)).toEqual(command);
|
||||
expect(taskPublicationCommandSchema.parse(command)).toEqual(command);
|
||||
const report = { title: "调查", current_conclusion: "524529仅作参考", conclusion_boundary: "待复核",
|
||||
stage: "final", outcome: "concluded", evidence_refs: ["observations"], data_gaps: [], scada_device_ids: ["524529"] };
|
||||
expect(taskReportSchema.parse(report).scada_device_ids).toEqual(["524529"]);
|
||||
expect(taskPublicationReportSchema.parse(report).scada_device_ids).toEqual(["524529"]);
|
||||
});
|
||||
|
||||
it("continues an incomplete SCADA presentation with the exact missing device instead of retrying other failures", () => {
|
||||
const rejection = new OperationalTaskClientError(409, "缺少七天同期对比设备:524529", "SCADA_PRESENTATION_INCOMPLETE");
|
||||
expect(scadaPresentationContinuation(rejection)).toContain("524529");
|
||||
expect(scadaPresentationContinuation(rejection)).toContain("publish_task_presentation");
|
||||
expect(scadaPresentationContinuation(new OperationalTaskClientError(403, "拒绝", "PERMISSION_DENIED"))).toBeNull();
|
||||
expect(scadaPresentationContinuation(new Error("transport failed"))).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
it("keeps all-null current chunks aligned to their own first slot across midnight", () => {
|
||||
for (const [start, end, labels, times] of [
|
||||
["2026-07-16T05:08:00+08:00", "2026-07-16T08:08:00+08:00", ["07:45", "08:00"],
|
||||
["2026-07-15T07:45:00+08:00", "2026-07-15T08:00:00+08:00"]],
|
||||
["2026-07-15T22:08:00+08:00", "2026-07-16T01:08:00+08:00", ["00:45", "01:00"],
|
||||
["2026-07-15T00:45:00+08:00", "2026-07-15T01:00:00+08:00"]],
|
||||
] as const) {
|
||||
const command = { action: "open_chart", chart_id: "chunk", evidence_refs: ["observations"], chart: {
|
||||
title: "流量同期分段", chart_type: "time_comparison", x_data: labels,
|
||||
scada: { role: "comparison", metric: "flow", unit: "m3/h", window_start: start, window_end: end },
|
||||
series: [{ name: "current", type: "line", data: [null, null], sample_times: [null, null] },
|
||||
{ name: "history", type: "scatter", data: [1, 2], sample_times: times }],
|
||||
} };
|
||||
expect(taskPresentationCommandSchema.safeParse(command).success).toBe(true);
|
||||
expect(taskPublicationCommandSchema.safeParse(command).success).toBe(true);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user